113 lines
3.3 KiB
TypeScript
113 lines
3.3 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { HookRegistry } from '../src/hooks.js';
|
|
import { loadHooksFromConfig } from '../src/hook-loader.js';
|
|
|
|
describe('loadHooksFromConfig', () => {
|
|
let tmpDir: string;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-hooks-'));
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('loads deny hooks from config file', async () => {
|
|
const configPath = path.join(tmpDir, 'hooks.json');
|
|
fs.writeFileSync(configPath, JSON.stringify({
|
|
hooks: {
|
|
'pre:tool': [
|
|
{ type: 'deny', tools: ['bash'], pattern: 'rm -rf' },
|
|
],
|
|
},
|
|
}));
|
|
|
|
const registry = new HookRegistry();
|
|
await loadHooksFromConfig(configPath, registry);
|
|
|
|
// Verify a hook was registered by firing pre:tool with matching tool+args
|
|
const result = await registry.fire('pre:tool', {
|
|
toolName: 'bash',
|
|
args: { command: 'rm -rf /' },
|
|
});
|
|
|
|
expect(result.cancelled).toBe(true);
|
|
expect(result.reason).toContain('rm -rf');
|
|
});
|
|
|
|
it('deny hook blocks matching tool and args pattern', async () => {
|
|
const configPath = path.join(tmpDir, 'hooks.json');
|
|
fs.writeFileSync(configPath, JSON.stringify({
|
|
hooks: {
|
|
'pre:tool': [
|
|
{ type: 'deny', tools: ['bash', 'write_file'], pattern: 'secrets' },
|
|
],
|
|
},
|
|
}));
|
|
|
|
const registry = new HookRegistry();
|
|
await loadHooksFromConfig(configPath, registry);
|
|
|
|
// bash with "secrets" in args — should block
|
|
const r1 = await registry.fire('pre:tool', {
|
|
toolName: 'bash',
|
|
args: { command: 'cat secrets.txt' },
|
|
});
|
|
expect(r1.cancelled).toBe(true);
|
|
|
|
// write_file with "secrets" in args — should block
|
|
const r2 = await registry.fire('pre:tool', {
|
|
toolName: 'write_file',
|
|
args: { path: '/tmp/secrets', content: 'data' },
|
|
});
|
|
expect(r2.cancelled).toBe(true);
|
|
});
|
|
|
|
it('deny hook allows non-matching tool or args', async () => {
|
|
const configPath = path.join(tmpDir, 'hooks.json');
|
|
fs.writeFileSync(configPath, JSON.stringify({
|
|
hooks: {
|
|
'pre:tool': [
|
|
{ type: 'deny', tools: ['bash'], pattern: 'rm -rf' },
|
|
],
|
|
},
|
|
}));
|
|
|
|
const registry = new HookRegistry();
|
|
await loadHooksFromConfig(configPath, registry);
|
|
|
|
// Different tool — should allow
|
|
const r1 = await registry.fire('pre:tool', {
|
|
toolName: 'read_file',
|
|
args: { path: '/tmp/rm -rf' },
|
|
});
|
|
expect(r1.cancelled).toBe(false);
|
|
|
|
// Same tool but no pattern match — should allow
|
|
const r2 = await registry.fire('pre:tool', {
|
|
toolName: 'bash',
|
|
args: { command: 'ls -la' },
|
|
});
|
|
expect(r2.cancelled).toBe(false);
|
|
});
|
|
|
|
it('returns silently when config file does not exist', async () => {
|
|
const configPath = path.join(tmpDir, 'nonexistent-hooks.json');
|
|
const registry = new HookRegistry();
|
|
|
|
// Should not throw
|
|
await loadHooksFromConfig(configPath, registry);
|
|
|
|
// Registry should have no hooks — fire returns not cancelled
|
|
const result = await registry.fire('pre:tool', {
|
|
toolName: 'bash',
|
|
args: { command: 'rm -rf /' },
|
|
});
|
|
expect(result.cancelled).toBe(false);
|
|
});
|
|
});
|