fix(#3351): reconcile state.patch report with persisted state.md (#3487)

* fix(#3351): reconcile state.patch report with persisted state.md

* chore(#3351): add changeset fragment

* chore(#3351): backfill pr number in changeset fragment

---------

Co-authored-by: sim <sim@local>
This commit is contained in:
Tom Boucher
2026-08-14 12:18:43 -04:00
committed by GitHub
parent 86a808f8ad
commit 58e3437a48
3 changed files with 114 additions and 0 deletions

View File

@@ -1365,6 +1365,87 @@ describe('cmdStatePatch and cmdStateUpdate (state patch, state update)', () => {
assert.ok(output.failed.includes('Missing'), 'Missing should be in failed list');
});
// #3351: state.patch's `updated`/`failed` report must reflect what actually
// persisted to STATE.md after the write completes — not whether the internal
// text-replace matched frontmatter text that the write pipeline then
// re-derives away (syncStateFrontmatter re-derives current_phase /
// current_phase_name from the body `Phase:` line on every write, and the
// FIELD_CLASSIFICATION preservation rows restore the pre-write values when
// the body source did not change).
describe('#3351: state.patch report reconciled against persisted STATE.md', () => {
const phaseStateMd = [
'---',
'gsd_state_version: 1.0',
'current_phase: 1',
'current_phase_name: alpha',
'risk_level: low',
'status: executing',
'---',
'',
'# Project State',
'',
'## Current Position',
'',
'Phase: 1 (alpha)',
'',
].join('\n');
function readFm(dir) {
return frontmatterLib.extractFrontmatter(
fs.readFileSync(path.join(dir, '.planning', 'STATE.md'), 'utf-8'),
);
}
test('body-derived/curated frontmatter fields re-derived by the write are reported failed, not updated', () => {
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), phaseStateMd);
const result = runGsdTools([
'query',
'state.patch',
JSON.stringify({ current_phase: '7', current_phase_name: 'omega' }),
], tmpDir);
assert.ok(result.success, `state patch failed: ${result.error}`);
const report = JSON.parse(result.output);
assert.deepEqual(report.updated, [], `phantom updates must not be reported: ${result.output}`);
assert.deepEqual(report.failed.sort(), ['current_phase', 'current_phase_name'].sort());
// On-disk truth: neither requested value persisted.
const fm = readFm(tmpDir);
assert.notEqual(String(fm.current_phase), '7', 'current_phase was re-derived away by the write pipeline');
assert.notEqual(String(fm.current_phase_name), 'omega', 'current_phase_name was restored by the curated preservation row');
});
test('mixed patch reports an accurate updated/failed split (one lands, one is re-derived away)', () => {
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), phaseStateMd);
const result = runGsdTools([
'query',
'state.patch',
JSON.stringify({ risk_level: 'high', current_phase: '7' }),
], tmpDir);
assert.ok(result.success, `state patch failed: ${result.error}`);
const report = JSON.parse(result.output);
assert.deepEqual(report.updated, ['risk_level']);
assert.deepEqual(report.failed, ['current_phase']);
const fm = readFm(tmpDir);
assert.equal(String(fm.risk_level), 'high', 'the custom frontmatter key must still land');
assert.notEqual(String(fm.current_phase), '7');
});
test('empty patch still reports both arrays empty', () => {
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), phaseStateMd);
const result = runGsdTools(['query', 'state.patch', '{}'], tmpDir);
assert.ok(result.success, `state patch failed: ${result.error}`);
const report = JSON.parse(result.output);
assert.deepEqual(report, { updated: [], failed: [] });
});
});
test('state update changes a single field', () => {
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), stateMd);