import test from 'node:test' import assert from 'node:assert/strict' import fs from 'node:fs' import path from 'node:path' import os from 'node:os' import { parseClaudeConfig, matchesPattern, substituteCommand, parseHookStdout, executeHookCommand, findProjectHookConfig, } from '../index.js' test('substituteCommand replaces CLAUDE_PROJECT_DIR and CLAUDE_PLUGIN_ROOT', () => { const cmd = 'python3 ${CLAUDE_PROJECT_DIR}/check.py --root ${CLAUDE_PLUGIN_ROOT}' const res = substituteCommand(cmd, { projectDir: '/my/proj', pluginRoot: '/my/plugin', }) assert.equal(res, 'python3 /my/proj/check.py --root /my/plugin') }) test('parseClaudeConfig handles settings.json format with matchers and timeouts', () => { const raw = { hooks: { PreToolUse: [ { matcher: 'bash|write', hooks: [ { type: 'command', command: 'echo "check"', timeout: 5 }, ], }, ], UserPromptSubmit: [ { hooks: [ { type: 'command', command: 'echo "prompt"' }, ], }, ], }, } const parsed = parseClaudeConfig(raw, { projectDir: '/app' }) assert.ok(parsed.PreToolUse) assert.equal(parsed.PreToolUse[0].matcher, 'bash|write') assert.equal(parsed.PreToolUse[0].hooks[0].timeoutSec, 5) assert.ok(parsed.UserPromptSubmit) }) test('matchesPattern tests exact, regex, wildcard, and empty', () => { assert.equal(matchesPattern('*', 'bash'), true) assert.equal(matchesPattern('', 'bash'), true) assert.equal(matchesPattern(undefined, 'bash'), true) assert.equal(matchesPattern('bash', 'bash'), true) assert.equal(matchesPattern('bash', 'read'), false) assert.equal(matchesPattern('Edit|Write', 'Edit'), true) assert.equal(matchesPattern('Edit|Write', 'Write'), true) assert.equal(matchesPattern('Edit|Write', 'bash'), false) }) test('parseHookStdout handles exit code 2 as deny', () => { const res = parseHookStdout('', 2, 'Sensitive file blocked') assert.equal(res.decision, 'deny') assert.equal(res.reason, 'Sensitive file blocked') }) test('parseHookStdout handles JSON decision and context', () => { const jsonOut = JSON.stringify({ decision: 'deny', reason: 'Security violation', additionalContext: 'Remember security rule 101', }) const res = parseHookStdout(jsonOut, 0, '') assert.equal(res.decision, 'deny') assert.equal(res.reason, 'Security violation') assert.deepEqual(res.additionalContext, ['Remember security rule 101']) }) test('executeHookCommand runs shell command and passes stdin payload', async () => { const res = await executeHookCommand( 'read line; echo "Received: $line"', { tool: 'test_tool' }, { timeoutSec: 5 } ) assert.equal(res.exitCode, 0) assert.ok(res.stdout.includes('Received: {"tool":"test_tool"}')) }) test('findProjectHookConfig discovers .claude/settings.json', () => { const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'hooks-test-')) const claudeDir = path.join(tmp, '.claude') fs.mkdirSync(claudeDir) fs.writeFileSync(path.join(claudeDir, 'settings.json'), JSON.stringify({ hooks: { PreToolUse: [ { matcher: 'bash', hooks: [{ type: 'command', command: 'echo 1' }] }, ], }, })) const conf = findProjectHookConfig(tmp) assert.ok(conf) assert.ok(conf.PreToolUse) fs.rmSync(tmp, { recursive: true, force: true }) })