fix: parseMustHavesBlock quoted strings + gsd state complete-phase (#2744)

* fix: parseMustHavesBlock quoted strings + gsd state complete-phase

Bug #2734: parseMustHavesBlock dropped quoted truths containing ':' because
fully-quoted strings like `"App-side UUIDv4: generated locally"` fell into
the kv-parse branch, the regex failed (value starts with '"'), and current
stayed as empty {}. Fix: detect fully-quoted strings before the ':' check
and extract them directly. Two regression tests added to frontmatter.test.cjs.

Bug #2735: `gsd state complete-phase` subcommand was missing — unknown
subcommands fell through to cmdStateLoad. Added cmdStateCompletePhase to
state.cjs (updates Status, Last Activity, and Current Position to COMPLETE),
exported it, and wired it into the case 'state': dispatch in gsd-tools.cjs.

Closes #2734
Closes #2735

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(state): unknown subcommand returns explicit error instead of silent fallthrough

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Tom Boucher
2026-04-26 13:37:43 -04:00
committed by GitHub
parent 3246810876
commit 1405728292
5 changed files with 129 additions and 2 deletions

View File

@@ -66,6 +66,13 @@ describe('dispatcher error paths', () => {
assert.ok(result.error.includes('Invalid --cwd'), `Expected "Invalid --cwd" in stderr, got: ${result.error}`);
});
// Unknown subcommand: state
test('state unknown subcommand errors', () => {
const result = runGsdTools('state bogus', tmpDir);
assert.strictEqual(result.success, false, 'Should exit non-zero');
assert.ok(result.error.includes('Unknown state subcommand'), `Expected "Unknown state subcommand" in stderr, got: ${result.error}`);
});
// Unknown subcommand: template
test('template unknown subcommand errors', () => {
const result = runGsdTools('template bogus', tmpDir);

View File

@@ -512,6 +512,41 @@ must_haves:
assert.strictEqual(result[0].min_lines, 100);
});
test('#2734: quoted truth containing ":" is preserved as a string — not dropped', () => {
// When a dash-item is a fully-quoted string that contains ':', the old code
// fell into the key-value branch, failed the kvMatch regex (because the value
// started with '"'), and silently left current as {}, losing the string.
const content = `---
phase: 01
must_haves:
truths:
- "App-side UUIDv4: generated locally"
- "No colon in this one"
- "Another colon: example"
---
`;
const result = parseMustHavesBlock(content, 'truths');
assert.ok(Array.isArray(result), 'should return an array');
assert.strictEqual(result.length, 3, `expected 3 truths, got ${result.length}: ${JSON.stringify(result)}`);
assert.strictEqual(result[0], 'App-side UUIDv4: generated locally');
assert.strictEqual(result[1], 'No colon in this one');
assert.strictEqual(result[2], 'Another colon: example');
});
test('#2734: single-quoted truth containing ":" is preserved as a string', () => {
const content = `---
phase: 01
must_haves:
truths:
- 'Key: value pattern preserved'
---
`;
const result = parseMustHavesBlock(content, 'truths');
assert.ok(Array.isArray(result), 'should return an array');
assert.strictEqual(result.length, 1);
assert.strictEqual(result[0], 'Key: value pattern preserved');
});
test('handles nested arrays within artifact objects', () => {
const content = `---
phase: 01