From 02d5986049316453d820e8892b9c960f9413959b Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Fri, 13 Feb 2026 08:12:27 -0800 Subject: [PATCH] test: cover setProjectPM save failure, deleteAlias save failure, hooks async/timeout validation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round 72: Add 4 tests for untested code paths (818 → 822): - package-manager.js: setProjectPackageManager wraps writeFile errors (lines 275-279) - session-aliases.js: deleteAlias returns failure when saveAliases fails (line 299) - validate-hooks.js: rejects non-boolean async field (line 28-31) - validate-hooks.js: rejects negative timeout value (lines 32-35) --- tests/ci/validators.test.js | 43 +++++++++++++++++++++++++++++++ tests/lib/package-manager.test.js | 25 ++++++++++++++++++ tests/lib/session-aliases.test.js | 42 ++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+) diff --git a/tests/ci/validators.test.js b/tests/ci/validators.test.js index bf21c399..baf0f36e 100644 --- a/tests/ci/validators.test.js +++ b/tests/ci/validators.test.js @@ -1917,6 +1917,49 @@ function runTests() { cleanupTestDir(testDir); cleanupTestDir(agentsDir); cleanupTestDir(skillsDir); })) passed++; else failed++; + // ── Round 72: validate-hooks.js async/timeout type validation ── + console.log('\nRound 72: validate-hooks.js (async and timeout type validation):'); + + if (test('rejects hook with non-boolean async field', () => { + const testDir = createTestDir(); + const hooksFile = path.join(testDir, 'hooks.json'); + fs.writeFileSync(hooksFile, JSON.stringify({ + PreToolUse: [{ + matcher: 'Write', + hooks: [{ + type: 'intercept', + command: 'echo test', + async: 'yes' // Should be boolean, not string + }] + }] + })); + const result = runValidatorWithDir('validate-hooks', 'HOOKS_FILE', hooksFile); + assert.strictEqual(result.code, 1, 'Should fail on non-boolean async'); + assert.ok(result.stderr.includes('async'), 'Should mention async in error'); + assert.ok(result.stderr.includes('boolean'), 'Should mention boolean type'); + cleanupTestDir(testDir); + })) passed++; else failed++; + + if (test('rejects hook with negative timeout value', () => { + const testDir = createTestDir(); + const hooksFile = path.join(testDir, 'hooks.json'); + fs.writeFileSync(hooksFile, JSON.stringify({ + PostToolUse: [{ + matcher: 'Edit', + hooks: [{ + type: 'intercept', + command: 'echo test', + timeout: -5 // Must be non-negative + }] + }] + })); + const result = runValidatorWithDir('validate-hooks', 'HOOKS_FILE', hooksFile); + assert.strictEqual(result.code, 1, 'Should fail on negative timeout'); + assert.ok(result.stderr.includes('timeout'), 'Should mention timeout in error'); + assert.ok(result.stderr.includes('non-negative'), 'Should mention non-negative'); + cleanupTestDir(testDir); + })) passed++; else failed++; + // Summary console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); diff --git a/tests/lib/package-manager.test.js b/tests/lib/package-manager.test.js index 12173898..7a6be58c 100644 --- a/tests/lib/package-manager.test.js +++ b/tests/lib/package-manager.test.js @@ -1306,6 +1306,31 @@ function runTests() { } })) passed++; else failed++; + // ── Round 72: setProjectPackageManager save failure wraps error ── + console.log('\nRound 72: setProjectPackageManager (save failure):'); + + if (test('setProjectPackageManager throws wrapped error when write fails', () => { + if (process.platform === 'win32' || process.getuid?.() === 0) { + console.log(' (skipped — chmod ineffective on Windows/root)'); + return; + } + const isoProject = path.join(os.tmpdir(), `ecc-pm-proj-r72-${Date.now()}`); + const claudeDir = path.join(isoProject, '.claude'); + fs.mkdirSync(claudeDir, { recursive: true }); + + // Make .claude directory read-only — can't create new files + fs.chmodSync(claudeDir, 0o555); + + try { + assert.throws(() => { + pm.setProjectPackageManager('npm', isoProject); + }, /Failed to save package manager config/); + } finally { + fs.chmodSync(claudeDir, 0o755); + fs.rmSync(isoProject, { recursive: true, force: true }); + } + })) passed++; else failed++; + // Summary console.log('\n=== Test Results ==='); console.log(`Passed: ${passed}`); diff --git a/tests/lib/session-aliases.test.js b/tests/lib/session-aliases.test.js index 17b3712b..8674b1f1 100644 --- a/tests/lib/session-aliases.test.js +++ b/tests/lib/session-aliases.test.js @@ -1030,6 +1030,48 @@ function runTests() { } })) passed++; else failed++; + // ── Round 72: deleteAlias save failure path ── + console.log('\nRound 72: deleteAlias (save failure):'); + + if (test('deleteAlias returns failure when saveAliases fails (read-only dir)', () => { + if (process.platform === 'win32' || process.getuid?.() === 0) { + console.log(' (skipped — chmod ineffective on Windows/root)'); + return; + } + const isoHome = path.join(os.tmpdir(), `ecc-alias-r72-${Date.now()}`); + const isoClaudeDir = path.join(isoHome, '.claude'); + fs.mkdirSync(isoClaudeDir, { recursive: true }); + const savedHome = process.env.HOME; + const savedProfile = process.env.USERPROFILE; + try { + process.env.HOME = isoHome; + process.env.USERPROFILE = isoHome; + delete require.cache[require.resolve('../../scripts/lib/session-aliases')]; + delete require.cache[require.resolve('../../scripts/lib/utils')]; + const freshAliases = require('../../scripts/lib/session-aliases'); + + // Create an alias first (writes the file) + freshAliases.setAlias('to-delete', '/path/session', 'Test'); + const ap = freshAliases.getAliasesPath(); + assert.ok(fs.existsSync(ap), 'Alias file should exist after setAlias'); + + // Make .claude directory read-only — save will fail (can't create temp file) + fs.chmodSync(isoClaudeDir, 0o555); + + const result = freshAliases.deleteAlias('to-delete'); + assert.strictEqual(result.success, false, 'Should fail when save is blocked'); + assert.ok(result.error.includes('Failed to delete alias'), + `Should return delete failure error, got: ${result.error}`); + } finally { + try { fs.chmodSync(isoClaudeDir, 0o755); } catch { /* best-effort */ } + process.env.HOME = savedHome; + process.env.USERPROFILE = savedProfile; + delete require.cache[require.resolve('../../scripts/lib/session-aliases')]; + delete require.cache[require.resolve('../../scripts/lib/utils')]; + fs.rmSync(isoHome, { recursive: true, force: true }); + } + })) passed++; else failed++; + // Summary console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0);