diff --git a/README.md b/README.md index 26a62b5..cb09d5a 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ Fires when Claude needs user attention. | Hook | Matcher | Description | | ------------------------------------------------------------------- | -------------------------------- | ------------------------------------------ | -| [notify-permission](hook-scripts/notification/notify-permission.js) | `permission_prompt\|idle_prompt` | Sends Slack alerts when Claude needs input | +| [notify-permission](hook-scripts/notification/notify-permission.js) | `permission_prompt\|idle_prompt\|elicitation_dialog` | Sends Slack alerts when Claude needs input | ### Utils @@ -217,7 +217,7 @@ Everything defaults to **deny** โ€” ask mode is strictly opt-in. A common setup: ## ๐Ÿงช Testing -Requires **Node โ‰ฅ 18** (no dependencies to install). All hooks include comprehensive tests, run in CI on Node 18, 20, and 22: +Requires **Node โ‰ฅ 18** (no npm dependencies). The `format-code` tests exercise the real formatters, so have `prettier`, `ruff`, and `uv` on your PATH โ€” CI installs them โ€” or expect those few tests to fail. All hooks include comprehensive tests, run in CI on Node 18, 20, and 22: ```bash # Run all tests diff --git a/hook-scripts/post-tool-use/format-code.js b/hook-scripts/post-tool-use/format-code.js index 31314d6..0140a5b 100644 --- a/hook-scripts/post-tool-use/format-code.js +++ b/hook-scripts/post-tool-use/format-code.js @@ -95,6 +95,7 @@ async function main() { log({ level: 'ERROR', error: e.message }); return console.log('{}'); } + if (data === null || typeof data !== 'object') data = {}; const { tool_name, tool_input, session_id, cwd } = data; diff --git a/hook-scripts/tests/meta/test-discovery.test.js b/hook-scripts/tests/meta/test-discovery.test.js index 5b6afd2..21278e4 100644 --- a/hook-scripts/tests/meta/test-discovery.test.js +++ b/hook-scripts/tests/meta/test-discovery.test.js @@ -41,10 +41,10 @@ const HOOK_SHAPE = /^hook-scripts\/tests\/[^/]+\/[^/]+\.test\.js$/; const PLUGIN_SHAPE = /^plugins\/[^/]+\/tests\/[^/]+\.test\.js$/; test('every *.test.js is reachable by the npm test glob', () => { - const all = [ - ...walk(path.join(ROOT, 'hook-scripts', 'tests')), - ...walk(path.join(ROOT, 'plugins')), - ]; + // Walk the whole repo, not just the expected roots โ€” a test file dropped + // anywhere else (hook-scripts/pre-tool-use/, site/, repo root, โ€ฆ) must + // fail this guard too, since the npm glob can't see it there either. + const all = walk(ROOT); assert.ok(all.length > 0, 'expected to discover at least one test file'); diff --git a/hook-scripts/tests/post-tool-use/auto-stage.test.js b/hook-scripts/tests/post-tool-use/auto-stage.test.js index a3b221f..e09d4b1 100644 --- a/hook-scripts/tests/post-tool-use/auto-stage.test.js +++ b/hook-scripts/tests/post-tool-use/auto-stage.test.js @@ -17,13 +17,17 @@ const { isInGitRepo, stageFile } = require('../../post-tool-use/auto-stage.js'); const SCRIPT_PATH = path.join(__dirname, '../../post-tool-use/auto-stage.js'); +// Hermetic HOME: the hook logs to ~/.claude/hooks-logs โ€” keep test noise +// out of the real home directory. realpathSync: /var vs /private/var on macOS. +const TEST_HOME = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'hook-test-home-'))); + // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ // Test helpers // โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ function runHook(toolName, toolInput, cwd = '/tmp') { return new Promise((resolve, reject) => { - const child = spawn('node', [SCRIPT_PATH]); + const child = spawn('node', [SCRIPT_PATH], { env: { ...process.env, HOME: TEST_HOME } }); let stdout = '', stderr = ''; child.stdout.on('data', d => stdout += d); @@ -154,7 +158,7 @@ describe('Integration: stdin/stdout hook flow', () => { }); it('handles malformed JSON', async () => { - const child = spawn('node', [SCRIPT_PATH]); + const child = spawn('node', [SCRIPT_PATH], { env: { ...process.env, HOME: TEST_HOME } }); let stdout = ''; const result = await new Promise(resolve => { child.stdout.on('data', d => stdout += d); diff --git a/hook-scripts/tests/post-tool-use/format-code.test.js b/hook-scripts/tests/post-tool-use/format-code.test.js index 13eed0f..813764c 100644 --- a/hook-scripts/tests/post-tool-use/format-code.test.js +++ b/hook-scripts/tests/post-tool-use/format-code.test.js @@ -17,6 +17,10 @@ const { getFormatter, formatFile, log, FORMATTERS } = require('../../post-tool-u const SCRIPT_PATH = path.join(__dirname, '../../post-tool-use/format-code.js'); +// Hermetic HOME: the hook logs to ~/.claude/hooks-logs โ€” keep test noise +// out of the real home directory. realpathSync: /var vs /private/var on macOS. +const TEST_HOME = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'hook-test-home-'))); + // ---------------------------------------------------------------------------- // Test helpers // ---------------------------------------------------------------------------- @@ -44,7 +48,7 @@ function readContent(filePath) { function runHook(hookData) { return new Promise((resolve, reject) => { - const child = spawn('node', [SCRIPT_PATH]); + const child = spawn('node', [SCRIPT_PATH], { env: { ...process.env, HOME: TEST_HOME } }); let stdout = ''; child.stdout.on('data', (data) => { stdout += data; }); @@ -252,7 +256,7 @@ describe('Integration: stdin/stdout hook flow', () => { }); it('returns {} for malformed JSON gracefully', async () => { - const child = spawn('node', [SCRIPT_PATH]); + const child = spawn('node', [SCRIPT_PATH], { env: { ...process.env, HOME: TEST_HOME } }); let stdout = ''; const result = await new Promise((resolve) => { diff --git a/plugins/standup-autopilot/tests/standup-autopilot.test.js b/plugins/standup-autopilot/tests/standup-autopilot.test.js index 07dc502..b2ebb49 100644 --- a/plugins/standup-autopilot/tests/standup-autopilot.test.js +++ b/plugins/standup-autopilot/tests/standup-autopilot.test.js @@ -688,7 +688,12 @@ describe('Integration: spawn with temp HOME', () => { try { const repo = path.join(home, 'myrepo'); fs.mkdirSync(repo); - const git = (...args) => require('node:child_process').execFileSync('git', args, { cwd: repo, stdio: 'pipe' }); + // GIT_CONFIG_GLOBAL/SYSTEM=/dev/null: a contributor's real gitconfig + // (e.g. commit.gpgsign=true) must not leak into this hermetic repo. + const git = (...args) => require('node:child_process').execFileSync('git', args, { + cwd: repo, stdio: 'pipe', + env: { ...process.env, GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_SYSTEM: '/dev/null' }, + }); git('init', '-q'); git('checkout', '-qb', 'feat/standup-test'); fs.writeFileSync(path.join(repo, 'a.txt'), 'one\n');