'use strict'; /** * Behavioral tests for scripts/check-glossary-refs.cjs — the CONTEXT.md * glossary drift gate (#2387). * * These drive the real CLI as a subprocess against synthetic CONTEXT.md / * bin/install.js fixtures in a temp dir, asserting on exit code and emitted * text. No source-grepping: the runtime behavior is the contract. */ const { test } = require('node:test'); const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const { spawnSync } = require('node:child_process'); const { createTempDir, cleanup } = require('./helpers.cjs'); const { copyScriptWithDeps } = require('./helpers/copy-script-fixture.cjs'); const { GENERATOR_SCRIPT_TIMEOUT_MS } = require('./helpers/timeouts.cjs'); const REPO_ROOT = path.resolve(__dirname, '..'); const SCRIPT_REL = path.join('scripts', 'check-glossary-refs.cjs'); // The real bin/install.js allRuntimes array, mirrored here so fixtures can // build both a matching and a deliberately-drifted CONTEXT.md against it. const REAL_RUNTIMES = [ 'claude', 'antigravity', 'codex', 'cursor', 'opencode', 'zcode', ]; function allRuntimesSentence(count, members) { return `Runtime enum: \`allRuntimes\` (${count} values: ${members.join(', ')})`; } /** * Build a throwaway repo containing exactly what the gate reads: CONTEXT.md, * bin/install.js, a real src/ file a clean fixture can legitimately reference, * and a copy of the gate together with its transitive relative-require graph * (walked and copied by copyScriptWithDeps, not hand-listed — see that * helper's docstring for why: a hand-copied dependency list silently goes * stale the moment the script gains a new require). A unique mkdtemp per call * keeps parallel tests from colliding, and the dir is removed via `t.after()` * so a failing assertion cannot leak it. */ function makeRepo(t, { contextBody, runtimes = REAL_RUNTIMES }) { const root = createTempDir('msd-glossary-refs-'); t.after(() => cleanup(root)); fs.mkdirSync(path.join(root, 'src'), { recursive: true }); fs.mkdirSync(path.join(root, 'bin'), { recursive: true }); copyScriptWithDeps(REPO_ROOT, root, SCRIPT_REL); fs.writeFileSync(path.join(root, 'CONTEXT.md'), contextBody); fs.writeFileSync( path.join(root, 'bin', 'install.js'), `'use strict';\nconst allRuntimes = [${runtimes.map((r) => `'${r}'`).join(', ')}];\nmodule.exports = { allRuntimes };\n`, ); // A real file a clean CONTEXT.md fixture can legitimately reference. fs.writeFileSync(path.join(root, 'src', 'real-module.cts'), '// fixture\n'); return root; } /** Run the gate in `root`; never throws — returns {status, stdout, stderr}. */ function run(root, args = []) { const res = spawnSync(process.execPath, [path.join(root, SCRIPT_REL), ...args], { cwd: root, encoding: 'utf8', timeout: GENERATOR_SCRIPT_TIMEOUT_MS, }); if (res.error) throw res.error; return { status: res.status, stdout: res.stdout || '', stderr: res.stderr || '' }; } test('a clean CONTEXT.md whose refs resolve and whose allRuntimes matches passes --check', (t) => { const context = [ '# Context', '', 'See `src/real-module.cts` for details.', '', `${allRuntimesSentence(6, REAL_RUNTIMES)}.`, '', ].join('\n'); const root = makeRepo(t, { contextBody: context }); const res = run(root, ['--check']); assert.equal(res.status, 0, `expected a clean pass: ${res.stderr}`); assert.match(res.stdout, /glossary references are current/); }); test('a reference to a nonexistent tracked file fails --check and names the token', (t) => { const context = [ '# Context', '', 'See `src/does-not-exist.cts` for details.', '', `${allRuntimesSentence(6, REAL_RUNTIMES)}.`, '', ].join('\n'); const root = makeRepo(t, { contextBody: context }); const res = run(root, ['--check']); assert.equal(res.status, 1); assert.match(res.stderr, /src\/does-not-exist\.cts/); }); test('allRuntimes count and membership drift is caught', (t) => { // Mirrors the real-world case: CONTEXT.md says 4 while bin/install.js has 6. const claimed4 = REAL_RUNTIMES.filter((r) => r !== 'cursor' && r !== 'zcode'); assert.equal(claimed4.length, 4); const context = [ '# Context', '', 'See `src/real-module.cts` for details.', '', `${allRuntimesSentence(4, claimed4)}.`, '', ].join('\n'); const root = makeRepo(t, { contextBody: context }); // bin/install.js defaults to the real 6 const res = run(root, ['--check']); assert.equal(res.status, 1); assert.match(res.stderr, /claims 4 values/); assert.match(res.stderr, /has 6/); assert.match(res.stderr, /cursor/); assert.match(res.stderr, /zcode/); }); test('a reference to a nonexistent msd-core/bin/lib/*.cjs path is skipped (generated, gitignored)', (t) => { const context = [ '# Context', '', 'Generated router lives at `msd-core/bin/lib/does-not-exist.cjs`.', '', `${allRuntimesSentence(6, REAL_RUNTIMES)}.`, '', ].join('\n'); const root = makeRepo(t, { contextBody: context }); const res = run(root, ['--check']); assert.equal(res.status, 0, `generated bin/lib path must be skipped, not asserted missing: ${res.stderr}`); }); test('an intentionally-absent documented path is skipped, not asserted missing', (t) => { // #2778. `tests/emitted-drift-ack.json` (ADR-2719 §3) is absent on a healthy // `next` BY DESIGN — it appears only inside a PR that needs it, which is what // makes touching it the alarm. Asserting its existence turns the correct state // into a drift finding. Under `tests/` it would otherwise be tracked, so this // is a real exemption rather than a prefix accident. const context = [ '# Context', '', 'The escape hatch is a committed acknowledgment (`tests/emitted-drift-ack.json`).', '', `${allRuntimesSentence(6, REAL_RUNTIMES)}.`, '', ].join('\n'); const root = makeRepo(t, { contextBody: context }); const res = run(root, ['--check']); assert.equal( res.status, 0, `an intentionally-absent path must be skipped, not asserted missing: ${res.stderr}`, ); }); test('a sibling tests/ path that is merely missing is still caught', (t) => { // The exemption must be exact, not a blanket hole under `tests/`. A different // missing tests/ file must still fail, or #2778's fix would disarm the gate for // the whole directory. const context = [ '# Context', '', 'See `tests/emitted-drift-ack-typo.json` for details.', '', `${allRuntimesSentence(6, REAL_RUNTIMES)}.`, '', ].join('\n'); const root = makeRepo(t, { contextBody: context }); const res = run(root, ['--check']); assert.equal(res.status, 1, 'a genuinely missing tests/ path must still be a finding'); assert.match(`${res.stdout}${res.stderr}`, /emitted-drift-ack-typo\.json/); }); test('a ~/-rooted path and a bare filename are both skipped', (t) => { const context = [ '# Context', '', 'See `~/.claude/x.md` and `core.cjs` for details.', '', `${allRuntimesSentence(6, REAL_RUNTIMES)}.`, '', ].join('\n'); const root = makeRepo(t, { contextBody: context }); const res = run(root, ['--check']); assert.equal(res.status, 0, `home path and bare filename must be skipped, not asserted missing: ${res.stderr}`); }); test('a `..`-traversal token cannot escape ROOT into a filesystem-existence probe', (t) => { // Security review finding: `src/../../../etc/passwd` passes PATH_TOKEN_RE (`.` // is a legal segment char) and the `src/` prefix, so without a confinement // check `path.join(ROOT, token)` normalizes out of the tree and existsSync // probes it — a doc lint turned filesystem oracle. It must be skipped, so the // gate neither errors nor reports a finding about an out-of-tree path. const context = [ '# Context', '', 'Escape attempt: `src/../../../../../../etc/passwd` and `src/../../etc/hosts`.', '', `${allRuntimesSentence(6, REAL_RUNTIMES)}.`, '', ].join('\n'); const root = makeRepo(t, { contextBody: context }); const res = run(root, ['--check']); assert.equal(res.status, 0, `..-traversal tokens must be confined to ROOT and skipped: ${res.stderr}`); assert.doesNotMatch(res.stderr, /etc\/passwd|etc\/hosts/, 'a confined gate must not name out-of-tree paths'); }); test('the real script runs cleanly against the real repo without crashing', () => { // Does NOT assert the exit code — CONTEXT.md may still be mid-edit — only // that the gate itself runs to a real verdict (0 or 1), not an unhandled // crash (no status / non-2-shaped exit). const res = spawnSync(process.execPath, [path.join(REPO_ROOT, SCRIPT_REL), '--check'], { cwd: REPO_ROOT, encoding: 'utf8', timeout: GENERATOR_SCRIPT_TIMEOUT_MS, }); assert.equal(res.error, undefined, `spawn must not error: ${res.error}`); assert.ok(res.status === 0 || res.status === 1, `expected exit 0 or 1, got ${res.status} (stderr: ${res.stderr})`); }); // --------------------------------------------------------------------------- // #3604 — backtick-pairing parity. extractTrackedRefs paired backticks in one // whole-text pass, so (a) an odd-backtick line (the RULESET.* predicate format // is backtick-wrapped and its values sometimes contain backticks) shifted the // pairing of every later line, and (b) the fact-store predicate lines wrap the // whole `CLASS.subkey=value` in ONE pair, hiding the real tests/… paths inside // the span's value. A broken ref the gate cannot see is a gate that reports a // false clean — the exact luck-based pass #2778's docstring warned about. // --------------------------------------------------------------------------- test('a broken tracked reference after a RULESET predicate with inner backticks is caught (#3604)', (t) => { // The predicate line carries an ODD backtick count (outer pair + one inner // unpaired tick), which desynchronized the old whole-text pairing. The broken // ref on a LATER line must still be found. const context = [ '# Context', '', "`RULESET.TESTS.inner=this value breaks ` pairing for everything after it`", '', 'Coverage lives in `src/does-not-exist.cts`.', '', `${allRuntimesSentence(6, REAL_RUNTIMES)}.`, '', ].join('\n'); const root = makeRepo(t, { contextBody: context }); const lf = run(root, ['--check']); assert.equal(lf.status, 1, 'a broken ref after a desynchronizing RULESET predicate must fail --check'); // CRLF replay: the same verdict under \r\n (recurring class #1658/#1668/#2206). const rootCrlf = makeRepo(t, { contextBody: context.replace(/\n/g, '\r\n') }); const crlf = run(rootCrlf, ['--check']); assert.equal(crlf.status, 1, 'CRLF input must yield the same verdict as LF'); }); test('tracked paths inside an outer-wrapped predicate span are extracted (#3604)', (t) => { // The line-647 shape: one backtick pair wraps the whole predicate, and the // VALUE carries two real paths. The resolving one must pass; the missing one // must be named. The old extractor read the whole span as one token (spaces // → rejected) and saw neither. const context = [ '# Context', '', '`WORKTREE.SEAM.demo-anchor=src/real-module.cts + tests/missing-anchor.test.cjs`', '', `${allRuntimesSentence(6, REAL_RUNTIMES)}.`, '', ].join('\n'); const root = makeRepo(t, { contextBody: context }); const res = run(root, ['--check']); assert.equal(res.status, 1, 'a missing path inside a predicate value must fail --check'); assert.match(`${res.stdout}${res.stderr}`, /tests\/missing-anchor\.test\.cjs/, 'the finding must name the missing token, not the enclosing span'); assert.doesNotMatch(`${res.stdout}${res.stderr}`, /real-module/, 'the resolving sibling in the same span must not be reported'); }); test('glob and NNNN template mentions are not drift (#3604)', (t) => { // `scripts/gen-*.cjs`-style globs and the documented ADR filename template // (`docs/adr/NNNN-*.md`, CONTRIBUTING's "Do not compute a next number // locally") are legitimate mentions. A fragment ending in `-` or `.` is a // glob/template remnant, never a real path. The bare template shape // (`docs/adr/NNNN.md`, no dash-star) ends in a word char and ends the // final-char guard's reach — only the NNNN skip covers it. const context = [ '# Context', '', '`RULESET.AUDIT.demo=search src/*.cts OR the scripts/gen-*.cjs generator`', '`RULESET.ADR-HEADER=every docs/adr/NNNN-*.md must open with Status and Date`', 'A fresh ADR starts from `docs/adr/NNNN.md`.', 'Retired scanners were `scripts/lint-*.cjs`.', '', `${allRuntimesSentence(6, REAL_RUNTIMES)}.`, '', ].join('\n'); const root = makeRepo(t, { contextBody: context }); const res = run(root, ['--check']); assert.equal(res.status, 0, `glob and NNNN template fragments must be skipped: ${res.stderr}`); }); test('retired-path exemptions are exact, not a blanket hole (#3604)', (t) => { // The emitted-attribution family retired by the #2724 cutover is documented // in CONTEXT.md as HISTORY ("Historically tests/fixtures/...") — absence is // the healthy steady state, the same semantics as the #2778 entry. But the // exemption must stay exact: a sibling missing tests/ path still fails. // // The NOT-mention must ride a PREDICATE span, mirroring CONTEXT.md's real // shape (RULESET.TESTS.eslint-harness): inside a predicate value the // sub-scan harvests `scripts/eslint-rules` WITHOUT the trailing slash, so // it is tracked-prefix-shaped and only the exemption saves it. Standalone // spans with trailing slashes never reach the exemption (PATH_TOKEN_RE // rejects them), which is exactly the vacuity this test must not have. const context = [ '# Context', '', 'Historically `tests/golden-install-parity.test.cjs` and `tests/workflow-size-baseline.json`.', '`RULESET.TESTS.harness=local plugin at eslint-rules/ (repo root, NOT scripts/eslint-rules/)`', 'A typo sibling `tests/golden-install-parity-typo.test.cjs` is real drift.', '', `${allRuntimesSentence(6, REAL_RUNTIMES)}.`, '', ].join('\n'); const root = makeRepo(t, { contextBody: context }); const res = run(root, ['--check']); assert.equal(res.status, 1, 'the sibling typo must still be a finding'); assert.match(`${res.stdout}${res.stderr}`, /golden-install-parity-typo/); assert.doesNotMatch(`${res.stdout}${res.stderr}`, /scripts\/eslint-rules/, 'the contrastive NOT-mention inside a predicate value must be exempt'); assert.doesNotMatch(`${res.stdout}${res.stderr}`, /workflow-size-baseline/, 'the retired-path mention must be exempt'); });