A phase directory can hold more than one `*-VERIFICATION.md` — an ad-hoc `03-CORRECTION-VERIFICATION.md` worksheet beside the real `03-VERIFICATION.md`. Discovery took the alphabetically-first match, so the worksheet won and the phase could report `missing` while a passing report sat next to it. The issue named two copies. There were seven, in four grammars: two `.sort()[0]` sites in the verification module, three `.find()` over UNSORTED readdir order (phase status, and `verification_path` twice — filesystem-dependent, so two machines on one commit could disagree), and two in shell. All seven now route through one exported `resolveVerificationFile`; the shell copies via a new `verification resolve-file` verb rather than hand-rolling the rule an eighth time. Fixing five of seven would have been worse than fixing none: the verify-work workflow is a WRITER that stamps `status: passed` onto the file it picks, so canonical-aware readers plus an alphabetical writer means the human_needed→passed canonicalization silently no-ops forever while the worksheet gets stamped. That divergence did not exist on next. The first resolver was itself a regression — it preferred ANY canonically-shaped name over the phase's own report, so a stray cross-phase or sentinel-numbered file outranked it. The global-canonical preference was removed rather than narrowed; the rule is pinned to the phase token via `PHASE_NUMBER_TOKEN_SOURCE`, its existing owner. Fixed in passing: the transition workflow's awk guarded on `NR==1` instead of `FNR==1`, so across a multi-file glob it armed only on the first file — a leading worksheet with no frontmatter blocked transition even when the canonical report passed. Also removed a U+00AD soft hyphen introduced earlier on this branch. Five broader-grammar AGGREGATE scans are deliberately out of scope — a different defect class (phase-unscoped scanning), tracked as #3511. Closes #3357 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2215,6 +2215,31 @@ describe('stats command', () => {
|
||||
assert.strictEqual(stats.plan_percent, 67);
|
||||
});
|
||||
|
||||
// #3473 F2 (companion to #3357): determinePhaseStatus now resolves its
|
||||
// *-VERIFICATION.md via the shared resolveVerificationFile resolver instead
|
||||
// of a hand-rolled `.find()` over unsorted readdir() order. Before this fix,
|
||||
// which of a canonical report and an ad-hoc `-CORRECTION-VERIFICATION.md`
|
||||
// worksheet "won" was filesystem-dependent; the canonical report must now
|
||||
// win deterministically regardless of directory-listing order.
|
||||
test('#3473 F2: phase status resolves the canonical report over a -CORRECTION- worksheet, not readdir order', () => {
|
||||
const p1 = path.join(tmpDir, '.planning', 'phases', '03-api');
|
||||
fs.mkdirSync(p1, { recursive: true });
|
||||
fs.writeFileSync(path.join(p1, '03-01-PLAN.md'), '# Plan');
|
||||
fs.writeFileSync(path.join(p1, '03-01-SUMMARY.md'), '# Summary');
|
||||
// The ad-hoc worksheet reports gaps_found; if it won the pick, the phase
|
||||
// would read 'Executed', not 'Complete'.
|
||||
fs.writeFileSync(path.join(p1, '03-CORRECTION-VERIFICATION.md'), '---\nstatus: gaps_found\n---\n# Correction worksheet');
|
||||
fs.writeFileSync(path.join(p1, '03-VERIFICATION.md'), '---\nstatus: passed\n---\n# Verification');
|
||||
|
||||
const result = runGsdTools('stats', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const stats = JSON.parse(result.output);
|
||||
const phase = stats.phases.find(p => p.number === '03');
|
||||
assert.ok(phase, 'phase 03 must be present in stats output');
|
||||
assert.strictEqual(phase.status, 'Complete', 'the canonical 03-VERIFICATION.md must win over the CORRECTION worksheet');
|
||||
});
|
||||
|
||||
test('counts requirements from REQUIREMENTS.md', () => {
|
||||
fs.writeFileSync(
|
||||
path.join(tmpDir, '.planning', 'REQUIREMENTS.md'),
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"paths": {
|
||||
"transition.md": {
|
||||
"reason": "#1526 (delegate, user decision 2026-08-13): added a `post_completion_mode` step at the top of <process> so transition.md is reusable when invoked by execute-phase AFTER phase.complete + verification already ran. The mode documents two invocations — standalone (run all steps) and post-completion delegation (SKIP verify_completion + update_roadmap_and_state to avoid a double phase.complete; RUN cleanup_handoff; BEGIN at evolve_project). Growth (~1.4 KB) is this one new step; no other step changed. (execute-phase.md, also touched by this PR, shrank slightly — its inline update_project_md + offer_next were replaced by a shorter delegation step — so it needs no ack.)"
|
||||
}
|
||||
}
|
||||
}
|
||||
11
tests/emitted-drift-acks/3357-verification-resolver.json
Normal file
11
tests/emitted-drift-acks/3357-verification-resolver.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": 1,
|
||||
"paths": {
|
||||
"transition.md": {
|
||||
"reason": "#3357: verify_completion now resolves THIS phase's own verification report through the shared `verification.resolve-file` seam (src/verification.cts resolveVerificationFile) instead of a blind `*-VERIFICATION.md` glob feeding awk directly, so a stray ad-hoc worksheet (e.g. `03-CORRECTION-VERIFICATION.md`) can no longer alphabetically outrank the real report. Growth is +632 bytes (23,247 -> 23,879): the `gsd_run query verification.resolve-file` call plus its comment, and the FNR-vs-NR frontmatter re-arm fix (with rationale comment) for correctness across the resolver's single-path result. The canonical runtime-launcher preamble was relocated (not duplicated) from `update_roadmap_and_state` up to `verify_completion` since that step now needs `gsd_run` earlier in document order; net preamble count is unchanged at exactly one."
|
||||
},
|
||||
"verify-work.md": {
|
||||
"reason": "#3357: the `03-VERIFICATION.md` staleness check now resolves the phase's own report via `gsd_run query verification.resolve-file \"$PHASE_DIR\" --raw` instead of `ls \"${PHASE_DIR}\"/*-VERIFICATION.md | head -1`, routing through the same shared resolver seam as transition.md so both call sites agree on which file is canonical. Growth is +13 bytes (38,983 -> 38,996), the delta between the old ls/head-1 pipeline and the gsd_run call."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
{
|
||||
"version": 1,
|
||||
"paths": {
|
||||
"ai-integration-phase.md": {
|
||||
"reason": "#3423 (epic #1891 F8): +bytes are the <files_to_read> -> <required_reading> tag rename (15 chars per block); no prose added or rewritten. No tier crossed."
|
||||
},
|
||||
"diagnose-issues.md": "#3423 (epic #1891 F8): +6 bytes are the <files_to_read> -> <required_reading> tag rename (2 tag tokens, +3 bytes each); no prose added or rewritten. No tier crossed.",
|
||||
"eval-review.md": {
|
||||
"reason": "#3423 (epic #1891 F8): +bytes are the <files_to_read> -> <required_reading> tag rename (15 chars per block); no prose added or rewritten. No tier crossed."
|
||||
},
|
||||
"new-milestone.md": {
|
||||
"reason": "#3423 (epic #1891 F8): +bytes are the <files_to_read> -> <required_reading> tag rename (15 chars per block); no prose added or rewritten. No tier crossed."
|
||||
},
|
||||
"quick.md": "#3423 (epic #1891 F8): +12 bytes are the <files_to_read> -> <required_reading> tag rename (4 tag tokens, +3 bytes each); no prose added or rewritten. No tier crossed.",
|
||||
"secure-phase.md": {
|
||||
"reason": "#3423 (epic #1891 F8): +bytes are the <files_to_read> -> <required_reading> tag rename (15 chars per block); no prose added or rewritten. No tier crossed."
|
||||
},
|
||||
"ui-review.md": {
|
||||
"reason": "#3423 (epic #1891 F8): +bytes are the <files_to_read> -> <required_reading> tag rename (15 chars per block); no prose added or rewritten. No tier crossed."
|
||||
},
|
||||
"validate-phase.md": {
|
||||
"reason": "#3423 (epic #1891 F8): +bytes are the <files_to_read> -> <required_reading> tag rename (15 chars per block); no prose added or rewritten. No tier crossed."
|
||||
},
|
||||
"verify-work.md": {
|
||||
"reason": "#3423 (epic #1891 F8): +bytes are the <files_to_read> -> <required_reading> tag rename (15 chars per block); no prose added or rewritten. No tier crossed."
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -120,6 +120,29 @@ describe('init commands', () => {
|
||||
assert.strictEqual(output.uat_path, absPlanningPath(tmpDir, 'phases', '03-api', '03-UAT.md'));
|
||||
});
|
||||
|
||||
// #3473 F2 (companion to #3357): init plan-phase's verification_path
|
||||
// projector now resolves via the shared resolveVerificationFile resolver
|
||||
// instead of a hand-rolled `.find()` over unsorted readdir() order. The
|
||||
// canonical report must win over an ad-hoc -CORRECTION- worksheet
|
||||
// deterministically, regardless of directory-listing order.
|
||||
test('#3473 F2: init plan-phase resolves the canonical report over a -CORRECTION- worksheet', () => {
|
||||
seedPhase(tmpDir, '03-api', {
|
||||
'03-CORRECTION-VERIFICATION.md': '# Ad-hoc correction worksheet',
|
||||
'03-VERIFICATION.md': '# Verification',
|
||||
});
|
||||
writePlanningDocs(tmpDir);
|
||||
|
||||
const result = runGsdTools('init plan-phase 03', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(
|
||||
output.verification_path,
|
||||
absPlanningPath(tmpDir, 'phases', '03-api', '03-VERIFICATION.md'),
|
||||
'the canonical 03-VERIFICATION.md must win over the CORRECTION worksheet',
|
||||
);
|
||||
});
|
||||
|
||||
// #2056: normalizePhaseName() strips ANY [A-Z][A-Z0-9_]*- prefix as a project
|
||||
// code, so a foreign-prefixed workstream/task id like "MEM-01" collapsed to
|
||||
// "01" and resolved to the unrelated numeric Phase 01. init plan-phase must
|
||||
@@ -362,6 +385,27 @@ describe('init commands', () => {
|
||||
assert.strictEqual(output.uat_path, absPlanningPath(tmpDir, 'phases', '03-api', '03-UAT.md'));
|
||||
});
|
||||
|
||||
// #3473 F2 (companion to #3357): init phase-op's verification_path projector
|
||||
// — the second of the two now-fixed init.cts sites — same regression as
|
||||
// the plan-phase test above.
|
||||
test('#3473 F2: init phase-op resolves the canonical report over a -CORRECTION- worksheet', () => {
|
||||
seedPhase(tmpDir, '03-api', {
|
||||
'03-CORRECTION-VERIFICATION.md': '# Ad-hoc correction worksheet',
|
||||
'03-VERIFICATION.md': '# Verification',
|
||||
});
|
||||
writePlanningDocs(tmpDir);
|
||||
|
||||
const result = runGsdTools('init phase-op 03', tmpDir);
|
||||
assert.ok(result.success, `Command failed: ${result.error}`);
|
||||
|
||||
const output = JSON.parse(result.output);
|
||||
assert.strictEqual(
|
||||
output.verification_path,
|
||||
absPlanningPath(tmpDir, 'phases', '03-api', '03-VERIFICATION.md'),
|
||||
'the canonical 03-VERIFICATION.md must win over the CORRECTION worksheet',
|
||||
);
|
||||
});
|
||||
|
||||
test('init plan-phase detects has_reviews and reviews_path when REVIEWS.md exists', () => {
|
||||
seedPhase(tmpDir, '03-api', {
|
||||
'03-REVIEWS.md': '# Cross-AI Reviews',
|
||||
|
||||
@@ -14,8 +14,20 @@
|
||||
* 8. CRLF line endings in frontmatter
|
||||
* 9. Body-only file (no frontmatter block) → missing
|
||||
* 10. Nonexistent phase directory → missing
|
||||
* 11. Multiple *-VERIFICATION.md files → first by sort
|
||||
* 11. Multiple *-VERIFICATION.md files, none matching the phase's own token →
|
||||
* alphabetically-first FALLBACK wins (the phase-pinned rule's #2 tier —
|
||||
* see #3492 below for the primary, phase-pinned tier)
|
||||
* 12. ship.md PHASE_VERIFICATION_INCOMPLETE sentinel (contract anchor for #651 consolidation)
|
||||
* 13. #3357/#3492: `<phase-token>-VERIFICATION.md` resolution — resolveVerificationFile
|
||||
* unit coverage plus behavioral tests through readVerificationStatus and
|
||||
* findStaleVerificationSummary. THE CONTRACT (#3492): a candidate whose
|
||||
* name exactly matches THIS phase's own token always wins, even over a
|
||||
* different phase's canonically-shaped file; alphabetical-first among all
|
||||
* dashed candidates is only the fallback when no exact match exists. The
|
||||
* resolveVerificationFile unit tests are the reliable anchors for this —
|
||||
* the readVerificationStatus/findStaleVerificationSummary behavioral tests
|
||||
* are illustrative (their outcome also depends on directory-basename
|
||||
* token derivation, not exercised in isolation there).
|
||||
*
|
||||
* PORTABILITY: pure JS — no shell-outs, no bash fences.
|
||||
* Cross-platform (passes on Windows). Ref: DEFECT.TEST-SHELL-PIPELINE-NONPORTABLE.
|
||||
@@ -35,7 +47,9 @@ const {
|
||||
VERIFIER_STATUSES,
|
||||
VERIFICATION_ROUTING_TABLE,
|
||||
defaultPhaseCleanCommitTimesMs,
|
||||
resolveVerificationFile,
|
||||
readVerificationStatus,
|
||||
findStaleVerificationSummary,
|
||||
} = require('../gsd-core/bin/lib/verification.cjs');
|
||||
|
||||
// #3145: class-norm timeout, not a per-suite value — see helpers/timeouts.cjs.
|
||||
@@ -313,8 +327,14 @@ describe('verification-status', () => {
|
||||
assert.equal(result.next_command, '/gsd-execute-phase');
|
||||
});
|
||||
|
||||
// Multiple *-VERIFICATION.md files → deterministic pick (first by sort)
|
||||
test('multiple *-VERIFICATION.md files in dir → first by sort order wins', () => {
|
||||
// Multiple *-VERIFICATION.md files, NEITHER matching the phase dir's own
|
||||
// token → deterministic FALLBACK pick (first by sort). This is the #2 tier
|
||||
// of the #3492 phase-pinned rule, not the contract itself — see the
|
||||
// `#3357/#3492` describe block below for the primary, phase-pinned tier
|
||||
// (resolveVerificationFile unit tests are the reliable anchors there).
|
||||
// mkPhaseDir's random-suffixed basename never matches "01"/"02", so this
|
||||
// exercises the fallback by construction.
|
||||
test('multiple *-VERIFICATION.md files, none matching the phase token → alphabetically-first FALLBACK wins', () => {
|
||||
const dir = mkPhaseDir('multi');
|
||||
try {
|
||||
// Write two files: alphabetically "01-a" comes before "02-b"
|
||||
@@ -326,7 +346,7 @@ describe('verification-status', () => {
|
||||
assert.equal(
|
||||
result.status,
|
||||
'passed',
|
||||
'When multiple *-VERIFICATION.md files exist, the first by lexicographic sort must be used',
|
||||
'With no exact phase-token match, the first by lexicographic sort must be used',
|
||||
);
|
||||
} finally {
|
||||
cleanup(dir);
|
||||
@@ -811,6 +831,299 @@ describe('verification-status', () => {
|
||||
|
||||
});
|
||||
|
||||
// ─── #3357/#3492: phase-pinned *-VERIFICATION.md resolution ──────────────────
|
||||
//
|
||||
// A phase dir can legitimately hold more than one `*-VERIFICATION.md` — the
|
||||
// real per-phase report (`03-VERIFICATION.md`) alongside an ad-hoc plan
|
||||
// worksheet (`03-CORRECTION-VERIFICATION.md`). The original "alphabetically
|
||||
// first" pick chose the worksheet ('C' < 'V'), and a worksheet with no
|
||||
// frontmatter `status:` made the whole phase read `missing` even though a
|
||||
// passing report sat right next to it (#3357).
|
||||
//
|
||||
// #3492 REGRESSION this block anchors: the #3357 fix's first cut preferred
|
||||
// ANY canonically-shaped `<token>-VERIFICATION.md`, regardless of WHOSE token
|
||||
// it carried — so a stray cross-phase or sentinel-numbered canonical file
|
||||
// (`999-VERIFICATION.md`) could outrank the querying phase's own (possibly
|
||||
// non-canonical) report. THE CONTRACT (verified against the built lib):
|
||||
// ['12-review-VERIFICATION.md', '999-VERIFICATION.md'] resolves to
|
||||
// '12-review-VERIFICATION.md' for phase token '12' (was '999-…').
|
||||
// ['03-CORRECTION-VERIFICATION.md', '04-VERIFICATION.md'] resolves to
|
||||
// '04-VERIFICATION.md' for phase token '04' (was '03-CORRECTION-…').
|
||||
// resolveVerificationFile is the single resolver findStaleVerificationSummary,
|
||||
// readVerificationStatus, commands.cts's determinePhaseStatus, and both
|
||||
// init.cts verification_path projectors all call, every one pinned to its own
|
||||
// phaseDir's token (#3473 F2 / #3492).
|
||||
//
|
||||
// These resolveVerificationFile unit tests are the RELIABLE ANCHORS for the
|
||||
// phase-pinned rule (a real `phaseToken` string, no filesystem/readdir order
|
||||
// involved). The readVerificationStatus/findStaleVerificationSummary
|
||||
// behavioral tests further down are illustrative only — their outcome
|
||||
// additionally depends on the temp directory's basename tokenizing the way
|
||||
// the test expects.
|
||||
describe('#3357/#3492: phase-pinned *-VERIFICATION.md resolution when multiple candidates exist', () => {
|
||||
|
||||
test('#3492 regression counterexample 1: a sentinel-numbered stray file does not outrank this phase\'s own non-canonical report', () => {
|
||||
assert.equal(
|
||||
resolveVerificationFile(
|
||||
['12-review-VERIFICATION.md', '999-VERIFICATION.md'],
|
||||
{ phaseToken: '12-review' },
|
||||
),
|
||||
'12-review-VERIFICATION.md',
|
||||
'this phase (token "12-review") owns 12-review-VERIFICATION.md; 999-VERIFICATION.md is a different phase and must not win',
|
||||
);
|
||||
});
|
||||
|
||||
test('#3492 regression counterexample 2: a cross-phase CORRECTION worksheet does not outrank this phase\'s own canonical report', () => {
|
||||
assert.equal(
|
||||
resolveVerificationFile(
|
||||
['03-CORRECTION-VERIFICATION.md', '04-VERIFICATION.md'],
|
||||
{ phaseToken: '04' },
|
||||
),
|
||||
'04-VERIFICATION.md',
|
||||
'this phase (token "04") owns 04-VERIFICATION.md; the 03-CORRECTION worksheet belongs to a different phase',
|
||||
);
|
||||
});
|
||||
|
||||
test('exact phase-token match wins over an ad-hoc -CORRECTION- worksheet for the SAME phase', () => {
|
||||
assert.equal(
|
||||
resolveVerificationFile(
|
||||
['03-CORRECTION-VERIFICATION.md', '03-VERIFICATION.md'],
|
||||
{ phaseToken: '03' },
|
||||
),
|
||||
'03-VERIFICATION.md',
|
||||
'the phase\'s own 03-VERIFICATION.md must win over its CORRECTION worksheet, not lose alphabetically',
|
||||
);
|
||||
});
|
||||
|
||||
test('order-independence: same candidates reversed → same answer', () => {
|
||||
assert.equal(
|
||||
resolveVerificationFile(
|
||||
['03-VERIFICATION.md', '03-CORRECTION-VERIFICATION.md'],
|
||||
{ phaseToken: '03' },
|
||||
),
|
||||
'03-VERIFICATION.md',
|
||||
'input order must not change which file is selected',
|
||||
);
|
||||
});
|
||||
|
||||
test('decimal phase token: 35.1-VERIFICATION.md wins over a -CORRECTION- sibling', () => {
|
||||
assert.equal(
|
||||
resolveVerificationFile(
|
||||
['35.1-CORRECTION-VERIFICATION.md', '35.1-VERIFICATION.md'],
|
||||
{ phaseToken: '35.1' },
|
||||
),
|
||||
'35.1-VERIFICATION.md',
|
||||
);
|
||||
});
|
||||
|
||||
test('letter-suffixed phase token: 03A-VERIFICATION.md wins over a -CORRECTION- sibling', () => {
|
||||
assert.equal(
|
||||
resolveVerificationFile(
|
||||
['03A-CORRECTION-VERIFICATION.md', '03A-VERIFICATION.md'],
|
||||
{ phaseToken: '03A' },
|
||||
),
|
||||
'03A-VERIFICATION.md',
|
||||
);
|
||||
});
|
||||
|
||||
test('multi-canonical tiebreak: no exact phase-token match among several canonically-shaped candidates → alphabetically first', () => {
|
||||
// Neither candidate's token is "50" — this is the (b) fallback tier, and
|
||||
// it must stay a plain alphabetical pick (not a second, separate
|
||||
// "canonical-shaped" preference — that concept no longer exists; #3492
|
||||
// removed it because it was exactly the regression mechanism above).
|
||||
assert.equal(
|
||||
resolveVerificationFile(
|
||||
['12-VERIFICATION.md', '999-VERIFICATION.md'],
|
||||
{ phaseToken: '50' },
|
||||
),
|
||||
'12-VERIFICATION.md',
|
||||
'"12-VERIFICATION.md" sorts before "999-VERIFICATION.md" and neither matches phase token "50"',
|
||||
);
|
||||
});
|
||||
|
||||
test('fallback: only a non-canonical file present → that file is still returned', () => {
|
||||
// Load-bearing: a phase whose only report is non-canonically named must
|
||||
// keep resolving to it, not to null — even when the phase token is known
|
||||
// and does not exactly match.
|
||||
assert.equal(
|
||||
resolveVerificationFile(['01-review-VERIFICATION.md'], { phaseToken: '01' }),
|
||||
'01-review-VERIFICATION.md',
|
||||
);
|
||||
});
|
||||
|
||||
test('fallback determinism: several non-canonical files, no phase token given → alphabetically first (unchanged)', () => {
|
||||
assert.equal(
|
||||
resolveVerificationFile(['02-b-VERIFICATION.md', '01-a-VERIFICATION.md']),
|
||||
'01-a-VERIFICATION.md',
|
||||
);
|
||||
});
|
||||
|
||||
test('no phaseToken and no exact match → falls back to alphabetically-first, never null, when candidates exist', () => {
|
||||
// #3492: an undeliverable/absent phase token must degrade to the original
|
||||
// pre-#3357 behavior (alphabetically-first), not to null.
|
||||
assert.equal(
|
||||
resolveVerificationFile(['999-VERIFICATION.md', '03-CORRECTION-VERIFICATION.md']),
|
||||
'03-CORRECTION-VERIFICATION.md',
|
||||
'with no phaseToken, plain alphabetical order decides — "03-…" sorts before "999-…"',
|
||||
);
|
||||
});
|
||||
|
||||
test('no matches → null', () => {
|
||||
assert.equal(resolveVerificationFile(['03-PLAN.md', '03-SUMMARY.md'], { phaseToken: '03' }), null);
|
||||
});
|
||||
|
||||
test('unrelated files are not miscounted as candidates', () => {
|
||||
// 03-PLAN.md / 03-SUMMARY.md never end in "-VERIFICATION.md". A bare
|
||||
// "VERIFICATION.md" (no leading phase-token dash) is also never a
|
||||
// candidate — it fails the very `.endsWith('-VERIFICATION.md')` filter
|
||||
// that builds the candidate list in the first place (the string is one
|
||||
// character too short to end with a leading-dash suffix).
|
||||
assert.equal(
|
||||
resolveVerificationFile(['03-PLAN.md', '03-SUMMARY.md', 'VERIFICATION.md'], { phaseToken: '03' }),
|
||||
null,
|
||||
'a bare VERIFICATION.md is never a dashed candidate',
|
||||
);
|
||||
});
|
||||
|
||||
test('behavioral (readVerificationStatus): a phase with both its own report and a cross-phase stray reports the OWN report\'s status, not the stray\'s', () => {
|
||||
// The directory basename is "03-canonical-test" so extractPhaseToken
|
||||
// derives token "03" — the exact same derivation readVerificationStatus
|
||||
// performs internally, so this exercises the real production call path
|
||||
// (not just the pure resolver), pinned to counterexample 2's shape.
|
||||
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3492-parent-'));
|
||||
const dir = path.join(baseDir, '03-canonical-test');
|
||||
fs.mkdirSync(dir);
|
||||
try {
|
||||
// A stray cross-phase canonical file with a DIFFERENT status — must not
|
||||
// be picked for THIS (token "03") phase.
|
||||
writeVerificationMd(dir, '99-VERIFICATION.md', 'gaps_found');
|
||||
// This phase's own (non-canonical, ad-hoc) report — must win.
|
||||
writeVerificationMd(dir, '03-CORRECTION-VERIFICATION.md', 'passed');
|
||||
|
||||
const result = readVerificationStatus(dir);
|
||||
assert.equal(
|
||||
result.status,
|
||||
'passed',
|
||||
'the phase must report its OWN report\'s status, not a cross-phase stray\'s',
|
||||
);
|
||||
} finally {
|
||||
cleanup(baseDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('behavioral (readVerificationStatus): a phase with both its own canonical report and an ad-hoc worksheet reports the canonical report\'s status, not missing (#3357 original regression)', () => {
|
||||
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3357-parent-'));
|
||||
const dir = path.join(baseDir, '03-canonical-test');
|
||||
fs.mkdirSync(dir);
|
||||
try {
|
||||
// The ad-hoc worksheet has no frontmatter `status:` at all — this is
|
||||
// the exact original #3357 failure mode: 'C' < 'V' picked this file
|
||||
// first and the phase read 'missing' despite the passing report sitting
|
||||
// right next to it.
|
||||
fs.writeFileSync(
|
||||
path.join(dir, '03-CORRECTION-VERIFICATION.md'),
|
||||
'# Ad-hoc correction worksheet\n\nNo frontmatter status here.\n',
|
||||
);
|
||||
writeVerificationMd(dir, '03-VERIFICATION.md', 'passed');
|
||||
|
||||
const result = readVerificationStatus(dir);
|
||||
assert.equal(
|
||||
result.status,
|
||||
'passed',
|
||||
'the phase must report the canonical report\'s status, not missing',
|
||||
);
|
||||
} finally {
|
||||
cleanup(baseDir);
|
||||
}
|
||||
});
|
||||
|
||||
test('behavioral (findStaleVerificationSummary): staleness is checked against THIS phase\'s own report, not a cross-phase stray', () => {
|
||||
// A stray cross-phase file ("99-VERIFICATION.md") is alphabetically AFTER
|
||||
// this phase's own "03-VERIFICATION.md", so this also demonstrates the
|
||||
// pin is not merely riding on alphabetical luck.
|
||||
const baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3492-stale-parent-'));
|
||||
const dir = path.join(baseDir, '03-stale-pin-test');
|
||||
fs.mkdirSync(dir);
|
||||
try {
|
||||
writeVerificationMd(dir, '03-VERIFICATION.md', 'passed');
|
||||
writeVerificationMd(dir, '99-VERIFICATION.md', 'passed');
|
||||
setMtime(path.join(dir, '03-VERIFICATION.md'), '2020-01-01T00:00:00Z');
|
||||
setMtime(path.join(dir, '99-VERIFICATION.md'), '2020-01-01T00:00:00Z');
|
||||
|
||||
// Root-style summary placement (mirrors the #2348 fixtures above) —
|
||||
// scanPhasePlans's nested-layout matcher requires `SUMMARY-<NN>...md`
|
||||
// inside a `plans/` subdir; a root-named `03-01-SUMMARY.md` dropped into
|
||||
// `plans/` matches neither isRootSummaryFile (wrong directory) nor
|
||||
// isNestedSummaryFile (wrong filename shape), so summaryFiles reads
|
||||
// empty and the phase is never stale — not what this test means to
|
||||
// exercise.
|
||||
const summaryPath = path.join(dir, '03-01-SUMMARY.md');
|
||||
fs.writeFileSync(summaryPath, '# summary\n');
|
||||
setMtime(summaryPath, '2021-01-01T00:00:00Z');
|
||||
|
||||
// Force the mtime path (no git clock) by injecting an empty resolver —
|
||||
// mirrors the existing #2348 test pattern elsewhere in this file.
|
||||
const result = findStaleVerificationSummary(dir, fs, () => new Map());
|
||||
assert.equal(result.determined, true);
|
||||
assert.equal(result.stale, true, 'the phase\'s own 03-VERIFICATION.md is older than its summary');
|
||||
assert.equal(
|
||||
result.verificationFile,
|
||||
'03-VERIFICATION.md',
|
||||
'staleness must be computed against the phase\'s own report, not the cross-phase 99-VERIFICATION.md stray',
|
||||
);
|
||||
} finally {
|
||||
cleanup(baseDir);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// ─── #3473 F2: resolveVerificationFile allowBare option ──────────────────────
|
||||
//
|
||||
// commands.cts (determinePhaseStatus) and two verification_path projectors in
|
||||
// init.cts each hand-rolled a fourth variant of this same selection: they
|
||||
// additionally accept a BARE `VERIFICATION.md`, which this module's own two
|
||||
// callers (findStaleVerificationSummary, readVerificationStatus) never have.
|
||||
// `allowBare` threads that one behavioral difference through the single
|
||||
// resolver instead of leaving a fourth hand-rolled implementation behind
|
||||
// (#3473 F2). A bare match is ranked BELOW any dashed candidate — canonical
|
||||
// or not — because a dashed file names its phase and a bare one does not.
|
||||
describe('#3473 F2: resolveVerificationFile allowBare option', () => {
|
||||
|
||||
test('allowBare defaults to false — a bare-only list returns null without the option', () => {
|
||||
assert.equal(resolveVerificationFile(['VERIFICATION.md']), null);
|
||||
});
|
||||
|
||||
test('allowBare:true, bare-only candidate → bare file returned', () => {
|
||||
assert.equal(
|
||||
resolveVerificationFile(['VERIFICATION.md'], { allowBare: true }),
|
||||
'VERIFICATION.md',
|
||||
);
|
||||
});
|
||||
|
||||
test('allowBare:true, bare + non-canonical dashed → the dashed fallback wins', () => {
|
||||
assert.equal(
|
||||
resolveVerificationFile(
|
||||
['VERIFICATION.md', '01-review-VERIFICATION.md'],
|
||||
{ allowBare: true },
|
||||
),
|
||||
'01-review-VERIFICATION.md',
|
||||
'a dashed non-canonical file names its phase and must win over a bare match',
|
||||
);
|
||||
});
|
||||
|
||||
test('allowBare:true, bare + canonical → the canonical file wins', () => {
|
||||
assert.equal(
|
||||
resolveVerificationFile(
|
||||
['VERIFICATION.md', '03-VERIFICATION.md'],
|
||||
{ allowBare: true },
|
||||
),
|
||||
'03-VERIFICATION.md',
|
||||
);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// ─── #3057 B3: findStaleVerificationSummary — indeterminate vs not-stale ─────
|
||||
//
|
||||
// The pre-fix catch-all returned `null` on ANY fs / scanPhasePlans / clock
|
||||
|
||||
Reference in New Issue
Block a user