test(#1977): consolidate 22 misc + repo-invariant regression tests
Final epic-#1969 batch. Fold 22 issue-named files: the 4 genuine repo-wide invariant scans (551-eslint-bin-lib-coverage, bug-3054 stale /gsd-next, bug-3810 no-gsd-sdk-runtime-refs, feat-3593 cli-negative-universal) into a NEW shared repo-invariants.test.cjs; the other 18 as singletons into their nearest module suite (model-resolver, codex-config, runtime-converters, security, state-transition, worktree-safety, roadmap-parser, etc.). Verbatim block-scoped describe wrappers; 334 subtests conserved 1:1. Host-env pre-check (B2+B6): the 6 CLI folds into GSD_TEST_MODE-setting hosts (model-resolver/ codex-config/runtime-converters) are benign — each origin independently sets GSD_TEST_MODE=1 itself (idempotent), unlike the B6 real-install case. Regenerates regression-name allowlist (222->213), ratchets file-count allowlist (state 17->16), makes 7 relocated allow-test-rule exemptions issue-ref-compliant (ADR-456; prunes stale ids). Repoints 2 tests/ refs in docs/TESTING-SUITES.md. lint:ci green. Part of epic #1969. Closes #1977. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1557,3 +1557,287 @@ describe('scanPhasePlans — call-site parity on mixed fixture', () => {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
// Folded from tests/feat-3594-parser-adversarial-roadmap.test.cjs — consolidation epic #1969 (B8 #1977)
|
||||
// ────────────────────────────────────────────────────────────────────────
|
||||
{
|
||||
const { describe: __foldDescribe } = require('node:test');
|
||||
__foldDescribe("folded:feat-3594-parser-adversarial-roadmap (consolidation epic #1969 B8 #1977)", () => {
|
||||
/**
|
||||
* Adversarial roadmap-parser tests (#3594).
|
||||
*
|
||||
* Loads each fixture in `tests/fixtures/adversarial/roadmap/` as the
|
||||
* project's `.planning/ROADMAP.md` and pins invariants on the public
|
||||
* `gsd-tools roadmap get-phase <N>` surface — which routes through the
|
||||
* SDK bridge when available and the CJS handler otherwise.
|
||||
*
|
||||
* Per CONTRIBUTING.md §"Testing Standards / Parser and project-file
|
||||
* inputs", the assertion target is the typed JSON shape the CLI emits,
|
||||
* not stderr prose. The harness in `tests/helpers/cli-negative.cjs`
|
||||
* (introduced by #3627 / #3593) is reused here for consistency.
|
||||
*
|
||||
* Several fixtures encode known historical regressions:
|
||||
* - fenced-code-block headings shadowing real phases (#2787)
|
||||
* - decimal phase prefix collisions (#3537)
|
||||
* - HTML-comment heading false positives
|
||||
*
|
||||
* Pre-existing parser bugs surfaced by these fixtures are NOT fixed in
|
||||
* this PR — fixing them is out of scope for "add adversarial test
|
||||
* coverage." Where a fixture exposes a still-open bug, the test
|
||||
* asserts the *currently observed* behavior with a comment naming the
|
||||
* open issue, so the flip from RED→GREEN is a one-line change the day
|
||||
* the real fix lands.
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
|
||||
const { describe, test } = require('node:test');
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
|
||||
const { runCli } = require('./helpers/cli-negative.cjs');
|
||||
const { createTempProject, cleanup } = require('./helpers.cjs');
|
||||
|
||||
const FIXTURE_DIR = path.join(__dirname, 'fixtures', 'adversarial', 'roadmap');
|
||||
|
||||
function loadFixture(name) {
|
||||
return fs.readFileSync(path.join(FIXTURE_DIR, name), 'utf-8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a temp project whose ROADMAP.md is the named fixture's content.
|
||||
* Returns the project directory; caller is responsible for cleanup.
|
||||
*/
|
||||
function projectWithFixture(t, fixtureName) {
|
||||
const projectDir = createTempProject('roadmap-adv-' + fixtureName.replace(/\W+/g, '-') + '-');
|
||||
t.after(() => cleanup(projectDir));
|
||||
fs.writeFileSync(path.join(projectDir, '.planning', 'ROADMAP.md'), loadFixture(fixtureName));
|
||||
return projectDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Run `gsd-tools roadmap get-phase <N>` and parse the JSON payload.
|
||||
* Returns `{ ok, exit, parsed, raw }` so tests can assert on either
|
||||
* the exit code or the structured payload.
|
||||
*/
|
||||
function getPhase(projectDir, phaseNum) {
|
||||
// No --json-errors — the get-phase command outputs JSON on success
|
||||
// via the normal stdout path. Reading the parsed payload is what the
|
||||
// workflows downstream do, so that's what we test.
|
||||
const result = runCli(['roadmap', 'get-phase', phaseNum], { cwd: projectDir, jsonErrors: false });
|
||||
let parsed = null;
|
||||
try {
|
||||
parsed = JSON.parse(result.stdout);
|
||||
} catch {
|
||||
// Leave parsed null; tests that depend on it must handle that.
|
||||
}
|
||||
return {
|
||||
exit: result.status,
|
||||
ok: result.status === 0,
|
||||
parsed,
|
||||
raw: result.stdout,
|
||||
stderr: result.stderr,
|
||||
hasStackTrace: result.hasStackTrace,
|
||||
};
|
||||
}
|
||||
|
||||
// ─── Fenced code block heading shadowing ────────────────────────────────────
|
||||
|
||||
describe('feat-3594: roadmap parser and fenced-code-block headings (#2787)', () => {
|
||||
test('phase 1 in real prose is found even when ## Phase 999 appears inside a ``` block', (t) => {
|
||||
const projectDir = projectWithFixture(t, 'phase-heading-inside-fenced-code.md');
|
||||
const result = getPhase(projectDir, '1');
|
||||
assert.equal(result.hasStackTrace, false, 'no V8 stack trace');
|
||||
assert.ok(result.parsed, `expected JSON payload, got: ${result.raw}`);
|
||||
assert.equal(result.parsed.found, true, 'phase 1 must be found');
|
||||
assert.equal(result.parsed.phase_number, '1');
|
||||
assert.match(result.parsed.phase_name, /real phase one/);
|
||||
});
|
||||
|
||||
test('phase 999 inside a fenced block is ignored', (t) => {
|
||||
const projectDir = projectWithFixture(t, 'phase-heading-inside-fenced-code.md');
|
||||
const result = getPhase(projectDir, '999');
|
||||
assert.equal(result.hasStackTrace, false, 'no stack trace');
|
||||
assert.ok(result.parsed, `expected JSON payload, got: ${result.raw}`);
|
||||
assert.equal(result.parsed.found, false, 'phase headings inside fenced blocks must not be parsed');
|
||||
});
|
||||
|
||||
test('fenced example heading does not shadow the real phase details and backlog phase stays unresolved (#1588)', (t) => {
|
||||
const projectDir = createTempProject('roadmap-1588-');
|
||||
t.after(() => cleanup(projectDir));
|
||||
fs.writeFileSync(
|
||||
path.join(projectDir, '.planning', 'STATE.md'),
|
||||
[
|
||||
'---',
|
||||
'gsd_state_version: 1.0',
|
||||
'milestone: v1.1',
|
||||
'status: planning',
|
||||
'---',
|
||||
'',
|
||||
].join('\n')
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(projectDir, '.planning', 'ROADMAP.md'),
|
||||
[
|
||||
'# Roadmap',
|
||||
'',
|
||||
'<details open>',
|
||||
'<summary>v1.1 Current (Phases 8-9) - PLANNED</summary>',
|
||||
'',
|
||||
'- [ ] **Phase 9: Real Phase**',
|
||||
'',
|
||||
'</details>',
|
||||
'',
|
||||
'## Phase Details',
|
||||
'',
|
||||
'```markdown',
|
||||
'### Phase 9: Fenced Example Phase',
|
||||
'**Goal:** This example must not be treated as roadmap structure.',
|
||||
'```',
|
||||
'',
|
||||
'### Phase 9: Real Phase',
|
||||
'**Goal:** Use the real phase details outside the fenced block.',
|
||||
'**Requirements:** REAL-01',
|
||||
'',
|
||||
'## Backlog',
|
||||
'',
|
||||
'### Phase 999.1: Backlog Thing',
|
||||
'**Goal:** Future backlog item.',
|
||||
'',
|
||||
].join('\n')
|
||||
);
|
||||
|
||||
const phase9 = getPhase(projectDir, '9');
|
||||
assert.ok(phase9.parsed, `expected JSON payload, got: ${phase9.raw}`);
|
||||
assert.equal(phase9.parsed.found, true, 'phase 9 must be found');
|
||||
assert.equal(phase9.parsed.phase_name, 'Real Phase');
|
||||
assert.equal(phase9.parsed.goal, 'Use the real phase details outside the fenced block.');
|
||||
assert.match(phase9.parsed.section, /REAL-01/, 'real phase section must be returned');
|
||||
|
||||
const backlog = getPhase(projectDir, '999.1');
|
||||
assert.ok(backlog.parsed, `expected JSON payload, got: ${backlog.raw}`);
|
||||
assert.equal(backlog.parsed.found, false, 'backlog sentinel phase must not resolve as an active roadmap phase');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Decimal phase prefix collisions ────────────────────────────────────────
|
||||
|
||||
describe('feat-3594: roadmap parser handles decimal phase prefix collisions (#3537)', () => {
|
||||
test('asking for phase "2" returns the integer phase, NOT phase 2.1 or 2.10', (t) => {
|
||||
const projectDir = projectWithFixture(t, 'decimal-phase-mixed.md');
|
||||
const result = getPhase(projectDir, '2');
|
||||
assert.ok(result.parsed, `expected JSON payload, got: ${result.raw}`);
|
||||
assert.equal(result.parsed.found, true);
|
||||
assert.equal(result.parsed.phase_number, '2');
|
||||
assert.match(result.parsed.phase_name, /integer phase two/);
|
||||
});
|
||||
|
||||
test('asking for phase "2.1" returns the decimal child', (t) => {
|
||||
const projectDir = projectWithFixture(t, 'decimal-phase-mixed.md');
|
||||
const result = getPhase(projectDir, '2.1');
|
||||
assert.ok(result.parsed, `expected JSON payload, got: ${result.raw}`);
|
||||
assert.equal(result.parsed.found, true);
|
||||
assert.equal(result.parsed.phase_number, '2.1');
|
||||
assert.match(result.parsed.phase_name, /decimal child/);
|
||||
});
|
||||
|
||||
test('asking for phase "2.10" returns the decimal sibling, NOT phase 2.1', (t) => {
|
||||
const projectDir = projectWithFixture(t, 'decimal-phase-mixed.md');
|
||||
const result = getPhase(projectDir, '2.10');
|
||||
assert.ok(result.parsed, `expected JSON payload, got: ${result.raw}`);
|
||||
assert.equal(result.parsed.found, true);
|
||||
assert.equal(result.parsed.phase_number, '2.10');
|
||||
assert.match(result.parsed.phase_name, /decimal phase 2\.10/);
|
||||
});
|
||||
|
||||
test('asking for phase "21" returns phase 21, NOT phase 2 (prefix-collision guard)', (t) => {
|
||||
const projectDir = projectWithFixture(t, 'decimal-phase-mixed.md');
|
||||
const result = getPhase(projectDir, '21');
|
||||
assert.ok(result.parsed, `expected JSON payload, got: ${result.raw}`);
|
||||
assert.equal(result.parsed.found, true);
|
||||
assert.equal(result.parsed.phase_number, '21');
|
||||
assert.match(result.parsed.phase_name, /phase twenty-one/);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Unicode phase titles ───────────────────────────────────────────────────
|
||||
|
||||
describe('feat-3594: roadmap parser preserves Unicode phase titles', () => {
|
||||
test('Japanese title round-trips through phase_name', (t) => {
|
||||
const projectDir = projectWithFixture(t, 'unicode-phase-titles.md');
|
||||
const result = getPhase(projectDir, '1');
|
||||
assert.ok(result.parsed, `expected JSON payload, got: ${result.raw}`);
|
||||
assert.equal(result.parsed.phase_name, '日本語フェーズ — initial setup');
|
||||
});
|
||||
|
||||
test('emoji + smart-quote title survives', (t) => {
|
||||
const projectDir = projectWithFixture(t, 'unicode-phase-titles.md');
|
||||
const result = getPhase(projectDir, '2');
|
||||
assert.ok(result.parsed, `expected JSON payload, got: ${result.raw}`);
|
||||
assert.match(result.parsed.phase_name, /🚧/);
|
||||
assert.match(result.parsed.phase_name, /Émile/);
|
||||
});
|
||||
|
||||
test('Greek-letter title survives', (t) => {
|
||||
const projectDir = projectWithFixture(t, 'unicode-phase-titles.md');
|
||||
const result = getPhase(projectDir, '3');
|
||||
assert.ok(result.parsed, `expected JSON payload, got: ${result.raw}`);
|
||||
assert.equal(result.parsed.phase_name, 'αβγ δεζ ηθι');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Repeated phase IDs ─────────────────────────────────────────────────────
|
||||
|
||||
describe('feat-3594: roadmap parser handles repeated phase IDs deterministically', () => {
|
||||
test('two declarations of phase 1: parser returns the FIRST match (current behavior)', (t) => {
|
||||
const projectDir = projectWithFixture(t, 'repeated-phase-ids.md');
|
||||
const result = getPhase(projectDir, '1');
|
||||
assert.ok(result.parsed, `expected JSON payload, got: ${result.raw}`);
|
||||
assert.equal(result.parsed.found, true);
|
||||
// The regex uses `content.match(...)` which returns the FIRST match.
|
||||
// Pin that — a future change to last-wins or de-dup would fire.
|
||||
assert.match(result.parsed.phase_name, /first declaration/);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── HTML comments ──────────────────────────────────────────────────────────
|
||||
|
||||
describe('feat-3594: roadmap parser and HTML-commented headings', () => {
|
||||
test('phase 1 in real prose is found even when phase 998/999 appear inside <!-- ... -->', (t) => {
|
||||
const projectDir = projectWithFixture(t, 'markdown-headings-inside-html-comment.md');
|
||||
const result = getPhase(projectDir, '1');
|
||||
assert.ok(result.parsed, `expected JSON payload, got: ${result.raw}`);
|
||||
assert.equal(result.parsed.found, true);
|
||||
assert.equal(result.parsed.phase_name, 'real phase');
|
||||
});
|
||||
|
||||
test('phase 999 inside an HTML comment remains ignored because backlog sentinels never resolve', (t) => {
|
||||
const projectDir = projectWithFixture(t, 'markdown-headings-inside-html-comment.md');
|
||||
const result = getPhase(projectDir, '999');
|
||||
assert.equal(result.hasStackTrace, false, 'no stack trace');
|
||||
assert.ok(result.parsed, `expected JSON payload, got: ${result.raw}`);
|
||||
assert.equal(result.parsed.found, false, 'backlog sentinel phases must not resolve');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Cross-corpus invariant ────────────────────────────────────────────────
|
||||
|
||||
describe('feat-3594: roadmap parser does not crash on ANY corpus fixture', () => {
|
||||
const fixtures = fs.readdirSync(FIXTURE_DIR).filter((f) => f.endsWith('.md') && f !== 'README.md');
|
||||
for (const fixture of fixtures) {
|
||||
test(`fixture "${fixture}" — get-phase with arbitrary IDs must not crash`, (t) => {
|
||||
const projectDir = projectWithFixture(t, fixture);
|
||||
for (const id of ['1', '2', '99', '999', '0', '2.1']) {
|
||||
const result = getPhase(projectDir, id);
|
||||
assert.equal(result.hasStackTrace, false, `${fixture} id=${id}: no V8 stack frame allowed`);
|
||||
// exit status varies (0 for found, non-zero for not-found —
|
||||
// both are valid). What's pinned: the parser produced SOME output
|
||||
// (either valid JSON or a clean stderr) without crashing.
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user