From d5ef3b4f1f4c6407a54a95419d26ab7678113084 Mon Sep 17 00:00:00 2001 From: Ethan Hurst Date: Wed, 25 Feb 2026 18:17:26 +1000 Subject: [PATCH] test(09-02): add cmdStateResolveBlocker and cmdStateRecordSession tests - 5 tests for cmdStateResolveBlocker: case-insensitive removal, None placeholder on empty, missing text error, missing STATE.md, no-match succeeds - 5 tests for cmdStateRecordSession: update all fields, timestamp-only update, None default resume-file, missing STATE.md, no session fields returns false --- tests/state.test.cjs | 194 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) diff --git a/tests/state.test.cjs b/tests/state.test.cjs index fc2b5e9e2..0d0dae8fd 100644 --- a/tests/state.test.cjs +++ b/tests/state.test.cjs @@ -1051,6 +1051,200 @@ describe('cmdStateUpdateProgress (state update-progress)', () => { }); }); +// ───────────────────────────────────────────────────────────────────────────── +// cmdStateResolveBlocker, cmdStateRecordSession +// ───────────────────────────────────────────────────────────────────────────── + +describe('cmdStateResolveBlocker (state resolve-blocker)', () => { + let tmpDir; + + const blockerFixture = [ + '# Project State', + '', + '## Blockers', + '', + '- Waiting for API credentials', + '- Need design review for dashboard', + '- Pending vendor approval', + '', + '## Session Continuity', + ].join('\n') + '\n'; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('removes matching blocker line (case-insensitive substring match)', () => { + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), blockerFixture); + + const result = runGsdTools('state resolve-blocker --text "api credentials"', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.resolved, true, 'resolved should be true'); + + const updated = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); + assert.ok(!updated.includes('Waiting for API credentials'), 'matched blocker should be removed'); + assert.ok(updated.includes('Need design review for dashboard'), 'other blocker should still be present'); + assert.ok(updated.includes('Pending vendor approval'), 'other blocker should still be present'); + }); + + test('adds None placeholder when last blocker resolved', () => { + const singleBlockerFixture = [ + '# Project State', + '', + '## Blockers', + '', + '- Single blocker', + '', + '## Session Continuity', + ].join('\n') + '\n'; + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), singleBlockerFixture); + + const result = runGsdTools('state resolve-blocker --text "single blocker"', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const updated = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); + assert.ok(!updated.includes('- Single blocker'), 'resolved blocker should be removed'); + + // Section should contain "None" placeholder, not be empty + const sectionMatch = updated.match(/## Blockers\n([\s\S]*?)(?=\n##|$)/i); + assert.ok(sectionMatch, 'Blockers section should still exist'); + assert.ok(sectionMatch[1].includes('None'), 'Blockers section should contain None placeholder'); + }); + + test('returns error when text not provided', () => { + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), blockerFixture); + + const result = runGsdTools('state resolve-blocker', tmpDir); + assert.ok(result.success, `Command should exit 0: ${result.error}`); + + const output = JSON.parse(result.output); + assert.ok(output.error !== undefined, 'output should have error field'); + assert.ok( + output.error.toLowerCase().includes('text'), + 'error should mention text required' + ); + }); + + test('returns error when STATE.md missing', () => { + const result = runGsdTools('state resolve-blocker --text "anything"', tmpDir); + assert.ok(result.success, `Command should exit 0: ${result.error}`); + + const output = JSON.parse(result.output); + assert.ok(output.error !== undefined, 'output should have error field'); + assert.ok(output.error.includes('STATE.md'), 'error should mention STATE.md'); + }); + + test('returns resolved true even if no line matches', () => { + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), blockerFixture); + + const result = runGsdTools('state resolve-blocker --text "nonexistent blocker text"', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.resolved, true, 'resolved should be true even when no line matches'); + }); +}); + +describe('cmdStateRecordSession (state record-session)', () => { + let tmpDir; + + const sessionFixture = [ + '# Project State', + '', + '## Session Continuity', + '', + '**Last session:** 2024-01-10', + '**Stopped at:** Phase 2, Plan 1', + '**Resume file:** None', + ].join('\n') + '\n'; + + beforeEach(() => { + tmpDir = createTempProject(); + }); + + afterEach(() => { + cleanup(tmpDir); + }); + + test('updates session fields with stopped-at and resume-file', () => { + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), sessionFixture); + + const result = runGsdTools( + 'state record-session --stopped-at "Phase 3, Plan 2" --resume-file ".planning/phases/03/03-02-PLAN.md"', + tmpDir + ); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.recorded, true, 'recorded should be true'); + assert.ok(Array.isArray(output.updated), 'updated should be an array'); + + const updated = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); + assert.ok(updated.includes('Phase 3, Plan 2'), 'Stopped at should be updated'); + assert.ok(updated.includes('.planning/phases/03/03-02-PLAN.md'), 'Resume file should be updated'); + + const today = new Date().toISOString().split('T')[0]; + assert.ok(updated.includes(today), 'Last session should be updated to today'); + }); + + test('updates Last session timestamp even with no other options', () => { + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), sessionFixture); + + const result = runGsdTools('state record-session', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.recorded, true, 'recorded should be true'); + + const updated = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); + const today = new Date().toISOString().split('T')[0]; + assert.ok(updated.includes(today), 'Last session should contain today\'s date'); + }); + + test('sets Resume file to None when not specified', () => { + fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), sessionFixture); + + const result = runGsdTools('state record-session --stopped-at "Phase 1 complete"', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const updated = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); + assert.ok(updated.includes('Phase 1 complete'), 'Stopped at should be updated'); + // Resume file should be set to None (default) + const resumeMatch = updated.match(/\*\*Resume file:\*\*\s*(.*)/i); + assert.ok(resumeMatch, 'Resume file field should exist'); + assert.ok(resumeMatch[1].trim() === 'None', 'Resume file should be None when not specified'); + }); + + test('returns error when STATE.md missing', () => { + const result = runGsdTools('state record-session', tmpDir); + assert.ok(result.success, `Command should exit 0: ${result.error}`); + + const output = JSON.parse(result.output); + assert.ok(output.error !== undefined, 'output should have error field'); + assert.ok(output.error.includes('STATE.md'), 'error should mention STATE.md'); + }); + + test('returns recorded false when no session fields found', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'STATE.md'), + '# Project State\n\n**Status:** Active\n**Phase:** 03\n' + ); + + const result = runGsdTools('state record-session', tmpDir); + assert.ok(result.success, `Command should exit 0: ${result.error}`); + + const output = JSON.parse(result.output); + assert.strictEqual(output.recorded, false, 'recorded should be false when no session fields found'); + assert.ok(output.reason !== undefined, 'should have a reason'); + }); +}); + // ───────────────────────────────────────────────────────────────────────────── // summary-extract command // ─────────────────────────────────────────────────────────────────────────────