Batch 9 of 17 in the ad hoc timeout literal migration (epic #4445). Replaces every bare numeric timeout/timeoutMs object-literal property in 14 files with a named constant, per eslint-rules/no-adhoc-timeout-literal.cjs. Removes the 14 files from the rule's allowlist. The issue's own guess ("all run a scripts/*.cjs generator or lint script once, BUILD_TIMEOUT_MS class") needed two corrections found by reading every site directly. First, BUILD_TIMEOUT_MS's own doc comment scopes it specifically to scripts/build-hooks.js, which none of this batch's generator/lint-script sites run — a new shared constant, GENERATOR_SCRIPT_TIMEOUT_MS, covers the class instead. Second, no-pending-3212-markers.test.cjs's single site spawns `git ls-files` directly, not a scripts/*.cjs script at all — routed to a second new shared constant, REAL_REPO_GIT_TIMEOUT_MS, promoted once emitted-attribution.test.cjs's own git-plumbing sites were found sharing the same class and value. Isolated Standards-axis review caught a further misclassification: one of REAL_REPO_GIT_TIMEOUT_MS's three emitted-attribution.test.cjs sites actually builds a fresh throwaway temp repo (createTempDir + git init), contradicting that constant's own real-repo-tree-only scope. Fixed with a new file-local FRESH_FIXTURE_GIT_TIMEOUT_MS holding the exact pre-existing value under an honest name, rather than reusing the shared GIT_FIXTURE_TIMEOUT_MS (which would have doubled the bound). emitted-attribution.test.cjs also gets two more file-local constants: HEAVY_REAL_TREE_TEST_TIMEOUT_MS (node:test's own per-test timeout option, not a spawn bound) and BUILD_HOOKS_UNDER_LOAD_TIMEOUT_MS (the same build-hooks.js script as the shared norm, at 4x its bound inside the suite's heaviest test). emitted-ack-trailer.test.cjs gets IMPOSSIBLY_SHORT_GIT_TIMEOUT_MS — the one value in this migration that is deliberately tiny (20ms), used to force a timeout in a negative test, not generous headroom. No src/bin file touched, no numeric value changed anywhere. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
450 lines
20 KiB
JavaScript
450 lines
20 KiB
JavaScript
// docs-guard-exempt: docs/CONTEXT-INDEX.json is mentioned only in module-doc comments; the test reads root CONTEXT.md and mocked fs, never a real docs/ path.
|
|
'use strict';
|
|
|
|
/**
|
|
* Integration tests for scripts/gen-context-index.cjs — the CI gate that
|
|
* keeps docs/CONTEXT-INDEX.json in sync with the predicates declared in the
|
|
* repo-root CONTEXT.md (ADR-1671, #2928 Phase 1, rows F1-F17).
|
|
*
|
|
* The committed artifact is plain JSON (not a `.cjs` CommonJS module): a
|
|
* shipped runtime module is the wrong place for ~120 KB of arbitrary
|
|
* CONTEXT.md prose, and embedding it there tripped both
|
|
* tests/cline-install.test.cjs (leaked `.claude/hooks/...` path literals) and
|
|
* tests/package-name-single-source.test.cjs (hardcoded package-name
|
|
* literals) — both true positives against runtime-code content scanning.
|
|
* docs/CONTEXT-INDEX.json mirrors docs/INVENTORY-MANIFEST.json's precedent:
|
|
* a committed, generated, `--check`-guarded JSON manifest that is not
|
|
* runtime code.
|
|
*
|
|
* Fixture isolation (ADR-1671 Phase 1 commit 3): gen-context-index.cjs now
|
|
* accepts `--context-path <p>` / `--index-path <p>` CLI overrides (and the
|
|
* same-named parameters on the exported `checkReport`/`buildFreshIndex`
|
|
* pure functions), so every test here spawns the real CLI (spawnSync, not an
|
|
* engine-direct call — an engine-direct call is false-green for CLI behavior
|
|
* per the design's own risk analysis) pointed directly at temp fixture
|
|
* files, with NO fs monkeypatching. The prior `--require` preload
|
|
* (tests/helpers/gen-context-index-fs-fixture.cjs) redirected two hardcoded
|
|
* absolute paths by patching fs.readFileSync/existsSync/writeFileSync — that
|
|
* indirection is no longer needed now that the paths are directly
|
|
* injectable, and the preload has been deleted.
|
|
*
|
|
* Prohibited: Raw Text Matching on Test Outputs (CONTRIBUTING.md). This
|
|
* generator's `--check --json` mode emits a typed `{ ok, reason, duplicates,
|
|
* count, classes }` report — `reason` is always one of the frozen `REASON`
|
|
* enum values. Rows F7-F10 assert on `report.reason === REASON.FAIL_X`
|
|
* (and, for F7, that `report.duplicates` names the duplicate id) instead of
|
|
* exit-code-only / stderr-substring assertions.
|
|
*/
|
|
|
|
const { describe, test, beforeEach, afterEach } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const { execFileSync } = require('node:child_process');
|
|
|
|
const { createTempDir, cleanup } = require('./helpers.cjs');
|
|
const { serializeIndex, buildFreshIndex, checkReport, REASON } = require('../scripts/gen-context-index.cjs');
|
|
const { GENERATOR_SCRIPT_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
|
|
|
|
const ROOT = path.resolve(__dirname, '..');
|
|
const SCRIPT = path.join(ROOT, 'scripts', 'gen-context-index.cjs');
|
|
const REAL_CONTEXT_PATH = path.join(ROOT, 'CONTEXT.md');
|
|
|
|
const STACK_FRAME_RE = /\n\s+at\s+\S+\s+\(.*:\d+:\d+\)/;
|
|
|
|
/**
|
|
* Spawn the real gen-context-index.cjs CLI with explicit `--context-path` /
|
|
* `--index-path` overrides — no fs monkeypatching, no `--require` preload.
|
|
*
|
|
* @param {string[]} args - CLI args (e.g. ['--check', '--json']).
|
|
* @param {{contextPath?: string, indexPath?: string}} [paths] - absolute
|
|
* fixture paths to pass via `--context-path`/`--index-path`. Omit a key to
|
|
* leave that seam at its real-repo default (read-only, untouched).
|
|
* @returns {{code: number, stdout: string, stderr: string}}
|
|
*/
|
|
function runGenContextIndex(args, paths = {}) {
|
|
const fullArgs = [...args];
|
|
if (paths.contextPath !== undefined) fullArgs.push('--context-path', paths.contextPath);
|
|
if (paths.indexPath !== undefined) fullArgs.push('--index-path', paths.indexPath);
|
|
|
|
try {
|
|
const stdout = execFileSync(process.execPath, [SCRIPT, ...fullArgs], {
|
|
cwd: ROOT,
|
|
encoding: 'utf8',
|
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
timeout: GENERATOR_SCRIPT_TIMEOUT_MS,
|
|
});
|
|
return { code: 0, stdout, stderr: '' };
|
|
} catch (err) {
|
|
return {
|
|
code: err.status ?? 1,
|
|
stdout: err.stdout ? err.stdout.toString() : '',
|
|
stderr: err.stderr ? err.stderr.toString() : '',
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse the single JSON line `--check --json` writes to stdout.
|
|
*
|
|
* @param {string} stdout
|
|
* @returns {object}
|
|
*/
|
|
function parseJsonReport(stdout) {
|
|
return JSON.parse(stdout.trim());
|
|
}
|
|
|
|
describe('gen-context-index.cjs REASON enum (three-coordinated-changes lock)', () => {
|
|
test('REASON key set is exactly the documented set', () => {
|
|
// Locks the documented enum shape (CONTRIBUTING.md three-coordinated-
|
|
// changes pattern): adding a reason requires updating this assertion
|
|
// too, so the typed surface cannot silently drift from what tests expect.
|
|
assert.deepEqual(Object.keys(REASON).sort(), [
|
|
'FAIL_CONTEXT_MISSING',
|
|
'FAIL_CONTEXT_UNREADABLE',
|
|
'FAIL_DUPLICATE_IDS',
|
|
'FAIL_INDEX_MISSING',
|
|
'FAIL_INDEX_UNPARSEABLE',
|
|
'FAIL_LIB_NOT_BUILT',
|
|
'FAIL_STALE',
|
|
'OK_UP_TO_DATE',
|
|
]);
|
|
});
|
|
|
|
test('REASON is frozen', () => {
|
|
assert.ok(Object.isFrozen(REASON));
|
|
});
|
|
});
|
|
|
|
describe('gen-context-index.cjs --check (F)', () => {
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempDir('gen-context-index-');
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('checkExitsZeroWhenIndexIsFresh', () => {
|
|
// Read-only against the real, already-fresh repo state — no override
|
|
// needed, and nothing is mutated.
|
|
const r = runGenContextIndex(['--check']);
|
|
assert.equal(r.code, 0);
|
|
});
|
|
|
|
test('checkExitsZeroAfterPureLineShift', () => {
|
|
// S5: the committed artifact carries no `line` field, so a pure line
|
|
// shift in CONTEXT.md must not perturb the byte-identical serialization.
|
|
const real = fs.readFileSync(REAL_CONTEXT_PATH, 'utf8');
|
|
const lines = real.split(/\r?\n/);
|
|
const shifted = [lines[0], '', '', ...lines.slice(1)].join('\n');
|
|
const shiftedPath = path.join(tmpDir, 'CONTEXT-shifted.md');
|
|
fs.writeFileSync(shiftedPath, shifted, 'utf8');
|
|
|
|
const r = runGenContextIndex(['--check'], { contextPath: shiftedPath });
|
|
assert.equal(r.code, 0, 'a pure line shift must not fail the gate (Q4 resolution, S5)');
|
|
});
|
|
|
|
test('checkExitsOneWhenPredicateValueChanged', () => {
|
|
const real = fs.readFileSync(REAL_CONTEXT_PATH, 'utf8');
|
|
const modified = real.replace(
|
|
// eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own maintainer-authored CONTEXT.md, bounded, not adversarial input
|
|
/`RULESET\.PR-SCOPE\.one-concern-per-pr=[^`]*`/,
|
|
'`RULESET.PR-SCOPE.one-concern-per-pr=CHANGED VALUE FOR TEST`',
|
|
);
|
|
assert.notEqual(modified, real, 'fixture setup sanity: the substitution must actually apply');
|
|
const modifiedPath = path.join(tmpDir, 'CONTEXT-value-changed.md');
|
|
fs.writeFileSync(modifiedPath, modified, 'utf8');
|
|
|
|
const r = runGenContextIndex(['--check'], { contextPath: modifiedPath });
|
|
assert.equal(r.code, 1);
|
|
});
|
|
|
|
test('checkExitsOneWhenPredicateAdded', () => {
|
|
const real = fs.readFileSync(REAL_CONTEXT_PATH, 'utf8');
|
|
const added = real + '\n`ZZZTEST.added-by-test=value`\n';
|
|
const addedPath = path.join(tmpDir, 'CONTEXT-added.md');
|
|
fs.writeFileSync(addedPath, added, 'utf8');
|
|
|
|
const r = runGenContextIndex(['--check'], { contextPath: addedPath });
|
|
assert.equal(r.code, 1);
|
|
});
|
|
|
|
test('checkExitsOneWhenPredicateRemoved', () => {
|
|
const real = fs.readFileSync(REAL_CONTEXT_PATH, 'utf8');
|
|
// eslint-disable-next-line local/no-unbounded-quantifier -- parses this repo's own maintainer-authored CONTEXT.md, bounded, not adversarial input
|
|
const removed = real.replace(/`RULESET\.PR-SCOPE\.one-concern-per-pr=[^`]*`\r?\n/, '');
|
|
assert.notEqual(removed, real, 'fixture setup sanity: the removal must actually apply');
|
|
const removedPath = path.join(tmpDir, 'CONTEXT-removed.md');
|
|
fs.writeFileSync(removedPath, removed, 'utf8');
|
|
|
|
const r = runGenContextIndex(['--check'], { contextPath: removedPath });
|
|
assert.equal(r.code, 1);
|
|
});
|
|
|
|
test('checkExitsOneWhenClassSetChanged', () => {
|
|
const real = fs.readFileSync(REAL_CONTEXT_PATH, 'utf8');
|
|
const classGained = real + '\n`BRANDNEWCLASSFORTEST.x=y`\n';
|
|
const classGainedPath = path.join(tmpDir, 'CONTEXT-class-gained.md');
|
|
fs.writeFileSync(classGainedPath, classGained, 'utf8');
|
|
|
|
const r = runGenContextIndex(['--check'], { contextPath: classGainedPath });
|
|
assert.equal(r.code, 1);
|
|
});
|
|
|
|
test('checkExitsOneAndNamesDuplicateIdentifier (F7)', () => {
|
|
const real = fs.readFileSync(REAL_CONTEXT_PATH, 'utf8');
|
|
const dupPath = path.join(tmpDir, 'CONTEXT-dup.md');
|
|
const dupIntroduced = real + '\n`RULESET.PR-SCOPE.one-concern-per-pr=duplicate copy for test`\n';
|
|
fs.writeFileSync(dupPath, dupIntroduced, 'utf8');
|
|
|
|
const r = runGenContextIndex(['--check', '--json'], { contextPath: dupPath });
|
|
assert.equal(r.code, 1);
|
|
assert.doesNotMatch(r.stderr, STACK_FRAME_RE, 'no bare stack trace in non-debug failure output');
|
|
|
|
const report = parseJsonReport(r.stdout);
|
|
assert.equal(report.ok, false);
|
|
assert.equal(report.reason, REASON.FAIL_DUPLICATE_IDS);
|
|
assert.ok(
|
|
report.duplicates.some((d) => d.id === 'RULESET.PR-SCOPE.one-concern-per-pr'),
|
|
'report.duplicates must name the duplicate id',
|
|
);
|
|
});
|
|
|
|
test('checkExitsOneWithRemedyWhenIndexMissing (F8)', () => {
|
|
const missingIndexPath = path.join(tmpDir, 'does-not-exist.json');
|
|
const r = runGenContextIndex(['--check', '--json'], { indexPath: missingIndexPath });
|
|
assert.equal(r.code, 1);
|
|
assert.doesNotMatch(r.stderr, STACK_FRAME_RE);
|
|
|
|
const report = parseJsonReport(r.stdout);
|
|
assert.equal(report.ok, false);
|
|
assert.equal(report.reason, REASON.FAIL_INDEX_MISSING);
|
|
});
|
|
|
|
test('checkExitsOneWithNamedReasonWhenIndexCorrupt (F9)', () => {
|
|
const corruptIndexPath = path.join(tmpDir, 'corrupt-index.json');
|
|
fs.writeFileSync(corruptIndexPath, 'this is not { valid javascript', 'utf8');
|
|
|
|
const r = runGenContextIndex(['--check', '--json'], { indexPath: corruptIndexPath });
|
|
assert.equal(r.code, 1);
|
|
assert.doesNotMatch(r.stderr, STACK_FRAME_RE, 'no bare stack trace for a corrupt committed index');
|
|
|
|
const report = parseJsonReport(r.stdout);
|
|
assert.equal(report.ok, false);
|
|
assert.equal(report.reason, REASON.FAIL_INDEX_UNPARSEABLE);
|
|
});
|
|
|
|
test('checkExitsOneWhenContextMdMissing (F10)', () => {
|
|
const missingContextPath = path.join(tmpDir, 'does-not-exist.md');
|
|
const r = runGenContextIndex(['--check', '--json'], { contextPath: missingContextPath });
|
|
assert.equal(r.code, 1);
|
|
assert.doesNotMatch(r.stderr, STACK_FRAME_RE, 'no bare stack trace when CONTEXT.md is missing');
|
|
|
|
const report = parseJsonReport(r.stdout);
|
|
assert.equal(report.ok, false);
|
|
assert.equal(report.reason, REASON.FAIL_CONTEXT_MISSING);
|
|
});
|
|
|
|
test('checkExitsOneWhenContextMdUnreadable', () => {
|
|
// Fault injection via the mandated technique (CONTRIBUTING.md /
|
|
// CLAUDE.md cross-platform IO-failure rule): monkeypatch fs.readFileSync
|
|
// to throw an injected EACCES for one specific fixture path, restore in
|
|
// `finally`. Never chmod 0o000 (root bypasses mode bits). This is an
|
|
// in-process call to the exported `checkReport` pure function rather
|
|
// than a subprocess spawn — a subprocess's fs cannot be monkeypatched
|
|
// from the parent test process without a `--require` preload, and
|
|
// `checkReport` IS the typed surface under test here, so calling it
|
|
// directly is not an engine-direct false-green for CLI *argv* behavior
|
|
// (that risk is covered by the spawned-CLI tests above); it is the
|
|
// correct level to exercise a fault the CLI itself cannot inject.
|
|
const fixtureContextPath = path.join(tmpDir, 'unreadable-context.md');
|
|
fs.writeFileSync(fixtureContextPath, '`FOO=bar`\n', 'utf8');
|
|
|
|
const origReadFileSync = fs.readFileSync;
|
|
fs.readFileSync = function patchedReadFileSync(p, ...rest) {
|
|
if (p === fixtureContextPath) {
|
|
const err = new Error(`EACCES: permission denied, open '${p}' (injected by test, never a real fs fault)`);
|
|
err.code = 'EACCES';
|
|
throw err;
|
|
}
|
|
return origReadFileSync.call(fs, p, ...rest);
|
|
};
|
|
try {
|
|
const report = checkReport(fixtureContextPath, path.join(tmpDir, 'unused-index.json'));
|
|
assert.equal(report.ok, false);
|
|
assert.equal(report.reason, REASON.FAIL_CONTEXT_UNREADABLE);
|
|
} finally {
|
|
fs.readFileSync = origReadFileSync;
|
|
}
|
|
});
|
|
|
|
test('checkIsCrlfAgnostic (F17)', () => {
|
|
// F17: a CRLF-committed index compared against the (LF) fresh real
|
|
// CONTEXT.md must still exit 0 — comparison is CRLF-normalized.
|
|
const freshSerialized = serializeIndex(buildFreshIndex());
|
|
const crlfSerialized = freshSerialized.replace(/\n/g, '\r\n');
|
|
const crlfIndexPath = path.join(tmpDir, 'context-index-crlf.json');
|
|
fs.writeFileSync(crlfIndexPath, crlfSerialized, 'utf8');
|
|
|
|
const r = runGenContextIndex(['--check'], { indexPath: crlfIndexPath });
|
|
assert.equal(r.code, 0, 'CRLF-vs-LF committed/fresh comparison must be normalized, not a false failure');
|
|
});
|
|
});
|
|
|
|
describe('gen-context-index.cjs --write / default / usage (F)', () => {
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempDir('gen-context-index-');
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('writeThenCheckIsClean', () => {
|
|
const writeTarget = path.join(tmpDir, 'write-target.json');
|
|
const w = runGenContextIndex(['--write'], { indexPath: writeTarget });
|
|
assert.equal(w.code, 0);
|
|
assert.ok(fs.existsSync(writeTarget), '--write must create the fixture-redirected index file');
|
|
|
|
const c = runGenContextIndex(['--check'], { indexPath: writeTarget });
|
|
assert.equal(c.code, 0, '--check must be clean immediately after --write');
|
|
});
|
|
|
|
test('writeIsByteIdenticalAcrossRuns', () => {
|
|
const target1 = path.join(tmpDir, 'w1.json');
|
|
const target2 = path.join(tmpDir, 'w2.json');
|
|
assert.equal(runGenContextIndex(['--write'], { indexPath: target1 }).code, 0);
|
|
assert.equal(runGenContextIndex(['--write'], { indexPath: target2 }).code, 0);
|
|
|
|
const content1 = fs.readFileSync(target1, 'utf8');
|
|
const content2 = fs.readFileSync(target2, 'utf8');
|
|
assert.equal(content1, content2, '--write must be deterministic across independent runs');
|
|
});
|
|
|
|
test('writtenIndexContainsNoLineField', () => {
|
|
const target = path.join(tmpDir, 'no-line-field.json');
|
|
assert.equal(runGenContextIndex(['--write'], { indexPath: target }).code, 0);
|
|
const content = fs.readFileSync(target, 'utf8');
|
|
assert.equal(content.includes('"line"'), false, 'the committed artifact must carry no `line` field anywhere (S5)');
|
|
});
|
|
|
|
test('defaultInvocationPrintsIndexToStdout', () => {
|
|
// Fully safe against the real repo: default mode only reads CONTEXT.md
|
|
// and the compiled predicates lib (read-only) and writes nothing.
|
|
const r = runGenContextIndex([]);
|
|
assert.equal(r.code, 0);
|
|
assert.ok(r.stdout.length > 0);
|
|
// Compare against the exact expected serialization (computed the same
|
|
// way the CLI does, via the exported pure functions) rather than
|
|
// hand-parsing the rendered text — avoids brittle delimiter-scanning
|
|
// over a JSON payload that legitimately contains ';' inside string values.
|
|
const expected = serializeIndex(buildFreshIndex()) + '\n';
|
|
assert.equal(r.stdout, expected);
|
|
});
|
|
|
|
test('unknownFlagExitsWithUsage', () => {
|
|
// Safe against the real repo: the unknown-flag branch never reads
|
|
// CONTEXT.md or the committed index at all.
|
|
const r = runGenContextIndex(['--totally-bogus-flag']);
|
|
assert.notEqual(r.code, 0);
|
|
assert.doesNotMatch(r.stderr, STACK_FRAME_RE, 'usage output must never be a bare stack trace');
|
|
});
|
|
|
|
// ─── DEFECT.GEN-CONTEXT-INDEX-PARSEARGS-GATE-BYPASS (MAJOR review finding):
|
|
// conflicting `--check --write` must be a hard usage error, not a silent
|
|
// `--write` win, and a missing/flag-shaped path value must never resolve
|
|
// to the cwd (which previously leaked a raw EISDIR stack trace). ────────
|
|
|
|
test('checkAndWriteTogetherIsUsageErrorNotASilentWrite (a)', () => {
|
|
const target = path.join(tmpDir, 'should-not-be-written.json');
|
|
const r = runGenContextIndex(['--check', '--write'], { indexPath: target });
|
|
assert.notEqual(r.code, 0, '--check --write together must not silently exit 0 as a write');
|
|
assert.doesNotMatch(r.stderr, STACK_FRAME_RE, 'usage output must never be a bare stack trace');
|
|
assert.equal(fs.existsSync(target), false, '--write must never win over --check and rewrite the index');
|
|
});
|
|
|
|
test('writeAndCheckReversedOrderIsAlsoAUsageError (a)', () => {
|
|
const target = path.join(tmpDir, 'should-also-not-be-written.json');
|
|
const r = runGenContextIndex(['--write', '--check'], { indexPath: target });
|
|
assert.notEqual(r.code, 0, 'conflicting mode flags must be a usage error regardless of order');
|
|
assert.doesNotMatch(r.stderr, STACK_FRAME_RE);
|
|
assert.equal(fs.existsSync(target), false);
|
|
});
|
|
|
|
test('missingTrailingValueForContextPathIsUsageErrorNotEisdirStackTrace (b)', () => {
|
|
const target = path.join(tmpDir, 'should-not-be-written-2.json');
|
|
// `--context-path` is the LAST arg: argv[i+1] is undefined, which used
|
|
// to resolve to the cwd via `path.resolve(undefined ?? '')`.
|
|
const r = runGenContextIndex(['--write', '--index-path', target, '--context-path']);
|
|
assert.notEqual(r.code, 0, 'a missing --context-path value must be a usage error');
|
|
assert.doesNotMatch(r.stderr, STACK_FRAME_RE, 'must never leak a raw EISDIR (or any) stack trace');
|
|
assert.equal(fs.existsSync(target), false, 'no write must happen when the path argument is rejected');
|
|
});
|
|
|
|
test('flagShapedValueForIndexPathIsUsageErrorNotSwallowedAsALiteralPath (b)', () => {
|
|
// `--index-path` is immediately followed by another flag rather than a
|
|
// path — must be rejected, not silently swallowed as the literal path
|
|
// "--json".
|
|
const r = runGenContextIndex(['--write', '--index-path', '--json']);
|
|
assert.notEqual(r.code, 0, 'a flag-shaped --index-path value must be a usage error');
|
|
assert.doesNotMatch(r.stderr, STACK_FRAME_RE);
|
|
});
|
|
});
|
|
|
|
// ─── DEFECT.GEN-CONTEXT-INDEX-DUPLICATE-GATE-UNPROVEN (MAJOR review finding):
|
|
// `FAIL_DUPLICATE_IDS` was only ever proven against synthetic fixtures — this
|
|
// branch hand-deleted the ONE live duplicate
|
|
// (`RULESET.WORKFLOW_MARKDOWN.FENCES`) from the real CONTEXT.md, so the
|
|
// committed docs/CONTEXT-INDEX.json ships `duplicates: []` and the gate has
|
|
// never been shown to catch a REAL duplicate in the real document. This
|
|
// suite re-inserts the exact deleted line (recovered from
|
|
// `git show origin/next:CONTEXT.md`) into a copy of the REAL CONTEXT.md and
|
|
// runs the real generator CLI against it. ───────────────────────────────────
|
|
|
|
describe('gen-context-index.cjs --check against a real-CONTEXT.md duplicate (real-data proof)', () => {
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempDir('gen-context-index-real-dup-');
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('reinsertingTheDeletedRulesetWorkflowMarkdownFencesLineFailsWithNamedDuplicate', () => {
|
|
// The exact line this branch deleted from the real CONTEXT.md (does NOT
|
|
// mention MD040 — the live line that replaced it does).
|
|
const deletedLine =
|
|
'`RULESET.WORKFLOW_MARKDOWN.FENCES=when editing shell snippets inside workflow markdown, preserve the opening language fence; malformed fence can create fresh CodeRabbit threads`';
|
|
|
|
const real = fs.readFileSync(REAL_CONTEXT_PATH, 'utf8');
|
|
assert.ok(
|
|
real.includes('RULESET.WORKFLOW_MARKDOWN.FENCES'),
|
|
'sanity: the real CONTEXT.md must still carry the live (MD040) FENCES line',
|
|
);
|
|
assert.ok(!real.includes(deletedLine), 'sanity: the deleted line must not already be present verbatim');
|
|
|
|
const reinserted = real + '\n' + deletedLine + '\n';
|
|
const fixturePath = path.join(tmpDir, 'CONTEXT-real-with-reinserted-duplicate.md');
|
|
fs.writeFileSync(fixturePath, reinserted, 'utf8');
|
|
|
|
const r = runGenContextIndex(['--check', '--json'], { contextPath: fixturePath });
|
|
assert.equal(r.code, 1, 'a real duplicate reintroduced into the real CONTEXT.md must fail the gate');
|
|
assert.doesNotMatch(r.stderr, STACK_FRAME_RE);
|
|
|
|
const report = parseJsonReport(r.stdout);
|
|
assert.equal(report.ok, false);
|
|
assert.equal(report.reason, REASON.FAIL_DUPLICATE_IDS);
|
|
assert.ok(
|
|
report.duplicates.some((d) => d.id === 'RULESET.WORKFLOW_MARKDOWN.FENCES'),
|
|
'report.duplicates must name RULESET.WORKFLOW_MARKDOWN.FENCES as the real duplicate',
|
|
);
|
|
});
|
|
});
|