* fix(3265): prefer YAML frontmatter for state-snapshot canonical fields stateSnapshot in both sdk/src/query/state.ts and the CJS twin (get-shit-done/bin/lib/state.cjs cmdStateSnapshot) passed the whole STATE.md blob to stateExtractField, whose bold pattern (**Field:**) has no line anchor. A body table cell such as "**Status:** to ✅ COMPLETE" therefore silenced the correct YAML frontmatter value. Fix: extractFrontmatter(content) first; stripFrontmatter(content) for the body passed to stateExtractField; for each canonical scalar field prefer the non-empty frontmatter value, falling back to body extraction when the key is absent or the file has no frontmatter block at all. Regression tests added in sdk/src/query/state.test.ts (vitest) and tests/state.test.cjs (node:test) covering: - frontmatter status beats **Status:** inside a table cell - frontmatter current_plan beats bold body value - no-frontmatter files continue to extract from body - field absent from frontmatter falls through to body extractor Fixes #3265 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: add changeset for #3275 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * test: reproduce fmStr drops non-string YAML scalars (#3275 CR finding) Add tests/bug-3275-fmstr-non-string-scalars.test.cjs with 5 cases covering CJS state-snapshot with numeric frontmatter scalars (current_phase: 19, total_phases: 7, total_plans_in_phase: 5), string regression, and no-frontmatter body fallback regression. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(state): fmStr accepts numeric/boolean YAML scalars (CR finding) Rename `fmStr` to `fmScalar` in both state.cjs and sdk/src/query/state.ts and broaden the type guard so that non-null number/boolean frontmatter values are coerced to String(v) instead of being discarded. The previous `typeof v === 'string'` check was a latent bug: if the YAML parser ever returns typed scalars (e.g. `current_phase: 19` as the number 19), the frontmatter value would be silently dropped and the stale body value used instead. Both files are updated identically (k014 parity). Also adds three SDK vitest regression cases (numeric current_phase, total_phases, total_plans_in_phase) in sdk/src/query/state.test.ts. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
147 lines
4.6 KiB
JavaScript
147 lines
4.6 KiB
JavaScript
/**
|
|
* GSD Tools Tests — Bug #3275 (CR finding)
|
|
*
|
|
* Regression guard: `state-snapshot` must prefer YAML frontmatter scalar
|
|
* values even when those scalars are numeric (e.g. current_phase: 19) or
|
|
* boolean — not just when they are strings.
|
|
*
|
|
* Prior to the fix, `fmStr` checked `typeof v === 'string'`, so a numeric
|
|
* frontmatter value like `current_phase: 19` was treated as missing and the
|
|
* snapshot fell back to body extraction, which could return a stale or
|
|
* incorrect value.
|
|
*/
|
|
|
|
const { test, describe, beforeEach, afterEach } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { runGsdTools, createTempProject, cleanup } = require('./helpers.cjs');
|
|
|
|
describe('state-snapshot: fmStr accepts non-string YAML scalars (#3275 CR)', () => {
|
|
let tmpDir;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = createTempProject();
|
|
});
|
|
|
|
afterEach(() => {
|
|
cleanup(tmpDir);
|
|
});
|
|
|
|
test('numeric current_phase in frontmatter wins over body extraction', () => {
|
|
// YAML parses bare integers as numbers, not strings.
|
|
// fmStr must not drop the frontmatter value when it is a number.
|
|
const stateMd = [
|
|
'---',
|
|
'gsd_state_version: 1.0',
|
|
'current_phase: 19',
|
|
'---',
|
|
'',
|
|
'# Project State',
|
|
'',
|
|
'**Current Phase:** 03',
|
|
'**Status:** executing',
|
|
'',
|
|
].join('\n');
|
|
|
|
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), stateMd);
|
|
|
|
const result = runGsdTools('state-snapshot', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
// Frontmatter numeric value must win over bold-body value
|
|
assert.strictEqual(output.current_phase, '19', 'numeric frontmatter current_phase must be used');
|
|
});
|
|
|
|
test('numeric total_phases in frontmatter wins over body extraction', () => {
|
|
const stateMd = [
|
|
'---',
|
|
'gsd_state_version: 1.0',
|
|
'total_phases: 7',
|
|
'---',
|
|
'',
|
|
'# Project State',
|
|
'',
|
|
'**Total Phases:** 3',
|
|
'**Status:** executing',
|
|
'',
|
|
].join('\n');
|
|
|
|
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), stateMd);
|
|
|
|
const result = runGsdTools('state-snapshot', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
// Frontmatter says 7, body says 3 — frontmatter must win
|
|
assert.strictEqual(output.total_phases, 7, 'numeric frontmatter total_phases must be used');
|
|
});
|
|
|
|
test('numeric total_plans_in_phase in frontmatter wins over body extraction', () => {
|
|
const stateMd = [
|
|
'---',
|
|
'gsd_state_version: 1.0',
|
|
'total_plans_in_phase: 5',
|
|
'---',
|
|
'',
|
|
'# Project State',
|
|
'',
|
|
'**Total Plans in Phase:** 2',
|
|
'**Status:** executing',
|
|
'',
|
|
].join('\n');
|
|
|
|
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), stateMd);
|
|
|
|
const result = runGsdTools('state-snapshot', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.total_plans_in_phase, 5, 'numeric frontmatter total_plans_in_phase must be used');
|
|
});
|
|
|
|
test('string current_phase in frontmatter still works (no regression)', () => {
|
|
const stateMd = [
|
|
'---',
|
|
'gsd_state_version: 1.0',
|
|
"current_phase: '19'",
|
|
'---',
|
|
'',
|
|
'# Project State',
|
|
'',
|
|
'**Current Phase:** 03',
|
|
'**Status:** executing',
|
|
'',
|
|
].join('\n');
|
|
|
|
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), stateMd);
|
|
|
|
const result = runGsdTools('state-snapshot', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.current_phase, '19', 'string frontmatter current_phase still works');
|
|
});
|
|
|
|
test('no-frontmatter file still extracts from body (no regression)', () => {
|
|
const stateMd = [
|
|
'# Project State',
|
|
'',
|
|
'**Current Phase:** 05',
|
|
'**Total Phases:** 8',
|
|
'**Status:** paused',
|
|
'',
|
|
].join('\n');
|
|
|
|
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), stateMd);
|
|
|
|
const result = runGsdTools('state-snapshot', tmpDir);
|
|
assert.ok(result.success, `Command failed: ${result.error}`);
|
|
|
|
const output = JSON.parse(result.output);
|
|
assert.strictEqual(output.current_phase, '05', 'body extraction still works without frontmatter');
|
|
assert.strictEqual(output.total_phases, 8, 'numeric body total_phases still extracted');
|
|
});
|
|
});
|