diff --git a/eslint.config.mjs b/eslint.config.mjs index 64e69f752..5bda479b9 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -22,22 +22,6 @@ const localPlugin = { }, }; -// Generated bin/lib files — never lint -const GENERATED_CJS_IGNORES = [ - 'get-shit-done/bin/lib/command-aliases.cjs', - 'get-shit-done/bin/lib/configuration.cjs', - 'get-shit-done/bin/lib/decisions.cjs', - 'get-shit-done/bin/lib/phase-lifecycle.cjs', - 'get-shit-done/bin/lib/plan-scan.cjs', - 'get-shit-done/bin/lib/project-root.cjs', - 'get-shit-done/bin/lib/schema-detect.cjs', - 'get-shit-done/bin/lib/secrets.cjs', - 'get-shit-done/bin/lib/state-document.cjs', - 'get-shit-done/bin/lib/validate.cjs', - 'get-shit-done/bin/lib/workstream-inventory-builder.cjs', - 'get-shit-done/bin/lib/workstream-name-policy.cjs', -]; - const sdkSrcExists = existsSync(join(__dirname, 'sdk', 'src')); export default tseslint.config( @@ -53,7 +37,6 @@ export default tseslint.config( '**/*.generated.cjs', // ADR-457: tsc-generated runtime artifact — lint the src/*.cts source, not the emitted .cjs. 'get-shit-done/bin/lib/semver-compare.cjs', - ...GENERATED_CJS_IGNORES, ], }, @@ -77,7 +60,6 @@ export default tseslint.config( // ── get-shit-done/bin/**/*.cjs + scripts/**/*.cjs ─────────────────────────── // CommonJS Node files: js.recommended + eslint-plugin-n + local plugin rules - // Type-aware via parserOptions.project=tsconfig.lint.json where applicable { files: ['get-shit-done/bin/**/*.cjs', 'scripts/**/*.cjs'], plugins: { diff --git a/tests/551-eslint-bin-lib-coverage.test.cjs b/tests/551-eslint-bin-lib-coverage.test.cjs new file mode 100644 index 000000000..01fcdc365 --- /dev/null +++ b/tests/551-eslint-bin-lib-coverage.test.cjs @@ -0,0 +1,94 @@ +'use strict'; + +/** + * Regression test for #551 — the ESLint harness (ADR-452) silently excluded 12 + * hand-written `get-shit-done/bin/lib/*.cjs` modules via a `GENERATED_CJS_IGNORES` + * list mislabeled "Generated bin/lib files — never lint". The files are hand-written + * (no `@generated` header, no generator emits them), so they belong in the harness. + * + * Invariant under test (not just today's file list): a `bin/lib/*.cjs` module must + * be linted UNLESS it is genuinely generated — i.e. it carries an `@generated` + * header or has a `src/.cts|.ts` source it is compiled from (ADR-457). This + * catches the next mislabeled file, not only the original 12. + * + * Behavior is checked through ESLint's own `isPathIgnored` API so the test reflects + * real resolved flat-config precedence, not a textual scan of eslint.config.mjs. + */ + +const { describe, test, before } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const { ESLint } = require('eslint'); + +const ROOT = path.resolve(__dirname, '..'); +const LIB_DIR = path.join(ROOT, 'get-shit-done', 'bin', 'lib'); + +// The 12 modules that #551 restored to lint coverage. +const HAND_WRITTEN = [ + 'command-aliases', + 'configuration', + 'decisions', + 'phase-lifecycle', + 'plan-scan', + 'project-root', + 'schema-detect', + 'secrets', + 'state-document', + 'validate', + 'workstream-inventory-builder', + 'workstream-name-policy', +].map((name) => path.join(LIB_DIR, `${name}.cjs`)); + +function isGenerated(absPath) { + if (!fs.existsSync(absPath)) return false; + const head = fs.readFileSync(absPath, 'utf8').slice(0, 500); + if (/@generated/.test(head)) return true; + const base = path.basename(absPath, '.cjs'); + return ( + fs.existsSync(path.join(ROOT, 'src', `${base}.cts`)) || + fs.existsSync(path.join(ROOT, 'src', `${base}.ts`)) + ); +} + +let eslint; +before(() => { + eslint = new ESLint({ cwd: ROOT }); +}); + +describe('#551: ESLint covers hand-written bin/lib/*.cjs', () => { + for (const file of HAND_WRITTEN) { + test(`lints ${path.basename(file)} (not ignored)`, async () => { + assert.equal( + await eslint.isPathIgnored(file), + false, + `${path.relative(ROOT, file)} is hand-written and must be linted, not ignored`, + ); + }); + } + + test('no hand-written bin/lib/*.cjs is silently ignored', async () => { + const offenders = []; + for (const entry of fs.readdirSync(LIB_DIR)) { + if (!entry.endsWith('.cjs')) continue; + const abs = path.join(LIB_DIR, entry); + if (isGenerated(abs)) continue; // legitimately excluded from the harness + if (await eslint.isPathIgnored(abs)) offenders.push(entry); + } + assert.deepEqual( + offenders, + [], + `Hand-written modules silently excluded from ESLint: ${offenders.join(', ')}`, + ); + }); + + test('genuinely tsc-generated semver-compare.cjs stays ignored (ADR-457)', async () => { + // Publish-time artifact compiled from src/semver-compare.cts; must not be linted. + const f = path.join(LIB_DIR, 'semver-compare.cjs'); + assert.equal( + await eslint.isPathIgnored(f), + true, + 'semver-compare.cjs is a tsc-generated publish-time artifact and must stay ignored', + ); + }); +});