109 lines
3.3 KiB
JavaScript
109 lines
3.3 KiB
JavaScript
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 })
|
|
})
|