fix(#4141): ignore Stryker's sandbox in eslint's global ignores (#4178)

* 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/<rule>` the original carries becomes
`Definition for rule 'local/<rule>' 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017zgWzB96uz3LVKdJePYTLR

* chore(#4141): backfill the changeset PR number

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mw7WhYnrWdY8cmj8NSJj4R

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
This commit is contained in:
Adnan
2026-09-05 12:37:13 +01:00
committed by GitHub
parent 294ec29857
commit dad16b6ef9
3 changed files with 58 additions and 0 deletions

View File

@@ -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)

View File

@@ -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',

View File

@@ -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)
// ────────────────────────────────────────────────────────────────────────