From 04e9bd5e7643fddfcf2b35324d77025f5560264f Mon Sep 17 00:00:00 2001 From: Tibsfox Date: Fri, 10 Apr 2026 07:50:17 -0700 Subject: [PATCH] fix(phase): update overview bullet checkbox on phase complete (#1998) (#2000) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cmdPhaseComplete used replaceInCurrentMilestone() to update the overview bullet checkbox (- [ ] → - [x]), but that function scopes replacements to content after the last tag. The current milestone's overview bullets appear before any
blocks, so the replacement never matched. Switch to direct .replace() which correctly finds and updates the first matching unchecked checkbox. This is safe because unchecked checkboxes ([ ]) only exist in the current milestone — archived phases have [x]. Closes #1998 Co-authored-by: Claude Opus 4.6 --- get-shit-done/bin/lib/phase.cjs | 2 +- .../bug-1998-phase-complete-checkbox.test.cjs | 137 ++++++++++++++++++ 2 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 tests/bug-1998-phase-complete-checkbox.test.cjs 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'); + }); +});