diff --git a/get-shit-done/bin/lib/phase.cjs b/get-shit-done/bin/lib/phase.cjs index 805c475b..9e829f06 100644 --- a/get-shit-done/bin/lib/phase.cjs +++ b/get-shit-done/bin/lib/phase.cjs @@ -625,7 +625,7 @@ function renameIntegerPhases(phasesDir, removedInt) { const m = dir.match(/^(\d+)([A-Z])?(?:\.(\d+))?-(.+)$/i); if (!m) return null; const dirInt = parseInt(m[1], 10); - return dirInt > removedInt ? { dir, oldInt: dirInt, letter: m[2] ? m[2].toUpperCase() : '', decimal: m[3] ? parseInt(m[3], 10) : null, slug: m[4] } : null; + return (dirInt > removedInt && dirInt < 999) ? { dir, oldInt: dirInt, letter: m[2] ? m[2].toUpperCase() : '', decimal: m[3] ? parseInt(m[3], 10) : null, slug: m[4] } : null; }) .filter(Boolean) .sort((a, b) => a.oldInt !== b.oldInt ? b.oldInt - a.oldInt : (b.decimal || 0) - (a.decimal || 0)); @@ -673,7 +673,7 @@ function updateRoadmapAfterPhaseRemoval(roadmapPath, targetPhase, isDecimal, rem const oldPad = oldStr.padStart(2, '0'), newPad = newStr.padStart(2, '0'); content = content.replace(new RegExp(`(#{2,4}\\s*Phase\\s+)${oldStr}(\\s*:)`, 'gi'), `$1${newStr}$2`); content = content.replace(new RegExp(`(Phase\\s+)${oldStr}([:\\s])`, 'g'), `$1${newStr}$2`); - content = content.replace(new RegExp(`${oldPad}-(\\d{2})`, 'g'), `${newPad}-$1`); + content = content.replace(new RegExp(`(? { const state = fs.readFileSync(path.join(tmpDir, '.planning', 'STATE.md'), 'utf-8'); assert.ok(state.includes('**Total Phases:** 1'), 'total phases should be decremented'); }); + + test('bug-2434: integer phase remove does not rename 999.x backlog directory', () => { + // Setup: an active integer phase 4 and a backlog phase 999.1 + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap + +### Phase 1: Foundation +**Goal:** Setup + +### Phase 2: Auth +**Goal:** Authentication + +### Phase 3: Features +**Goal:** Core features + +### Phase 4: Extras +**Goal:** Extra stuff + +### Phase 999.1: Backlog item +**Goal:** Parked backlog task +` + ); + + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-auth'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-features'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '04-extras'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '999.1-backlog-item'), { recursive: true }); + + const result = runGsdTools('phase remove 4', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + // Backlog directory must remain at 999.1, not be decremented to 998.1 + assert.ok( + fs.existsSync(path.join(tmpDir, '.planning', 'phases', '999.1-backlog-item')), + 'backlog directory 999.1-backlog-item must not be renamed' + ); + assert.ok( + !fs.existsSync(path.join(tmpDir, '.planning', 'phases', '998.1-backlog-item')), + 'backlog directory must not be incorrectly renamed to 998.1' + ); + }); + + test('bug-2435: integer phase remove does not corrupt YYYY-MM-DD dates in ROADMAP.md', () => { + // Setup: removing phase 4 from a roadmap containing 2026-04-14 date strings + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap + +### Phase 1: Foundation +**Goal:** Setup +**Completed:** 2026-01-15 + +### Phase 2: Auth +**Goal:** Authentication +**Completed:** 2026-02-20 + +### Phase 3: Features +**Goal:** Core features +**Completed:** 2026-04-14 + +### Phase 4: Extras +**Goal:** Extra stuff + +### Phase 5: Final +**Goal:** Final phase +` + ); + + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-auth'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-features'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '04-extras'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '05-final'), { recursive: true }); + + const result = runGsdTools('phase remove 4', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8'); + + // Dates must be preserved exactly + assert.ok(roadmap.includes('2026-01-15'), 'date 2026-01-15 must not be corrupted'); + assert.ok(roadmap.includes('2026-02-20'), 'date 2026-02-20 must not be corrupted'); + assert.ok(roadmap.includes('2026-04-14'), 'date 2026-04-14 must not be corrupted'); + + // Phase 5 should be renumbered to 4 + assert.ok(roadmap.includes('Phase 4: Final'), 'Phase 5 should be renumbered to Phase 4'); + }); + + test('bug-2435: integer phase remove does not corrupt date whose month matches removed phase number', () => { + // Setup: removing phase 4 from a roadmap containing 2026-05-14 + // When renumbering phase 5→4, the regex must not replace "05-14" in the date "2026-05-14" + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap + +### Phase 1: Foundation +**Goal:** Setup +**Completed:** 2026-01-15 + +### Phase 2: Auth +**Goal:** Authentication +**Completed:** 2026-02-20 + +### Phase 3: Features +**Goal:** Core features +**Completed:** 2026-03-10 + +### Phase 4: Extras +**Goal:** Extra stuff + +### Phase 5: Final +**Goal:** Final phase +**Due:** 2026-05-14 +` + ); + + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '01-foundation'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '02-auth'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '03-features'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '04-extras'), { recursive: true }); + fs.mkdirSync(path.join(tmpDir, '.planning', 'phases', '05-final'), { recursive: true }); + + const result = runGsdTools('phase remove 4', tmpDir); + assert.ok(result.success, `Command failed: ${result.error}`); + + const roadmap = fs.readFileSync(path.join(tmpDir, '.planning', 'ROADMAP.md'), 'utf-8'); + + // Date "2026-05-14" must not be corrupted to "2026-04-14" when phase 5 is renumbered to 4 + assert.ok(roadmap.includes('2026-05-14'), 'date 2026-05-14 must not be corrupted when renumbering phase 5→4'); + assert.ok(!roadmap.includes('2026-04-14'), 'date must not be incorrectly mutated to 2026-04-14'); + + // Phase 5 should be renumbered to 4 + assert.ok(roadmap.includes('Phase 4: Final'), 'Phase 5 should be renumbered to Phase 4'); + }); }); // ─────────────────────────────────────────────────────────────────────────────