Initial commit
This commit is contained in:
263
hooks/hooks-app/__tests__/dispatcher.test.ts
Normal file
263
hooks/hooks-app/__tests__/dispatcher.test.ts
Normal file
@@ -0,0 +1,263 @@
|
||||
// plugin/hooks/hooks-app/__tests__/dispatcher.test.ts
|
||||
import { shouldProcessHook, dispatch, gateMatchesKeywords } from '../src/dispatcher';
|
||||
import { HookInput, HookConfig, GateConfig } from '../src/types';
|
||||
import * as fs from 'fs/promises';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
describe('Dispatcher - Event Filtering', () => {
|
||||
test('PostToolUse with enabled tool returns true', () => {
|
||||
const input: HookInput = {
|
||||
hook_event_name: 'PostToolUse',
|
||||
cwd: '/test',
|
||||
tool_name: 'Edit'
|
||||
};
|
||||
|
||||
const hookConfig: HookConfig = {
|
||||
enabled_tools: ['Edit', 'Write']
|
||||
};
|
||||
|
||||
expect(shouldProcessHook(input, hookConfig)).toBe(true);
|
||||
});
|
||||
|
||||
test('PostToolUse with disabled tool returns false', () => {
|
||||
const input: HookInput = {
|
||||
hook_event_name: 'PostToolUse',
|
||||
cwd: '/test',
|
||||
tool_name: 'Read'
|
||||
};
|
||||
|
||||
const hookConfig: HookConfig = {
|
||||
enabled_tools: ['Edit', 'Write']
|
||||
};
|
||||
|
||||
expect(shouldProcessHook(input, hookConfig)).toBe(false);
|
||||
});
|
||||
|
||||
test('SubagentStop with enabled agent returns true', () => {
|
||||
const input: HookInput = {
|
||||
hook_event_name: 'SubagentStop',
|
||||
cwd: '/test',
|
||||
agent_name: 'test-namespace:test-agent'
|
||||
};
|
||||
|
||||
const hookConfig: HookConfig = {
|
||||
enabled_agents: ['test-namespace:test-agent']
|
||||
};
|
||||
|
||||
expect(shouldProcessHook(input, hookConfig)).toBe(true);
|
||||
});
|
||||
|
||||
test('SubagentStop with disabled agent returns false', () => {
|
||||
const input: HookInput = {
|
||||
hook_event_name: 'SubagentStop',
|
||||
cwd: '/test',
|
||||
agent_name: 'other-agent'
|
||||
};
|
||||
|
||||
const hookConfig: HookConfig = {
|
||||
enabled_agents: ['test-namespace:test-agent']
|
||||
};
|
||||
|
||||
expect(shouldProcessHook(input, hookConfig)).toBe(false);
|
||||
});
|
||||
|
||||
test('SubagentStop checks subagent_name if agent_name missing', () => {
|
||||
const input: HookInput = {
|
||||
hook_event_name: 'SubagentStop',
|
||||
cwd: '/test',
|
||||
subagent_name: 'test-namespace:test-agent'
|
||||
};
|
||||
|
||||
const hookConfig: HookConfig = {
|
||||
enabled_agents: ['test-namespace:test-agent']
|
||||
};
|
||||
|
||||
expect(shouldProcessHook(input, hookConfig)).toBe(true);
|
||||
});
|
||||
|
||||
test('UserPromptSubmit always returns true', () => {
|
||||
const input: HookInput = {
|
||||
hook_event_name: 'UserPromptSubmit',
|
||||
cwd: '/test'
|
||||
};
|
||||
|
||||
const hookConfig: HookConfig = {};
|
||||
|
||||
expect(shouldProcessHook(input, hookConfig)).toBe(true);
|
||||
});
|
||||
|
||||
test('No filtering config returns true', () => {
|
||||
const input: HookInput = {
|
||||
hook_event_name: 'PostToolUse',
|
||||
cwd: '/test',
|
||||
tool_name: 'Edit'
|
||||
};
|
||||
|
||||
const hookConfig: HookConfig = {};
|
||||
|
||||
expect(shouldProcessHook(input, hookConfig)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Dispatcher - Gate Chaining', () => {
|
||||
let testDir: string;
|
||||
|
||||
beforeEach(async () => {
|
||||
// Create temporary directory for test config
|
||||
testDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gates-test-'));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up
|
||||
await fs.rm(testDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
test('gate chaining works - gate-a chains to gate-b on pass', async () => {
|
||||
// Create gates.json with chaining config
|
||||
const gatesConfig = {
|
||||
hooks: {
|
||||
PostToolUse: {
|
||||
gates: ['gate-a']
|
||||
}
|
||||
},
|
||||
gates: {
|
||||
'gate-a': {
|
||||
command: 'echo "gate-a passed"',
|
||||
on_pass: 'gate-b' // Chain to gate-b on pass
|
||||
},
|
||||
'gate-b': {
|
||||
command: 'echo "gate-b passed"',
|
||||
on_pass: 'CONTINUE'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await fs.writeFile(path.join(testDir, 'gates.json'), JSON.stringify(gatesConfig, null, 2));
|
||||
|
||||
const input: HookInput = {
|
||||
hook_event_name: 'PostToolUse',
|
||||
cwd: testDir,
|
||||
tool_name: 'Edit'
|
||||
};
|
||||
|
||||
const result = await dispatch(input);
|
||||
|
||||
// Should contain output from both gates
|
||||
expect(result.context).toContain('gate-a passed');
|
||||
expect(result.context).toContain('gate-b passed');
|
||||
expect(result.blockReason).toBeUndefined();
|
||||
});
|
||||
|
||||
test('circular chain prevention - exceeds max gate depth', async () => {
|
||||
// Create gates.json with circular chain
|
||||
const gatesConfig = {
|
||||
hooks: {
|
||||
PostToolUse: {
|
||||
gates: ['gate-a']
|
||||
}
|
||||
},
|
||||
gates: {
|
||||
'gate-a': {
|
||||
command: 'echo "gate-a"',
|
||||
on_pass: 'gate-b'
|
||||
},
|
||||
'gate-b': {
|
||||
command: 'echo "gate-b"',
|
||||
on_pass: 'gate-a' // Circular chain back to gate-a
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await fs.writeFile(path.join(testDir, 'gates.json'), JSON.stringify(gatesConfig, null, 2));
|
||||
|
||||
const input: HookInput = {
|
||||
hook_event_name: 'PostToolUse',
|
||||
cwd: testDir,
|
||||
tool_name: 'Edit'
|
||||
};
|
||||
|
||||
const result = await dispatch(input);
|
||||
|
||||
// Should hit circuit breaker
|
||||
expect(result.blockReason).toContain('Exceeded max gate chain depth');
|
||||
expect(result.blockReason).toContain('circular');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Keyword Matching', () => {
|
||||
test('no keywords - gate always runs', () => {
|
||||
const gateConfig: GateConfig = {
|
||||
command: 'npm test'
|
||||
};
|
||||
|
||||
expect(gateMatchesKeywords(gateConfig, 'hello world')).toBe(true);
|
||||
expect(gateMatchesKeywords(gateConfig, undefined)).toBe(true);
|
||||
expect(gateMatchesKeywords(gateConfig, '')).toBe(true);
|
||||
});
|
||||
|
||||
test('empty keywords array - gate always runs', () => {
|
||||
const gateConfig: GateConfig = {
|
||||
command: 'npm test',
|
||||
keywords: []
|
||||
};
|
||||
|
||||
expect(gateMatchesKeywords(gateConfig, 'hello world')).toBe(true);
|
||||
expect(gateMatchesKeywords(gateConfig, undefined)).toBe(true);
|
||||
});
|
||||
|
||||
test('no user message with keywords - gate does not run', () => {
|
||||
const gateConfig: GateConfig = {
|
||||
command: 'npm test',
|
||||
keywords: ['test', 'testing']
|
||||
};
|
||||
|
||||
expect(gateMatchesKeywords(gateConfig, undefined)).toBe(false);
|
||||
expect(gateMatchesKeywords(gateConfig, '')).toBe(false);
|
||||
});
|
||||
|
||||
test('keyword match - case insensitive', () => {
|
||||
const gateConfig: GateConfig = {
|
||||
command: 'npm test',
|
||||
keywords: ['test']
|
||||
};
|
||||
|
||||
expect(gateMatchesKeywords(gateConfig, 'run the TEST')).toBe(true);
|
||||
expect(gateMatchesKeywords(gateConfig, 'RUN THE Test')).toBe(true);
|
||||
expect(gateMatchesKeywords(gateConfig, 'test this')).toBe(true);
|
||||
});
|
||||
|
||||
test('multiple keywords - any matches', () => {
|
||||
const gateConfig: GateConfig = {
|
||||
command: 'npm test',
|
||||
keywords: ['test', 'testing', 'spec', 'verify']
|
||||
};
|
||||
|
||||
expect(gateMatchesKeywords(gateConfig, 'run the tests')).toBe(true);
|
||||
expect(gateMatchesKeywords(gateConfig, 'verify this works')).toBe(true);
|
||||
expect(gateMatchesKeywords(gateConfig, 'check the spec')).toBe(true);
|
||||
expect(gateMatchesKeywords(gateConfig, 'we are testing')).toBe(true);
|
||||
});
|
||||
|
||||
test('no keyword match - gate does not run', () => {
|
||||
const gateConfig: GateConfig = {
|
||||
command: 'npm test',
|
||||
keywords: ['test', 'testing']
|
||||
};
|
||||
|
||||
expect(gateMatchesKeywords(gateConfig, 'hello world')).toBe(false);
|
||||
expect(gateMatchesKeywords(gateConfig, 'run the linter')).toBe(false);
|
||||
});
|
||||
|
||||
test('substring matching - partial word matches', () => {
|
||||
const gateConfig: GateConfig = {
|
||||
command: 'npm test',
|
||||
keywords: ['test']
|
||||
};
|
||||
|
||||
// Intentional substring matching (not word-boundary)
|
||||
expect(gateMatchesKeywords(gateConfig, 'latest version')).toBe(true);
|
||||
expect(gateMatchesKeywords(gateConfig, 'contest results')).toBe(true);
|
||||
expect(gateMatchesKeywords(gateConfig, 'testing')).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user