diff --git a/get-shit-done/bin/lib/phase.cjs b/get-shit-done/bin/lib/phase.cjs index 3bda6758b..4df2beb60 100644 --- a/get-shit-done/bin/lib/phase.cjs +++ b/get-shit-done/bin/lib/phase.cjs @@ -712,7 +712,7 @@ function cmdPhaseComplete(cwd, phaseNum, raw) { `(-\\s*\\[)[ ](\\]\\s*.*Phase\\s+${escapeRegex(phaseNum)}[:\\s][^\\n]*)`, 'i' ); - roadmapContent = replaceInCurrentMilestone(roadmapContent, checkboxPattern, `$1x$2 (completed ${today})`); + roadmapContent = roadmapContent.replace(checkboxPattern, `$1x$2 (completed ${today})`); // Progress table: update Status to Complete, add date (handles 4 or 5 column tables) const phaseEscaped = escapeRegex(phaseNum); diff --git a/tests/bug-1998-phase-complete-checkbox.test.cjs b/tests/bug-1998-phase-complete-checkbox.test.cjs new file mode 100644 index 000000000..61aeafb31 --- /dev/null +++ b/tests/bug-1998-phase-complete-checkbox.test.cjs @@ -0,0 +1,137 @@ +/** + * Regression tests for bug #1998 + * + * phase complete must update the top-level overview bullet checkbox + * (- [ ] Phase N: → - [x] Phase N:) in addition to the Progress table row. + * + * Root cause: the checkbox update used replaceInCurrentMilestone() which + * scopes to content after , missing the current milestone's + * overview bullets that appear before any
blocks. + */ + +'use strict'; + +const { describe, test, beforeEach, afterEach } = require('node:test'); +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const os = require('node:os'); +const { execFileSync } = require('node:child_process'); + +const gsdTools = path.resolve(__dirname, '..', 'get-shit-done', 'bin', 'gsd-tools.cjs'); + +describe('bug #1998: phase complete updates overview checkbox', () => { + let tmpDir; + let planningDir; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-1998-')); + planningDir = path.join(tmpDir, '.planning'); + fs.mkdirSync(planningDir, { recursive: true }); + + // Minimal config + fs.writeFileSync( + path.join(planningDir, 'config.json'), + JSON.stringify({ project_code: 'TEST' }) + ); + + // Minimal STATE.md + fs.writeFileSync( + path.join(planningDir, 'STATE.md'), + '---\ncurrent_phase: 1\nstatus: executing\n---\n# State\n' + ); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + test('checkbox updated when no archived milestones exist', () => { + const phasesDir = path.join(planningDir, 'phases', '01-foundation'); + fs.mkdirSync(phasesDir, { recursive: true }); + fs.writeFileSync( + path.join(phasesDir, '01-1-SUMMARY.md'), + '---\nstatus: complete\n---\n# Summary\nDone.' + ); + fs.writeFileSync( + path.join(phasesDir, '01-1-PLAN.md'), + '---\nphase: 1\nplan: 1\n---\n# Plan 1\n' + ); + + const roadmapPath = path.join(planningDir, 'ROADMAP.md'); + fs.writeFileSync(roadmapPath, [ + '# Roadmap', + '', + '## Phases', + '', + '- [ ] **Phase 1: Foundation** - core setup', + '- [ ] **Phase 2: Features** - add features', + '', + '## Progress', + '', + '| Phase | Plans | Status | Completed |', + '|-------|-------|--------|-----------|', + '| 1. Foundation | 0/1 | Pending | - |', + '| 2. Features | 0/1 | Pending | - |', + ].join('\n')); + + try { + execFileSync('node', [gsdTools, 'phase', 'complete', '1'], { cwd: tmpDir, timeout: 10000 }); + } catch { + // Command may exit non-zero if STATE.md update fails, but ROADMAP.md update happens first + } + + const result = fs.readFileSync(roadmapPath, 'utf-8'); + assert.match(result, /- \[x\] \*\*Phase 1: Foundation\*\*/, 'overview checkbox should be checked'); + assert.match(result, /- \[ \] \*\*Phase 2: Features\*\*/, 'phase 2 checkbox should remain unchecked'); + }); + + test('checkbox updated when archived milestones exist in
', () => { + const phasesDir = path.join(planningDir, 'phases', '01-setup'); + fs.mkdirSync(phasesDir, { recursive: true }); + fs.writeFileSync( + path.join(phasesDir, '01-1-SUMMARY.md'), + '---\nstatus: complete\n---\n# Summary\nDone.' + ); + fs.writeFileSync( + path.join(phasesDir, '01-1-PLAN.md'), + '---\nphase: 1\nplan: 1\n---\n# Plan 1\n' + ); + + const roadmapPath = path.join(planningDir, 'ROADMAP.md'); + fs.writeFileSync(roadmapPath, [ + '# Roadmap v2.0', + '', + '## Phases', + '', + '- [ ] **Phase 1: Setup** - initial setup', + '- [ ] **Phase 2: Build** - build features', + '', + '## Progress', + '', + '| Phase | Plans | Status | Completed |', + '|-------|-------|--------|-----------|', + '| 1. Setup | 0/1 | Pending | - |', + '| 2. Build | 0/1 | Pending | - |', + '', + '
', + 'v1.0 (Archived)', + '', + '## v1.0 Phases', + '- [x] **Phase 1: Init** - initialization', + '- [x] **Phase 2: Deploy** - deployment', + '', + '
', + ].join('\n')); + + try { + execFileSync('node', [gsdTools, 'phase', 'complete', '1'], { cwd: tmpDir, timeout: 10000 }); + } catch { + // May exit non-zero + } + + const result = fs.readFileSync(roadmapPath, 'utf-8'); + assert.match(result, /- \[x\] \*\*Phase 1: Setup\*\*/, 'current milestone checkbox should be checked'); + assert.match(result, /- \[ \] \*\*Phase 2: Build\*\*/, 'phase 2 checkbox should remain unchecked'); + }); +});