From dad16b6ef9a908cf48a09387485a2a0eb8d74bcc Mon Sep 17 00:00:00 2001 From: Adnan Date: Sat, 5 Sep 2026 12:37:13 +0100 Subject: [PATCH] fix(#4141): ignore Stryker's sandbox in eslint's global ignores (#4178) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(#4141): ignore Stryker's sandbox in eslint's global ignores `stryker.config.mjs` sets `tempDirName: '.stryker-tmp'`, `.gitignore` ignores it, and Stryker's own always-ignored list carries it. eslint's global `ignores` did not, and there is no `.eslintignore`. A mutation run that dies before cleanup — chunk timeout, CI cancellation, Ctrl+C, a crash — leaves a full sandbox copy of the tree at `.stryker-tmp/sandbox-*/`, and `npm run lint` then lints the copy. The failure is not the duplicate-file failure that would suggest, which is what makes it confusing: the `local` plugin is registered only in path-scoped config blocks (`tests/**/*.cjs`, `scripts/**/*.cjs`, `hooks/**`), and a copy at `.stryker-tmp/sandbox-*/tests/*.cjs` matches none of them, so every inline `// eslint-disable-next-line local/` the original carries becomes `Definition for rule 'local/' was not found` at the copy's path. The error text names rules rather than paths, so the first read is "my change broke the local plugin" — observed as 834 errors on a branch whose own diff was clean, from a sandbox eight days stale. CI never sees this (fresh checkout), so it is purely a local-contributor tax, but a loud and misleading one. The regression test asserts the invariant the way ESLint resolves it — `isPathIgnored()` over real flat-config precedence, not a textual scan of the ignores array — mirroring the #551 block it sits beside, and carries the control that the real `tests/worktree.test.cjs` at the mirrored path is still linted, so a pattern widened enough to swallow the tree cannot pass. Scoped to the reported bug: `reports/` may deserve the same treatment, but no lint failure has been reproduced from it, so it is not claimed here. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017zgWzB96uz3LVKdJePYTLR * chore(#4141): backfill the changeset PR number Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_017zgWzB96uz3LVKdJePYTLR * chore(#4141): restore the file's 2-blank-line block separator Review nit: the #4141 block left three blank lines before the RETIRED block, where every other block boundary in this file uses two. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Mw7WhYnrWdY8cmj8NSJj4R --------- Co-authored-by: Claude Opus 5 Co-authored-by: Tom Boucher --- .changeset/kind-finches-hum.md | 5 ++++ eslint.config.mjs | 5 ++++ tests/repo-invariants.test.cjs | 48 ++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 .changeset/kind-finches-hum.md diff --git a/.changeset/kind-finches-hum.md b/.changeset/kind-finches-hum.md new file mode 100644 index 000000000..906448d4d --- /dev/null +++ b/.changeset/kind-finches-hum.md @@ -0,0 +1,5 @@ +--- +type: Fixed +pr: 4178 +--- +**`npm run lint` no longer fails on a leftover Stryker sandbox** — `eslint.config.mjs` now ignores `.stryker-tmp/**`, the scratch directory Stryker itself, `.gitignore` and `stryker.config.mjs` already treat as disposable. A mutation run interrupted before cleanup used to leave a copy of the tree there, and linting that copy reported the path-scoped `local/*` rules as undefined — hundreds of "Definition for rule … was not found" errors on a clean branch. (#4141) diff --git a/eslint.config.mjs b/eslint.config.mjs index 7b7435964..0b6bb13f1 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -78,6 +78,11 @@ export default tseslint.config( '.worktrees/**', '.claude/**', 'coverage/**', + // #4141: Stryker's sandbox (tempDirName in stryker.config.mjs, also gitignored + // and always-ignored by Stryker itself). A run that dies before cleanup leaves a + // copy of the tree here; linting it reports the path-scoped `local/*` rules as + // undefined, which reads as the plugin being broken rather than as scratch space. + '.stryker-tmp/**', '**/*.generated.cjs', // ADR-457: tsc-generated runtime artifact — lint the src/*.cts source, not the emitted .cjs. 'gsd-core/bin/lib/claude-orchestration.cjs', diff --git a/tests/repo-invariants.test.cjs b/tests/repo-invariants.test.cjs index 822b94163..d1567037e 100644 --- a/tests/repo-invariants.test.cjs +++ b/tests/repo-invariants.test.cjs @@ -124,6 +124,54 @@ describe('ESLint coverage tracks the bin/lib TS migration (ADR-457 / #537)', () } +// ──────────────────────────────────────────────────────────────────────── +// #4141 — Stryker's sandbox is scratch space and must not be linted +// ──────────────────────────────────────────────────────────────────────── +{ + const { describe: __d, test: __t, before: __b } = require('node:test'); + const __assert = require('node:assert/strict'); + const __path = require('node:path'); + const { ESLint: __ESLint } = require('eslint'); + + const __ROOT = __path.resolve(__dirname, '..'); + // `stryker.config.mjs` sets tempDirName '.stryker-tmp', .gitignore ignores it, and + // Stryker itself always ignores it. A run that dies before cleanup (chunk timeout, + // CI cancellation, Ctrl+C) leaves a whole sandboxed copy of the tree behind, and + // `npm run lint` then lints the copy: the `local/*` plugin is registered only in + // path-scoped blocks (tests/**, scripts/**, hooks/**), none of which match + // `.stryker-tmp/sandbox-*/…`, so every inline `local/…` disable comment in the copy + // becomes "Definition for rule … was not found". Verified as ESLint resolves it, not + // as a textual scan of the ignores array — same approach as the #551 block above. + const __SANDBOX_FILE = '.stryker-tmp/sandbox-nBAAwa/tests/worktree.test.cjs'; + const __REAL_FILE = 'tests/worktree.test.cjs'; + + let __eslint; + __b(() => { + __eslint = new __ESLint({ cwd: __ROOT }); + }); + + __d("ESLint ignores Stryker's sandbox (#4141)", () => { + __t('a file inside .stryker-tmp is ignored', async () => { + __assert.equal( + await __eslint.isPathIgnored(__path.join(__ROOT, __SANDBOX_FILE)), + true, + `${__SANDBOX_FILE} is a Stryker sandbox copy and must never be linted`, + ); + }); + + // Control: without this the assertion above would also pass if the ignore + // pattern were widened enough to swallow the real tree. + __t('the real file at the mirrored path is still linted', async () => { + __assert.equal( + await __eslint.isPathIgnored(__path.join(__ROOT, __REAL_FILE)), + false, + `${__REAL_FILE} is committed source and must stay linted`, + ); + }); + }); +} + + // ──────────────────────────────────────────────────────────────────────── // RETIRED: folded:bug-3054-stale-gsd-next-references (consolidation epic #1969 B8 #1977) // ────────────────────────────────────────────────────────────────────────