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 <vinicius-tersi@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
});
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user