diff --git a/get-shit-done/bin/lib/commands.cjs b/get-shit-done/bin/lib/commands.cjs index 6978f6365..4f23c5dfb 100644 --- a/get-shit-done/bin/lib/commands.cjs +++ b/get-shit-done/bin/lib/commands.cjs @@ -421,7 +421,7 @@ function cmdProgressRender(cwd, format, raw) { } } catch {} - const percent = totalPlans > 0 ? Math.round((totalSummaries / totalPlans) * 100) : 0; + const percent = totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0; if (format === 'table') { // Render markdown table diff --git a/get-shit-done/bin/lib/roadmap.cjs b/get-shit-done/bin/lib/roadmap.cjs index 8aa08b4d6..b9cc5b416 100644 --- a/get-shit-done/bin/lib/roadmap.cjs +++ b/get-shit-done/bin/lib/roadmap.cjs @@ -208,7 +208,7 @@ function cmdRoadmapAnalyze(cwd, raw) { completed_phases: completedPhases, total_plans: totalPlans, total_summaries: totalSummaries, - progress_percent: totalPlans > 0 ? Math.round((totalSummaries / totalPlans) * 100) : 0, + progress_percent: totalPlans > 0 ? Math.min(100, Math.round((totalSummaries / totalPlans) * 100)) : 0, current_phase: currentPhase ? currentPhase.number : null, next_phase: nextPhase ? nextPhase.number : null, missing_phase_details: missingDetails.length > 0 ? missingDetails : null, diff --git a/get-shit-done/bin/lib/state.cjs b/get-shit-done/bin/lib/state.cjs index 039574439..456f6517a 100644 --- a/get-shit-done/bin/lib/state.cjs +++ b/get-shit-done/bin/lib/state.cjs @@ -238,7 +238,7 @@ function cmdStateUpdateProgress(cwd, raw) { } } - const percent = totalPlans > 0 ? Math.round(totalSummaries / totalPlans * 100) : 0; + const percent = totalPlans > 0 ? Math.min(100, Math.round(totalSummaries / totalPlans * 100)) : 0; const barWidth = 10; const filled = Math.round(percent / 100 * barWidth); const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(barWidth - filled); diff --git a/tests/commands.test.cjs b/tests/commands.test.cjs index a6d776d8e..a7342b4c0 100644 --- a/tests/commands.test.cjs +++ b/tests/commands.test.cjs @@ -473,6 +473,34 @@ describe('progress command', () => { assert.ok(result.output.includes('Phase'), 'should have table header'); assert.ok(result.output.includes('foundation'), 'should include phase name'); }); + + test('does not crash when summaries exceed plans (orphaned SUMMARY.md)', () => { + fs.writeFileSync( + path.join(tmpDir, '.planning', 'ROADMAP.md'), + `# Roadmap v1.0 MVP\n` + ); + const p1 = path.join(tmpDir, '.planning', 'phases', '01-foundation'); + fs.mkdirSync(p1, { recursive: true }); + // 1 plan but 2 summaries (orphaned SUMMARY.md after PLAN.md deletion) + fs.writeFileSync(path.join(p1, '01-01-PLAN.md'), '# Plan'); + fs.writeFileSync(path.join(p1, '01-01-SUMMARY.md'), '# Done'); + fs.writeFileSync(path.join(p1, '01-02-SUMMARY.md'), '# Orphaned summary'); + + // bar format - should not crash with RangeError + const barResult = runGsdTools('progress bar --raw', tmpDir); + assert.ok(barResult.success, `Bar format crashed: ${barResult.error}`); + assert.ok(barResult.output.includes('100%'), 'percent should be clamped to 100%'); + + // table format - should not crash with RangeError + const tableResult = runGsdTools('progress table --raw', tmpDir); + assert.ok(tableResult.success, `Table format crashed: ${tableResult.error}`); + + // json format - percent should be clamped + const jsonResult = runGsdTools('progress json', tmpDir); + assert.ok(jsonResult.success, `JSON format crashed: ${jsonResult.error}`); + const output = JSON.parse(jsonResult.output); + assert.ok(output.percent <= 100, `percent should be <= 100 but got ${output.percent}`); + }); }); // ─────────────────────────────────────────────────────────────────────────────