From 3704829aa6669448d3b59b123a389b68dab2593b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=82CHES?= Date: Mon, 23 Feb 2026 09:51:28 -0600 Subject: [PATCH] fix: clamp progress bar percent to prevent RangeError crash (#715) When orphaned SUMMARY.md files cause totalSummaries > totalPlans, the progress percentage exceeds 100%, making String.repeat() throw RangeError on negative arguments. Clamp percent to Math.min(100, ...) at all three computation sites (state, commands, roadmap). Closes #633 Co-authored-by: vinicius-tersi Co-authored-by: Claude Opus 4.6 --- get-shit-done/bin/lib/commands.cjs | 2 +- get-shit-done/bin/lib/roadmap.cjs | 2 +- get-shit-done/bin/lib/state.cjs | 2 +- tests/commands.test.cjs | 28 ++++++++++++++++++++++++++++ 4 files changed, 31 insertions(+), 3 deletions(-) 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}`); + }); }); // ─────────────────────────────────────────────────────────────────────────────