test(#3308): failing-first tests for planning-snapshot parsed projection

ADR-3180 epic #3180 Phase 10 (§8.1): tests for the not-yet-existing
src/planning-snapshot.cts (buildPlanningSnapshot, worstScope), the
not-yet-existing scripts/lint-planning-snapshot-bypass-drift.cjs guard,
and the new STATE_UNREADABLE reason on tests/unusable-input.test.cjs's
already-shipped UNUSABLE_REASON enum. RED by construction: the modules
under test do not exist yet.
This commit is contained in:
sim
2026-08-12 21:43:25 -04:00
parent b8cb031ce2
commit d1b659703e
3 changed files with 748 additions and 1 deletions

View File

@@ -0,0 +1,203 @@
/**
* Tests for the planning-snapshot bypass drift guard (Phase 10, issue #3308,
* ADR-3180 §8.1 rule 2) — `scripts/lint-planning-snapshot-bypass-drift.cjs`.
*
* ADR-3180 §8.1 rule 2: a diagnostic rule may see only PARSED values from
* `src/planning-snapshot.cts`, never raw `.planning/` document text.
* `cmdValidateHealth` (`src/verify.cts`) is the one diagnostic-rule-shaped
* function in the repo today that has NOT yet been migrated onto the
* snapshot (Phase 11, issue #3309) — this guard tracks that acknowledged
* debt via a shrink-only ratchet baseline, keyed function-scoped through
* `DIAGNOSTIC_RULE_FUNCTIONS` (mirroring
* `lint-completion-ratio-drift.cjs`'s `FUNCTION_SCOPED_EXEMPTIONS`, inverted:
* this registry names which functions the rule APPLIES TO, not which are
* exempt from it).
*
* NOTE: `scripts/lint-planning-snapshot-bypass-drift.cjs` does not exist yet
* (Phase 10 is TDD — this test file is written first and is expected to fail
* with a require/MODULE_NOT_FOUND error until the guard script lands). This
* mirrors `tests/planning-prompt-drift.test.cjs`'s structure and asserts
* only the guard's PURE functions, driven with in-memory strings/objects —
* no shelling out to the CLI.
*/
'use strict';
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const drift = require('../scripts/lint-planning-snapshot-bypass-drift.cjs');
const {
findSnapshotBypassDrift,
diffAgainstBaseline,
writeBaseline,
dedupeViolationsForBaseline,
sortEntries,
DIAGNOSTIC_RULE_FUNCTIONS,
} = drift;
const { createTempDir, cleanup } = require('./helpers.cjs');
const fs = require('node:fs');
const path = require('node:path');
const REGISTERED_FILE = path.join('src', 'verify.cts');
const REGISTERED_FN = 'cmdValidateHealth';
// A minimal fixture carrying one registered diagnostic-rule-shaped function
// (containing a raw-read primitive) and one unregistered function (carrying
// the identical raw-read line) — proves detection is function-SCOPED, not
// whole-file.
function fixtureSource() {
return [
'function cmdValidateHealth(cwd) {',
' const raw = platformReadSync(x);',
' return raw;',
'}',
'',
'function cmdUnrelated(cwd) {',
' const raw = platformReadSync(x);',
' return raw;',
'}',
'',
].join('\n');
}
// ─── G1/G3: registered-function raw-read line, baseline mechanics ────────
describe('findSnapshotBypassDrift — G1/G3: registered function raw-read detection', () => {
test('a raw-read line inside a DIAGNOSTIC_RULE_FUNCTIONS-registered function is detected', () => {
const out = findSnapshotBypassDrift(fixtureSource(), REGISTERED_FILE);
// Exactly ONE violation: the registered cmdValidateHealth copy, not the
// unregistered cmdUnrelated copy (see G2 below for the direct negative).
assert.strictEqual(out.length, 1);
assert.strictEqual(out[0].line, 2);
assert.strictEqual(out[0].found, 'platformReadSync(');
assert.strictEqual(out[0].text, 'const raw = platformReadSync(x);');
assert.strictEqual(out[0].file, REGISTERED_FILE);
});
test('G1: a registered-function violation present in the baseline is KNOWN — neither fresh nor stale', () => {
const violations = findSnapshotBypassDrift(fixtureSource(), REGISTERED_FILE);
const baseline = [{ file: REGISTERED_FILE, text: 'const raw = platformReadSync(x);' }];
const { fresh, stale } = diffAgainstBaseline(violations, baseline);
assert.deepStrictEqual(fresh, []);
assert.deepStrictEqual(stale, []);
});
test('G3: a registered-function violation NOT in the baseline is reported under fresh', () => {
const violations = findSnapshotBypassDrift(fixtureSource(), REGISTERED_FILE);
const { fresh, stale } = diffAgainstBaseline(violations, []);
assert.strictEqual(fresh.length, 1);
assert.strictEqual(fresh[0].text, 'const raw = platformReadSync(x);');
assert.deepStrictEqual(stale, []);
});
});
// ─── G2: function-scoped, not whole-file ──────────────────────────────────
describe('findSnapshotBypassDrift — G2: function-scoped detection (not whole-file)', () => {
test('the identical raw-read line inside an UNREGISTERED function produces no violation for that line', () => {
const out = findSnapshotBypassDrift(fixtureSource(), REGISTERED_FILE);
const unregisteredLines = out.filter((v) => v.line === 7);
assert.deepStrictEqual(unregisteredLines, [], 'cmdUnrelated (line 7) must not be flagged — it is not in DIAGNOSTIC_RULE_FUNCTIONS');
});
test('a registered function name in a file NOT listed in DIAGNOSTIC_RULE_FUNCTIONS is never flagged', () => {
const out = findSnapshotBypassDrift(fixtureSource(), path.join('src', 'other-file.cts'));
assert.deepStrictEqual(out, []);
});
test('DIAGNOSTIC_RULE_FUNCTIONS registers exactly src/verify.cts -> cmdValidateHealth today', () => {
assert.ok(DIAGNOSTIC_RULE_FUNCTIONS.has(REGISTERED_FILE));
assert.ok(DIAGNOSTIC_RULE_FUNCTIONS.get(REGISTERED_FILE).has(REGISTERED_FN));
});
});
// ─── G4: stale ratchet entries ─────────────────────────────────────────────
describe('diffAgainstBaseline — G4: stale entries', () => {
test('a baseline entry whose (file, text) pair no longer appears in a fresh scan is reported under stale', () => {
const baseline = [{ file: REGISTERED_FILE, text: 'const raw = platformReadSync(oldSite);' }];
const violations = findSnapshotBypassDrift(fixtureSource(), REGISTERED_FILE);
const { fresh, stale } = diffAgainstBaseline(violations, baseline);
// The baseline's own (unmatched) entry is stale...
assert.strictEqual(stale.length, 1);
assert.strictEqual(stale[0].text, 'const raw = platformReadSync(oldSite);');
// ...and the fixture's real, unacknowledged violation is separately fresh.
assert.strictEqual(fresh.length, 1);
assert.strictEqual(fresh[0].text, 'const raw = platformReadSync(x);');
});
});
// ─── G5: writeBaseline / dedupeViolationsForBaseline regeneration ────────
describe('writeBaseline / dedupeViolationsForBaseline — G5: regeneration matches a fresh detection pass', () => {
test('dedupeViolationsForBaseline collapses violations into sorted, deduped baseline rows matching the detection pass', () => {
const violations = findSnapshotBypassDrift(fixtureSource(), REGISTERED_FILE);
const deduped = dedupeViolationsForBaseline(violations);
const sorted = sortEntries(deduped);
assert.strictEqual(sorted.length, 1);
assert.strictEqual(sorted[0].file, REGISTERED_FILE);
assert.strictEqual(sorted[0].text, 'const raw = platformReadSync(x);');
assert.strictEqual(sorted[0].count, 1);
// A freshly-written baseline must itself be immediately KNOWN (not
// fresh/stale) against the SAME detection pass — the round-trip
// invariant a ratchet baseline exists to guarantee.
const { fresh, stale } = diffAgainstBaseline(violations, sorted);
assert.deepStrictEqual(fresh, []);
assert.deepStrictEqual(stale, []);
});
test('writeBaseline persists entries to disk that exactly match a fresh scanRepo/detection pass', (t) => {
const root = createTempDir('gsd-planning-snapshot-bypass-drift-');
t.after(() => cleanup(root));
const violations = findSnapshotBypassDrift(fixtureSource(), REGISTERED_FILE);
writeBaseline(root, violations);
const writtenPath = path.join(root, 'scripts', 'baselines', 'planning-snapshot-bypass-baseline.json');
const written = JSON.parse(fs.readFileSync(writtenPath, 'utf8'));
assert.strictEqual(written.entries.length, 1);
assert.strictEqual(written.entries[0].file, REGISTERED_FILE);
assert.strictEqual(written.entries[0].text, 'const raw = platformReadSync(x);');
assert.strictEqual(written.entries[0].count, 1);
// The written baseline immediately reconciles with the pass that produced
// it — no fresh, no stale.
const { fresh, stale } = diffAgainstBaseline(violations, written.entries);
assert.deepStrictEqual(fresh, []);
assert.deepStrictEqual(stale, []);
// The baseline names its owner issue (#3309, Phase 11) and ADR §8.1, not
// #3218 (the sibling prompt-drift guard's owner issue).
assert.match(written.$comment, /#3309/);
assert.doesNotMatch(written.$comment, /#3218/);
assert.strictEqual(written.entries[0].owner_issue, '#3309');
});
});
// ─── G6: owner functions (ADR-3180 §7) are never flagged ─────────────────
describe('findSnapshotBypassDrift — G6: ADR-3180 §7 owner-function calls never match the raw-read primitive regex', () => {
test('a call to an ADR-3180 §7 owner (getMilestoneInfo) inside a registered function is never flagged', () => {
const source = [
'function cmdValidateHealth(cwd) {',
' const info = getMilestoneInfo(cwd);',
' return info;',
'}',
'',
].join('\n');
const out = findSnapshotBypassDrift(source, REGISTERED_FILE);
assert.deepStrictEqual(out, [], 'getMilestoneInfo(cwd) must not match platformReadSync(/readFileSync(/readdirSync(');
});
test('other ADR-3180 §7 owner calls (listMilestonePhaseDirs, isPhaseComplete, scanPhasePlans, stateFieldValue) also never match', () => {
const source = [
'function cmdValidateHealth(cwd) {',
' const dirs = listMilestonePhaseDirs(phasesDir, opts);',
' const done = isPhaseComplete(phaseDir, deps);',
' const scan = scanPhasePlans(phaseDir);',
" const label = stateFieldValue(fm, body, null, 'Phase', opts);",
' return { dirs, done, scan, label };',
'}',
'',
].join('\n');
const out = findSnapshotBypassDrift(source, REGISTERED_FILE);
assert.deepStrictEqual(out, []);
});
});

View File

@@ -0,0 +1,497 @@
'use strict';
/**
* Tests for `src/planning-snapshot.cts` (Phase 10, #3308, ADR-3180 §8.1).
*
* Design: .gsd/phase/refactor-3308-planning-snapshot-parsed-projection/40-design.md
* Test matrix: .gsd/phase/refactor-3308-planning-snapshot-parsed-projection/50-test-matrix.md
*
* TDD RED: `src/planning-snapshot.cts` does not exist yet — this file's
* `require('../gsd-core/bin/lib/planning-snapshot.cjs')` throws
* MODULE_NOT_FOUND until a companion implementation phase adds it. That is
* the intended starting state.
*
* Fixture provenance (CONTRIBUTING.md / repo rule): every case calls the REAL
* compiled owners (`getMilestoneInfo`, `listMilestonePhaseDirs`,
* `isPhaseComplete`, `scanPhasePlans`, `stateFieldValue`, `planningPaths`)
* against real temp `.planning/` trees — no hand-built in-memory mocks of any
* owner. Fixture helpers mirror `tests/completion-ratio-scope-withholding.test.cjs`
* verbatim (`writeRoadmap`/`writeState`/`writeFile`, and the directory-vs-file
* swap technique for IO-failure rows), and the readdirSync fault-injection
* helper mirrors `tests/verify.test.cjs`'s `injectMilestonesFault` (`t.mock`,
* auto-restored — never `chmod 0o000`, which root bypasses in CI/Docker).
*/
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const fc = require('fast-check');
const { createTempDir, cleanup } = require('./helpers.cjs');
// `export =` shape TBD by the implementing phase — this module is "the sole
// export consumers reach for" per the design doc, with `worstScope` also
// exported for direct unit testing per this phase's brief. Support either a
// callable-with-properties export (mirrors plan-scan.cjs) or a plain object
// export (mirrors verification.cjs) so this file does not lock in a shape the
// design doc leaves to the implementer.
const planningSnapshotLib = require('../gsd-core/bin/lib/planning-snapshot.cjs');
const buildPlanningSnapshot = planningSnapshotLib.buildPlanningSnapshot ?? planningSnapshotLib;
const { worstScope } = planningSnapshotLib;
const { SCOPE } = require('../gsd-core/bin/lib/planning-scope.cjs');
const { _unusableInputEmissionCountForTests } = require('../gsd-core/bin/lib/unusable-input.cjs');
// ─── Fixture helpers (mirrors tests/completion-ratio-scope-withholding.test.cjs) ─
function planningDirOf(cwd) {
return path.join(cwd, '.planning');
}
function writeRoadmap(cwd, content) {
fs.mkdirSync(planningDirOf(cwd), { recursive: true });
fs.writeFileSync(path.join(planningDirOf(cwd), 'ROADMAP.md'), content);
}
function writeState(cwd, fields) {
fs.mkdirSync(planningDirOf(cwd), { recursive: true });
const lines = ['---'];
for (const [k, v] of Object.entries(fields)) lines.push(`${k}: ${v}`);
lines.push('---', '');
fs.writeFileSync(path.join(planningDirOf(cwd), 'STATE.md'), lines.join('\n'));
}
function writeFile(cwd, relPath, content) {
const full = path.join(cwd, relPath);
fs.mkdirSync(path.dirname(full), { recursive: true });
fs.writeFileSync(full, content);
}
// Appends raw text to an already-written STATE.md (writeState above writes
// only the frontmatter block) — used by the currentPhaseLabel rows to add a
// body carrying `## Current Position` / a bare `Phase:` line.
function appendToState(cwd, body) {
const statePath = path.join(planningDirOf(cwd), 'STATE.md');
fs.writeFileSync(statePath, fs.readFileSync(statePath, 'utf-8') + body);
}
// Makes a FILE unreadable-as-a-file: a DIRECTORY node where callers expect a
// regular file (ROADMAP.md / STATE.md). `platformReadSync` -> `fs.readFileSync`
// throws EISDIR on it, deterministically and cross-platform — no chmod.
function makeFileUnreadableAsDir(fullPath) {
fs.mkdirSync(fullPath, { recursive: true });
}
// Makes a DIRECTORY unreadable-as-a-directory: a REGULAR FILE where callers
// expect a directory (a phase's nested `plans/` subdirectory). `readdirSync`
// throws ENOTDIR on it, deterministically and cross-platform — no chmod.
// (This is the inverse of `makePhasesDirUnreadable` in the sibling test file.)
function makeDirUnreadableAsFile(fullPath) {
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
fs.writeFileSync(fullPath, 'not a directory\n');
}
// A matched plan/summary pair plus a passing `*-VERIFICATION.md` —
// `isPhaseComplete` requires `verification.status === 'passed'` for
// `complete: true`, which plan/summary pairing alone does not establish.
function makeCompletePhaseDir(cwd, relPhaseDir) {
writeFile(cwd, `${relPhaseDir}/01-01-PLAN.md`, '# Plan\n');
writeFile(cwd, `${relPhaseDir}/01-01-SUMMARY.md`, '# Summary\n');
writeFile(cwd, `${relPhaseDir}/01-VERIFICATION.md`, '---\nstatus: passed\n---\n');
}
function buildHealthyTwoPhaseFixture(cwd) {
writeState(cwd, { milestone: 'v1.0' });
writeRoadmap(cwd, [
'## v1.0 Current 🚧',
'',
'### Phase 1: Foo',
'',
'### Phase 2: Bar',
].join('\n'));
makeCompletePhaseDir(cwd, '.planning/phases/01-foo');
makeCompletePhaseDir(cwd, '.planning/phases/02-bar');
}
// ─── fs fault injection (mirrors tests/verify.test.cjs's injectMilestonesFault) ─
function fsError(code, targetPath) {
const err = new Error(`${code}: operation failed, scandir '${targetPath}'`);
err.code = code;
err.syscall = 'scandir';
err.path = targetPath;
return err;
}
// Scoped readdirSync fault: throws ONLY for the exact `targetPhaseDir` path.
// Every other path (crucially, the phases/ enumeration read that must still
// list this directory's NAME) passes through to the real implementation.
// `t.mock` auto-restores after the test.
function injectPhaseDirFault(t, targetPhaseDir) {
const original = fs.readdirSync;
t.mock.method(fs, 'readdirSync', function (p, ...rest) {
if (p === targetPhaseDir) throw fsError('EACCES', targetPhaseDir);
return original.call(this, p, ...rest);
});
}
// Measure NEW unusable-input diagnostics emitted while `fn` runs, with
// stderr stubbed to keep the suite's own output clean (mirrors
// tests/unusable-input.test.cjs's emissionsDuring).
function emissionsDuring(fn) {
const before = _unusableInputEmissionCountForTests();
const original = process.stderr.write;
process.stderr.write = () => true;
let result;
try {
result = fn();
} finally {
process.stderr.write = original;
}
return [result, _unusableInputEmissionCountForTests() - before];
}
// ═════════════════════════════════════════════════════════════════════════
// Happy path — rows 1-4
// ═════════════════════════════════════════════════════════════════════════
describe('happy path', () => {
test('builds a fully-COMPLETE snapshot for a healthy two-phase milestone', (t) => {
const cwd = createTempDir('gsd-3308-h1-');
t.after(() => cleanup(cwd));
buildHealthyTwoPhaseFixture(cwd);
const snap = buildPlanningSnapshot(cwd);
assert.strictEqual(snap.milestone.scope, SCOPE.COMPLETE);
assert.strictEqual(snap.phaseDirs.scope, SCOPE.COMPLETE);
assert.strictEqual(snap.phases.scope, SCOPE.COMPLETE);
assert.strictEqual(snap.phases.value.length, 2);
for (const p of snap.phases.value) {
assert.strictEqual(p.complete, true);
assert.strictEqual(p.scope, SCOPE.COMPLETE);
assert.strictEqual(p.verificationStatus, 'passed');
assert.strictEqual(p.planCount, 1);
assert.strictEqual(p.summaryCount, 1);
}
});
test('zero phase directories is a COMPLETE empty array, not a non-answer', (t) => {
const cwd = createTempDir('gsd-3308-h2-');
t.after(() => cleanup(cwd));
writeState(cwd, { milestone: 'v1.0' });
writeRoadmap(cwd, ['## v1.0 Current 🚧', ''].join('\n'));
fs.mkdirSync(path.join(planningDirOf(cwd), 'phases'), { recursive: true });
const snap = buildPlanningSnapshot(cwd);
assert.deepStrictEqual(snap.phaseDirs, { value: [], scope: SCOPE.COMPLETE });
assert.deepStrictEqual(snap.phases, { value: [], scope: SCOPE.COMPLETE });
});
test('single phase directory produces a one-element phases array', (t) => {
const cwd = createTempDir('gsd-3308-h3-');
t.after(() => cleanup(cwd));
writeState(cwd, { milestone: 'v1.0' });
writeRoadmap(cwd, ['## v1.0 Current 🚧', '', '### Phase 1: Foo'].join('\n'));
makeCompletePhaseDir(cwd, '.planning/phases/01-foo');
const snap = buildPlanningSnapshot(cwd);
assert.strictEqual(snap.phaseDirs.value.length, 1);
assert.strictEqual(snap.phases.value.length, 1);
assert.strictEqual(snap.phases.value[0].dir, '01-foo');
});
test('three phase directories each carry independent completion', (t) => {
const cwd = createTempDir('gsd-3308-h4-');
t.after(() => cleanup(cwd));
writeState(cwd, { milestone: 'v1.0' });
writeRoadmap(cwd, [
'## v1.0 Current 🚧', '',
'### Phase 1: Foo', '',
'### Phase 2: Bar', '',
'### Phase 3: Baz',
].join('\n'));
makeCompletePhaseDir(cwd, '.planning/phases/01-foo'); // complete
writeFile(cwd, '.planning/phases/02-bar/02-01-PLAN.md', '# Plan\n'); // no summary/verification
writeFile(cwd, '.planning/phases/03-baz/03-01-PLAN.md', '# Plan\n');
writeFile(cwd, '.planning/phases/03-baz/03-01-SUMMARY.md', '# Summary\n');
writeFile(cwd, '.planning/phases/03-baz/03-VERIFICATION.md', '---\nstatus: gaps_found\n---\n');
const snap = buildPlanningSnapshot(cwd);
assert.strictEqual(snap.phases.value.length, 3);
const foo = snap.phases.value.find((p) => p.dir === '01-foo');
const bar = snap.phases.value.find((p) => p.dir === '02-bar');
const baz = snap.phases.value.find((p) => p.dir === '03-baz');
assert.strictEqual(foo.complete, true);
assert.strictEqual(bar.complete, false);
assert.strictEqual(bar.verificationStatus, 'missing');
assert.strictEqual(baz.complete, false);
assert.strictEqual(baz.verificationStatus, 'gaps_found');
});
test('Phase field under Current Position is extracted verbatim as currentPhaseLabel', (t) => {
const cwd = createTempDir('gsd-3308-h12-');
t.after(() => cleanup(cwd));
writeState(cwd, { milestone: 'v1.0' });
appendToState(cwd, ['', '## Current Position', '', 'Phase: 3 of 8 (User Auth)', ''].join('\n'));
const snap = buildPlanningSnapshot(cwd);
assert.deepStrictEqual(snap.currentPhaseLabel, { value: '3 of 8 (User Auth)', scope: SCOPE.COMPLETE });
});
});
// ═════════════════════════════════════════════════════════════════════════
// Negative space — rows 5, 9, 11, 14
// ═════════════════════════════════════════════════════════════════════════
describe('negative space', () => {
test('absent ROADMAP.md yields a non-COMPLETE milestone but does not crash phase enumeration', (t) => {
const cwd = createTempDir('gsd-3308-n5-');
t.after(() => cleanup(cwd));
fs.mkdirSync(planningDirOf(cwd), { recursive: true });
writeFile(cwd, '.planning/phases/01-foo/01-01-PLAN.md', '# Plan\n');
writeFile(cwd, '.planning/phases/02-bar/02-01-PLAN.md', '# Plan\n');
const snap = buildPlanningSnapshot(cwd);
assert.notStrictEqual(snap.milestone.scope, SCOPE.COMPLETE);
assert.strictEqual(snap.milestone.value, null);
assert.ok(Array.isArray(snap.phaseDirs.value), 'phase enumeration must not throw');
assert.deepStrictEqual(snap.phaseDirs.value.slice().sort(), ['01-foo', '02-bar']);
});
test('absent STATE.md is a non-answer but not an unusable-input diagnostic', (t) => {
const cwd = createTempDir('gsd-3308-n9-');
t.after(() => cleanup(cwd));
fs.mkdirSync(planningDirOf(cwd), { recursive: true });
// No STATE.md at all.
const [snap, emitted] = emissionsDuring(() => buildPlanningSnapshot(cwd));
assert.deepStrictEqual(snap.currentPhaseLabel, { value: null, scope: SCOPE.UNREADABLE });
assert.strictEqual(emitted, 0, 'a project that never ran state.init is not corruption');
});
test('missing Current Position section yields TRUNCATED scope with a whole-body fallback value', (t) => {
const cwd = createTempDir('gsd-3308-n11-');
t.after(() => cleanup(cwd));
writeState(cwd, { milestone: 'v1.0' });
appendToState(cwd, ['', '## Notes', '', 'Phase: 3 of 8 (User Auth)', ''].join('\n'));
const snap = buildPlanningSnapshot(cwd);
assert.strictEqual(snap.currentPhaseLabel.scope, SCOPE.TRUNCATED);
assert.strictEqual(snap.currentPhaseLabel.value, '3 of 8 (User Auth)');
});
test('a truncated milestone window still forwards its over-inclusive phase set, not an empty one', (t) => {
const cwd = createTempDir('gsd-3308-n14-');
t.after(() => cleanup(cwd));
writeState(cwd, { milestone: 'v2.0' });
writeRoadmap(cwd, [
'## v1.0 Planned', '',
'### Phase 1: Foo', '',
'## v2.0 Current 🚧',
].join('\n'));
writeFile(cwd, '.planning/phases/01-foo/01-01-PLAN.md', '# Plan\n');
writeFile(cwd, '.planning/phases/02-bar/02-01-PLAN.md', '# Plan\n');
const snap = buildPlanningSnapshot(cwd);
assert.strictEqual(snap.phaseDirs.scope, SCOPE.TRUNCATED);
assert.deepStrictEqual(snap.phaseDirs.value.slice().sort(), ['01-foo', '02-bar']);
});
test('a legitimately-missing verification file is not conflated with an unreadable phase directory', (t) => {
const cwd = createTempDir('gsd-3308-ns18-');
t.after(() => cleanup(cwd));
writeState(cwd, { milestone: 'v1.0' });
writeRoadmap(cwd, ['## v1.0 Current 🚧', '', '### Phase 1: Foo', '', '### Phase 2: Bar'].join('\n'));
// 01-foo: real, readable directory with no *-VERIFICATION.md at all —
// a well-formed 'missing' answer, scope COMPLETE.
writeFile(cwd, '.planning/phases/01-foo/01-01-PLAN.md', '# Plan\n');
// 02-bar: real directory on disk, but faulted below — a non-answer,
// scope UNREADABLE, even though readVerificationStatus's own no-throw
// fail-open contract still reports 'missing' for the same reason.
const barDir = path.join(planningDirOf(cwd), 'phases', '02-bar');
fs.mkdirSync(barDir, { recursive: true });
injectPhaseDirFault(t, barDir);
const snap = buildPlanningSnapshot(cwd);
const foo = snap.phases.value.find((p) => p.dir === '01-foo');
const bar = snap.phases.value.find((p) => p.dir === '02-bar');
assert.strictEqual(foo.complete, false);
assert.strictEqual(foo.verificationStatus, 'missing');
assert.strictEqual(foo.scope, SCOPE.COMPLETE, 'a genuinely missing verification file is a real answer');
assert.strictEqual(bar.complete, false);
assert.strictEqual(bar.verificationStatus, 'missing');
assert.strictEqual(bar.scope, SCOPE.UNREADABLE, 'an unreadable directory is a non-answer, not a real one');
});
});
// ═════════════════════════════════════════════════════════════════════════
// Hostile input — rows 6, 7, 8, 10, 13
// ═════════════════════════════════════════════════════════════════════════
describe('hostile input', () => {
test('unreadable ROADMAP.md reports milestone scope UNREADABLE', (t) => {
const cwd = createTempDir('gsd-3308-h6-');
t.after(() => cleanup(cwd));
writeState(cwd, { milestone: 'v1.0' });
makeFileUnreadableAsDir(path.join(planningDirOf(cwd), 'ROADMAP.md'));
const snap = buildPlanningSnapshot(cwd);
assert.strictEqual(snap.milestone.scope, SCOPE.UNREADABLE);
});
test('an unreadable nested plans dir taints the phase record to TRUNCATED via worstScope, not COMPLETE', (t) => {
const cwd = createTempDir('gsd-3308-h7-');
t.after(() => cleanup(cwd));
writeState(cwd, { milestone: 'v1.0' });
writeRoadmap(cwd, ['## v1.0 Current 🚧', '', '### Phase 1: Foo'].join('\n'));
makeCompletePhaseDir(cwd, '.planning/phases/01-foo');
// Swap the nested plans/ subdirectory for a regular file: readdirSync
// throws ENOTDIR inside scanPhasePlans, independent of isPhaseComplete's
// own (unaffected) readdirSync(phaseDir) call on the same phase.
makeDirUnreadableAsFile(path.join(planningDirOf(cwd), 'phases', '01-foo', 'plans'));
const snap = buildPlanningSnapshot(cwd);
const phase = snap.phases.value.find((p) => p.dir === '01-foo');
assert.ok(phase, 'expected phase 01-foo in the snapshot');
assert.strictEqual(phase.complete, true, 'isPhaseComplete alone still reads COMPLETE');
assert.strictEqual(phase.scope, SCOPE.TRUNCATED, 'worstScope must promote the record to TRUNCATED');
});
test('an unreadable phase directory reports scope UNREADABLE from both owners combined', (t) => {
const cwd = createTempDir('gsd-3308-h8-');
t.after(() => cleanup(cwd));
writeState(cwd, { milestone: 'v1.0' });
writeRoadmap(cwd, ['## v1.0 Current 🚧', '', '### Phase 1: Foo'].join('\n'));
const phaseDir = path.join(planningDirOf(cwd), 'phases', '01-foo');
fs.mkdirSync(phaseDir, { recursive: true });
injectPhaseDirFault(t, phaseDir);
const snap = buildPlanningSnapshot(cwd);
const phase = snap.phases.value.find((p) => p.dir === '01-foo');
assert.ok(phase, 'the phase must still be enumerated — listMilestonePhaseDirs reads the phases/ dir, not this one');
assert.strictEqual(phase.scope, SCOPE.UNREADABLE, 'worst of two independently-UNREADABLE owner calls');
});
test('unreadable-but-present STATE.md emits exactly one STATE_UNREADABLE diagnostic', (t) => {
const cwd = createTempDir('gsd-3308-h10-');
t.after(() => cleanup(cwd));
makeFileUnreadableAsDir(path.join(planningDirOf(cwd), 'STATE.md'));
const [snap, emitted] = emissionsDuring(() => buildPlanningSnapshot(cwd));
assert.deepStrictEqual(snap.currentPhaseLabel, { value: null, scope: SCOPE.UNREADABLE });
assert.strictEqual(emitted, 1);
});
test('unterminated frontmatter does not double-emit and still yields a body-only phase label', (t) => {
const cwd = createTempDir('gsd-3308-h13-');
t.after(() => cleanup(cwd));
fs.mkdirSync(planningDirOf(cwd), { recursive: true });
// Opens `---` and never closes it. Every non-empty line in the tail is
// frontmatter-shaped (>= 2 keys), so extractFrontmatter's own
// FRONTMATTER_UNTERMINATED diagnostic fires exactly once. stripFrontmatter
// is then a no-op (no closing fence to strip), so `body` is the full raw
// content — the bare `Phase:` line inside it is still reachable via
// stateExtractField's plain line-start pattern.
fs.writeFileSync(
path.join(planningDirOf(cwd), 'STATE.md'),
['---', 'gsd_state_version: 1', 'Phase: 3 of 8 (User Auth)', ''].join('\n'),
);
const [snap, emitted] = emissionsDuring(() => buildPlanningSnapshot(cwd));
assert.strictEqual(emitted, 1, 'exactly one diagnostic reaches the operator, from extractFrontmatter itself');
assert.strictEqual(snap.currentPhaseLabel.value, '3 of 8 (User Auth)');
});
});
// ═════════════════════════════════════════════════════════════════════════
// Independence — rows 15, 16, 17, 19
// ═════════════════════════════════════════════════════════════════════════
describe('independence', () => {
test('phases-level scope is the worst of every individual PhaseSnapshot scope, not the first or last', (t) => {
const cwd = createTempDir('gsd-3308-i15-');
t.after(() => cleanup(cwd));
writeState(cwd, { milestone: 'v1.0' });
writeRoadmap(cwd, ['## v1.0 Current 🚧', '', '### Phase 1: Foo', '', '### Phase 2: Bar'].join('\n'));
makeCompletePhaseDir(cwd, '.planning/phases/01-foo');
makeCompletePhaseDir(cwd, '.planning/phases/02-bar');
makeDirUnreadableAsFile(path.join(planningDirOf(cwd), 'phases', '02-bar', 'plans'));
const snap = buildPlanningSnapshot(cwd);
const foo = snap.phases.value.find((p) => p.dir === '01-foo');
const bar = snap.phases.value.find((p) => p.dir === '02-bar');
assert.strictEqual(foo.scope, SCOPE.COMPLETE);
assert.strictEqual(bar.scope, SCOPE.TRUNCATED);
assert.strictEqual(snap.phases.scope, SCOPE.TRUNCATED, 'array-level scope must be the worst-of, not first/last-wins');
});
test('a truncated phase-enumeration window taints phases.scope even when every phase itself reads COMPLETE', (t) => {
const cwd = createTempDir('gsd-3308-i16-');
t.after(() => cleanup(cwd));
writeState(cwd, { milestone: 'v2.0' });
writeRoadmap(cwd, [
'## v1.0 Planned', '',
'### Phase 1: Foo', '',
'## v2.0 Current 🚧',
].join('\n'));
makeCompletePhaseDir(cwd, '.planning/phases/01-foo');
const snap = buildPlanningSnapshot(cwd);
assert.strictEqual(snap.phaseDirs.scope, SCOPE.TRUNCATED);
const foo = snap.phases.value.find((p) => p.dir === '01-foo');
assert.strictEqual(foo.scope, SCOPE.COMPLETE, 'the individual phase read cleanly');
assert.strictEqual(snap.phases.scope, SCOPE.TRUNCATED, 'the array-level scope must still fold in phaseDirs.scope');
});
test('two calls against unchanged disk state produce identical snapshots', (t) => {
const cwd = createTempDir('gsd-3308-i19-');
t.after(() => cleanup(cwd));
buildHealthyTwoPhaseFixture(cwd);
const first = buildPlanningSnapshot(cwd);
const second = buildPlanningSnapshot(cwd);
assert.deepStrictEqual(first, second, 'buildPlanningSnapshot must be pure w.r.t. disk state — no hidden mutation/caching');
});
});
// ═════════════════════════════════════════════════════════════════════════
// worstScope — pure unit coverage, row 17 (independence / boundary)
// ═════════════════════════════════════════════════════════════════════════
describe('worstScope — pure unit coverage', () => {
const SCOPES = [SCOPE.COMPLETE, SCOPE.TRUNCATED, SCOPE.UNSCOPED, SCOPE.UNREADABLE];
test('COMPLETE only when every input is COMPLETE', () => {
assert.strictEqual(worstScope(SCOPE.COMPLETE), SCOPE.COMPLETE);
assert.strictEqual(worstScope(SCOPE.COMPLETE, SCOPE.COMPLETE), SCOPE.COMPLETE);
assert.strictEqual(worstScope(SCOPE.COMPLETE, SCOPE.COMPLETE, SCOPE.COMPLETE), SCOPE.COMPLETE);
for (const bad of [SCOPE.TRUNCATED, SCOPE.UNSCOPED, SCOPE.UNREADABLE]) {
assert.notStrictEqual(worstScope(SCOPE.COMPLETE, bad), SCOPE.COMPLETE);
assert.notStrictEqual(worstScope(bad, SCOPE.COMPLETE), SCOPE.COMPLETE);
}
});
test('UNREADABLE wins over every other combination', () => {
for (const other of SCOPES) {
assert.strictEqual(worstScope(SCOPE.UNREADABLE, other), SCOPE.UNREADABLE);
assert.strictEqual(worstScope(other, SCOPE.UNREADABLE), SCOPE.UNREADABLE);
}
assert.strictEqual(worstScope(SCOPE.COMPLETE, SCOPE.TRUNCATED, SCOPE.UNSCOPED, SCOPE.UNREADABLE), SCOPE.UNREADABLE);
});
test('worstScope picks the most severe of any scope combination, order-independent', () => {
// Seeded explicitly (no unseeded fc.assert — the Wave-3 defect this
// directive's own text names, PR #3335).
fc.assert(
fc.property(fc.constantFrom(...SCOPES), fc.constantFrom(...SCOPES), (a, b) => {
assert.strictEqual(worstScope(a, b), worstScope(b, a));
}),
{ seed: 20261012 },
);
});
});

View File

@@ -72,7 +72,7 @@ describe('UNUSABLE_REASON', () => {
// (enum + call site + this assertion) instead of a silent widening.
assert.deepStrictEqual(
Object.keys(UNUSABLE_REASON).sort(),
['FRONTMATTER_UNTERMINATED', 'LAST_ACTIVITY_UNPARSEABLE', 'ROADMAP_UNREADABLE'],
['FRONTMATTER_UNTERMINATED', 'LAST_ACTIVITY_UNPARSEABLE', 'ROADMAP_UNREADABLE', 'STATE_UNREADABLE'],
);
assert.strictEqual(UNUSABLE_REASON.FRONTMATTER_UNTERMINATED, 'frontmatter_unterminated');
});
@@ -87,6 +87,53 @@ describe('UNUSABLE_REASON', () => {
});
});
// ─── STATE_UNREADABLE: a STATE.md that exists but could not be read ─────────
describe('STATE_UNREADABLE', () => {
test('a genuinely unreadable STATE.md produces exactly one diagnostic', () => {
_resetUnusableInputWarningsForTests();
const emitted = emissionsDuring(() => {
const wrote = warnUnusableInput({
reason: UNUSABLE_REASON.STATE_UNREADABLE,
source: '/u/state-unreadable.md',
});
assert.strictEqual(wrote, true);
});
assert.strictEqual(emitted, 1);
});
test('the same STATE.md path reported twice yields one diagnostic', () => {
_resetUnusableInputWarningsForTests();
const source = '/u/state-unreadable-dedup/STATE.md';
const emitted = emissionsDuring(() => {
const first = warnUnusableInput({ reason: UNUSABLE_REASON.STATE_UNREADABLE, source });
const repeat = warnUnusableInput({ reason: UNUSABLE_REASON.STATE_UNREADABLE, source });
assert.strictEqual(first, true);
assert.strictEqual(repeat, false, 'same (path, cause) must dedup');
});
assert.strictEqual(emitted, 1);
});
test('two different STATE.md paths are never suppressed as one', () => {
_resetUnusableInputWarningsForTests();
const emitted = emissionsDuring(() => {
warnUnusableInput({
reason: UNUSABLE_REASON.STATE_UNREADABLE,
source: '/u/state-unreadable-a/STATE.md',
});
warnUnusableInput({
reason: UNUSABLE_REASON.STATE_UNREADABLE,
source: '/u/state-unreadable-b/STATE.md',
});
});
assert.strictEqual(emitted, 2, 'keying too coarsely would hide a real second fault');
});
test('the reason value is the frozen string "state_unreadable"', () => {
assert.strictEqual(UNUSABLE_REASON.STATE_UNREADABLE, 'state_unreadable');
});
});
// ─── The discriminator: truncated vs. everything that merely looks like it ───
describe('extractFrontmatter — flags a genuinely truncated frontmatter', () => {