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