From 5398ac793d13678c5b5fa5c4b2edc30b55566c34 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Fri, 13 Feb 2026 02:01:57 -0800 Subject: [PATCH] fix: clamp progressBar to prevent RangeError on overflow, add 10 tests progressBar() in skill-create-output.js could crash with RangeError when percent > 100 because repeat() received a negative count. Fixed by clamping filled to [0, width]. New tests: - progressBar edge cases: 0%, 100%, and >100% confidence - Empty patterns/instincts arrays - post-edit-format: null tool_input, missing file_path, prettier failure - setup-package-manager: --detect output completeness, current marker --- scripts/skill-create-output.js | 2 +- tests/hooks/hooks.test.js | 21 +++++++++ tests/scripts/setup-package-manager.test.js | 16 +++++++ tests/scripts/skill-create-output.test.js | 48 +++++++++++++++++++++ 4 files changed, 86 insertions(+), 1 deletion(-) diff --git a/scripts/skill-create-output.js b/scripts/skill-create-output.js index 9ae0ebf9..718fae6c 100644 --- a/scripts/skill-create-output.js +++ b/scripts/skill-create-output.js @@ -53,7 +53,7 @@ function stripAnsi(str) { } function progressBar(percent, width = 30) { - const filled = Math.round(width * percent / 100); + const filled = Math.min(width, Math.max(0, Math.round(width * percent / 100))); const empty = width - filled; const bar = chalk.green('█'.repeat(filled)) + chalk.gray('░'.repeat(empty)); return `${bar} ${chalk.bold(percent)}%`; diff --git a/tests/hooks/hooks.test.js b/tests/hooks/hooks.test.js index d62e0123..7e184631 100644 --- a/tests/hooks/hooks.test.js +++ b/tests/hooks/hooks.test.js @@ -680,6 +680,27 @@ async function runTests() { assert.strictEqual(result.code, 0, 'Should exit 0 for invalid JSON'); })) passed++; else failed++; + if (await asyncTest('handles null tool_input gracefully', async () => { + const stdinJson = JSON.stringify({ tool_input: null }); + const result = await runScript(path.join(scriptsDir, 'post-edit-format.js'), stdinJson); + assert.strictEqual(result.code, 0, 'Should exit 0 for null tool_input'); + assert.ok(result.stdout.includes('tool_input'), 'Should pass through data'); + })) passed++; else failed++; + + if (await asyncTest('handles missing file_path in tool_input', async () => { + const stdinJson = JSON.stringify({ tool_input: {} }); + const result = await runScript(path.join(scriptsDir, 'post-edit-format.js'), stdinJson); + assert.strictEqual(result.code, 0, 'Should exit 0 for missing file_path'); + assert.ok(result.stdout.includes('tool_input'), 'Should pass through data'); + })) passed++; else failed++; + + if (await asyncTest('exits 0 and passes data when prettier is unavailable', async () => { + const stdinJson = JSON.stringify({ tool_input: { file_path: '/nonexistent/path/file.ts' } }); + const result = await runScript(path.join(scriptsDir, 'post-edit-format.js'), stdinJson); + assert.strictEqual(result.code, 0, 'Should exit 0 even when prettier fails'); + assert.ok(result.stdout.includes('tool_input'), 'Should pass through original data'); + })) passed++; else failed++; + // post-edit-typecheck.js tests console.log('\npost-edit-typecheck.js:'); diff --git a/tests/scripts/setup-package-manager.test.js b/tests/scripts/setup-package-manager.test.js index 4a458132..543dd569 100644 --- a/tests/scripts/setup-package-manager.test.js +++ b/tests/scripts/setup-package-manager.test.js @@ -159,6 +159,22 @@ function runTests() { assert.ok(result.stdout.includes('pnpm')); })) passed++; else failed++; + // --detect output completeness + console.log('\n--detect output completeness:'); + + if (test('shows all three command types in detection output', () => { + const result = run(['--detect']); + assert.strictEqual(result.code, 0); + assert.ok(result.stdout.includes('Install:'), 'Should show Install command'); + assert.ok(result.stdout.includes('Run script:'), 'Should show Run script command'); + assert.ok(result.stdout.includes('Execute binary:'), 'Should show Execute binary command'); + })) passed++; else failed++; + + if (test('shows current marker for active package manager', () => { + const result = run(['--detect']); + assert.ok(result.stdout.includes('(current)'), 'Should mark current PM'); + })) passed++; else failed++; + // Summary console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); diff --git a/tests/scripts/skill-create-output.test.js b/tests/scripts/skill-create-output.test.js index 01713134..ce594a17 100644 --- a/tests/scripts/skill-create-output.test.js +++ b/tests/scripts/skill-create-output.test.js @@ -179,6 +179,54 @@ function runTests() { assert.ok(combined.includes('Everything Claude Code'), 'Should include project name'); })) passed++; else failed++; + // progressBar edge cases (tests the clamp fix) + console.log('\nprogressBar edge cases:'); + + if (test('does not crash with confidence > 1.0 (percent > 100)', () => { + const output = new SkillCreateOutput('repo'); + // confidence 1.5 => percent 150 — previously crashed with RangeError + const logs = captureLog(() => output.patterns([ + { name: 'Overconfident', trigger: 'always', confidence: 1.5, evidence: 'too much' }, + ])); + const combined = stripAnsi(logs.join('\n')); + assert.ok(combined.includes('150%'), 'Should show 150%'); + })) passed++; else failed++; + + if (test('renders 0% confidence bar without crash', () => { + const output = new SkillCreateOutput('repo'); + const logs = captureLog(() => output.patterns([ + { name: 'Zero Confidence', trigger: 'never', confidence: 0.0, evidence: 'none' }, + ])); + const combined = stripAnsi(logs.join('\n')); + assert.ok(combined.includes('0%'), 'Should show 0%'); + })) passed++; else failed++; + + if (test('renders 100% confidence bar without crash', () => { + const output = new SkillCreateOutput('repo'); + const logs = captureLog(() => output.patterns([ + { name: 'Perfect', trigger: 'always', confidence: 1.0, evidence: 'certain' }, + ])); + const combined = stripAnsi(logs.join('\n')); + assert.ok(combined.includes('100%'), 'Should show 100%'); + })) passed++; else failed++; + + // Empty array edge cases + console.log('\nempty array edge cases:'); + + if (test('patterns() with empty array produces header but no entries', () => { + const output = new SkillCreateOutput('repo'); + const logs = captureLog(() => output.patterns([])); + const combined = logs.join('\n'); + assert.ok(combined.includes('Patterns'), 'Should show header'); + })) passed++; else failed++; + + if (test('instincts() with empty array produces box but no entries', () => { + const output = new SkillCreateOutput('repo'); + const logs = captureLog(() => output.instincts([])); + const combined = logs.join('\n'); + assert.ok(combined.includes('Instincts'), 'Should show box title'); + })) passed++; else failed++; + // Box drawing crash fix (regression test) console.log('\nbox() crash prevention:');