fix(#4806): unparseable VERIFICATION.md frontmatter reports a distinct parse error, never status missing (#4896)

* test(#4806): failing-first — unparseable VERIFICATION.md frontmatter is a parse error, not status missing / Field not found

* fix(#4806): unparseable VERIFICATION.md frontmatter reports a distinct parse error — never 'missing' or 'Field not found'

* test(#4806): census pin 66→67 — cmdFrontmatterGet's unparseable-frontmatter error is a new output({error}) call site

* chore(#4806): backfill changeset PR number (4896)

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-09-20 10:26:53 -04:00
committed by GitHub
parent e2d879f681
commit bd79a97df0
6 changed files with 90 additions and 4 deletions

View File

@@ -783,3 +783,33 @@ describe('frontmatter get — truncated vs absent frontmatter (#1882)', () => {
assert.strictEqual(r.stderr, '', 'a horizontal rule is valid Markdown, not a truncated file');
});
});
// ─── #4806: unparseable frontmatter is a distinct error, not "Field not found" ──
describe('#4806 frontmatter get — unparseable frontmatter', () => {
test('reports a parse error naming the field, not "Field not found"', () => {
// An invalid backslash escape inside a double-quoted value is a YAML
// SYNTAX error: the file HAS a status key but its frontmatter cannot be
// read. Reporting "Field not found" tells the caller the key is absent —
// indistinguishable from a file that genuinely lacks it.
const file = writeTempFile('---\nstatus: "passed\n---\n\n# Verification Report\n');
const result = runGsdTools(['frontmatter', 'get', file, '--field', 'status']);
// The verb answers exit-0 JSON with an `error` FIELD (its documented
// error shape) — the assertion is on the error text, not the exit code.
assert.strictEqual(result.success, true, `command failed: ${result.error}`);
const parsed = JSON.parse(result.output);
assert.ok(
(parsed.error || '').includes('not parseable YAML'),
`must report a parse error, got: ${JSON.stringify(parsed)}`,
);
assert.ok(!parsed.error.includes('Field not found'), 'parse failure must not read as Field not found');
});
test('a well-formed file still returns the field', () => {
const file = writeTempFile('---\nstatus: passed\n---\n\n# V\n');
const result = runGsdTools(['frontmatter', 'get', file, '--field', 'status']);
assert.ok(result.success, `command failed: ${result.error}`);
const parsed = JSON.parse(result.output);
assert.strictEqual(parsed.status, 'passed');
});
});

View File

@@ -914,12 +914,12 @@ describe('#3912 A3-A5: output({error}) records DEGRADED — shape-exhaustive plu
assert.deepStrictEqual(
perFile,
{
'commands.cts': 5, 'frontmatter.cts': 7, 'gsd2-import.cts': 2, 'phase.cts': 4,
'roadmap.cts': 3, 'state.cts': 27, 'template.cts': 3, 'verify.cts': 8, 'workstream.cts': 7, // +1 #3807: advance-plan's ambiguous-position error; +1 #3784: advance-plan's ambiguous-PLAN-position error (two plan spellings, different numbers)
'commands.cts': 5, 'frontmatter.cts': 8, 'gsd2-import.cts': 2, 'phase.cts': 4,
'roadmap.cts': 3, 'state.cts': 27, 'template.cts': 3, 'verify.cts': 8, 'workstream.cts': 7, // +1 #3807: advance-plan's ambiguous-position error; +1 #3784: advance-plan's ambiguous-PLAN-position error (two plan spellings, different numbers); +1 #4806: cmdFrontmatterGet's unparseable-frontmatter error
},
`per-file output({error}) census drifted: ${JSON.stringify(perFile)}`,
);
assert.strictEqual(total, 66, `enumerated output({error}) population drifted from the measured 66 (64 + #3807's ambiguous-position error + #3784's ambiguous-plan-position error): got ${total}`);
assert.strictEqual(total, 67, `enumerated output({error}) population drifted from the measured 67 (66 + #4806's cmdFrontmatterGet unparseable-frontmatter error): got ${total}`);
});
});

View File

@@ -3165,3 +3165,27 @@ describe('#2868: verification status CLI drives the execute-phase stranded-phase
});
});
}
// ─── #4806: unparseable VERIFICATION.md frontmatter is a parse error, not "missing" ──
describe('#4806: unparseable VERIFICATION.md frontmatter reports a parse error, not missing', () => {
test('a VERIFICATION.md whose frontmatter fails to parse reports status unparseable, not missing', () => {
// The file EXISTS and verification ran — "missing" (and its next_command
// re-running execute-phase) is a false statement about the phase. The
// YAML syntax error is in the report, not in the phase's execution.
const dir = mkPhaseDir('unparseable');
fs.writeFileSync(path.join(dir, '01-foo-VERIFICATION.md'),
'---\nstatus: "passed\n---\n\n# Verification Report\n');
const result = readVerificationStatus(dir);
assert.equal(result.status, 'unparseable', 'status must be unparseable');
assert.ok(result.next_action.includes('not parseable YAML'),
'next_action must name the YAML parse failure');
});
test('a well-formed control file still reports passed (unchanged)', () => {
const dir = mkPhaseDir('control');
writeVerificationMd(dir, '01-foo-VERIFICATION.md', 'passed');
const result = readVerificationStatus(dir);
assert.equal(result.status, 'passed');
});
});