feat: initial commit for omp-hooks plugin
This commit is contained in:
commit
a0d4230537
6 files changed
+624
No files matched your search
@@ -0,0 +1,108 @@
|
||||
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 })
|
||||
})
|
||||
Reference in new issue
Block a user