claude-flow
Version:
Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration
322 lines • 14.3 kB
JavaScript
/**
* V3 Hooks System - Tests
*
* Comprehensive tests for hook registry and executor.
*
* @module v3/shared/hooks/hooks.test
*/
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { createHookRegistry } from './registry.js';
import { createHookExecutor } from './executor.js';
import { HookEvent, HookPriority } from './types.js';
import { createEventBus } from '../core/event-bus.js';
describe('HookRegistry', () => {
let registry;
beforeEach(() => {
registry = createHookRegistry();
});
it('should register a hook', () => {
const handler = vi.fn();
const id = registry.register(HookEvent.PreToolUse, handler);
expect(id).toBeDefined();
expect(registry.has(id)).toBe(true);
expect(registry.count()).toBe(1);
});
it('should unregister a hook', () => {
const handler = vi.fn();
const id = registry.register(HookEvent.PreToolUse, handler);
const result = registry.unregister(id);
expect(result).toBe(true);
expect(registry.has(id)).toBe(false);
expect(registry.count()).toBe(0);
});
it('should return false when unregistering non-existent hook', () => {
const result = registry.unregister('non-existent');
expect(result).toBe(false);
});
it('should get handlers sorted by priority', () => {
const handler1 = vi.fn();
const handler2 = vi.fn();
const handler3 = vi.fn();
registry.register(HookEvent.PreToolUse, handler1, HookPriority.Normal);
registry.register(HookEvent.PreToolUse, handler2, HookPriority.High);
registry.register(HookEvent.PreToolUse, handler3, HookPriority.Low);
const handlers = registry.getHandlers(HookEvent.PreToolUse);
expect(handlers).toHaveLength(3);
expect(handlers[0].handler).toBe(handler2); // High priority first
expect(handlers[1].handler).toBe(handler1); // Normal priority second
expect(handlers[2].handler).toBe(handler3); // Low priority last
});
it('should filter disabled hooks', () => {
const handler = vi.fn();
const id = registry.register(HookEvent.PreToolUse, handler, HookPriority.Normal, {
enabled: false,
});
const handlers = registry.getHandlers(HookEvent.PreToolUse);
const allHandlers = registry.getHandlers(HookEvent.PreToolUse, true);
expect(handlers).toHaveLength(0);
expect(allHandlers).toHaveLength(1);
});
it('should enable and disable hooks', () => {
const handler = vi.fn();
const id = registry.register(HookEvent.PreToolUse, handler);
registry.disable(id);
expect(registry.getHandlers(HookEvent.PreToolUse)).toHaveLength(0);
registry.enable(id);
expect(registry.getHandlers(HookEvent.PreToolUse)).toHaveLength(1);
});
it('should list hooks with filters', () => {
registry.register(HookEvent.PreToolUse, vi.fn(), HookPriority.High);
registry.register(HookEvent.PostToolUse, vi.fn(), HookPriority.Normal);
registry.register(HookEvent.PreEdit, vi.fn(), HookPriority.Low);
const allHooks = registry.listHooks();
expect(allHooks).toHaveLength(3);
const preToolHooks = registry.listHooks({ event: HookEvent.PreToolUse });
expect(preToolHooks).toHaveLength(1);
const highPriorityHooks = registry.listHooks({ minPriority: HookPriority.Normal });
expect(highPriorityHooks).toHaveLength(2);
});
it('should get event types', () => {
registry.register(HookEvent.PreToolUse, vi.fn());
registry.register(HookEvent.PostToolUse, vi.fn());
registry.register(HookEvent.PreEdit, vi.fn());
const eventTypes = registry.getEventTypes();
expect(eventTypes).toContain(HookEvent.PreToolUse);
expect(eventTypes).toContain(HookEvent.PostToolUse);
expect(eventTypes).toContain(HookEvent.PreEdit);
});
it('should track statistics', () => {
registry.register(HookEvent.PreToolUse, vi.fn());
registry.register(HookEvent.PostToolUse, vi.fn());
registry.recordExecution(true, 10);
registry.recordExecution(true, 20);
registry.recordExecution(false, 5);
const stats = registry.getStats();
expect(stats.totalHooks).toBe(2);
expect(stats.totalExecutions).toBe(3);
expect(stats.totalFailures).toBe(1);
expect(stats.avgExecutionTime).toBe((10 + 20 + 5) / 3);
});
it('should reset statistics', () => {
registry.recordExecution(true, 10);
registry.resetStats();
const stats = registry.getStats();
expect(stats.totalExecutions).toBe(0);
expect(stats.totalFailures).toBe(0);
});
it('should clear all hooks', () => {
registry.register(HookEvent.PreToolUse, vi.fn());
registry.register(HookEvent.PostToolUse, vi.fn());
registry.clear();
expect(registry.count()).toBe(0);
expect(registry.getEventTypes()).toHaveLength(0);
});
});
describe('HookExecutor', () => {
let registry;
let executor;
let eventBus;
beforeEach(() => {
registry = createHookRegistry();
eventBus = createEventBus();
executor = createHookExecutor(registry, eventBus);
});
it('should execute single hook successfully', async () => {
const handler = vi.fn(async () => ({ success: true }));
registry.register(HookEvent.PreToolUse, handler);
const context = {
event: HookEvent.PreToolUse,
timestamp: new Date(),
tool: { name: 'Read', parameters: { path: 'file.ts' } },
};
const result = await executor.execute(HookEvent.PreToolUse, context);
expect(result.success).toBe(true);
expect(result.hooksExecuted).toBe(1);
expect(result.hooksFailed).toBe(0);
expect(handler).toHaveBeenCalledWith(context);
});
it('should execute multiple hooks in priority order', async () => {
const executionOrder = [];
const handler1 = vi.fn(async () => {
executionOrder.push(1);
return { success: true };
});
const handler2 = vi.fn(async () => {
executionOrder.push(2);
return { success: true };
});
const handler3 = vi.fn(async () => {
executionOrder.push(3);
return { success: true };
});
registry.register(HookEvent.PreToolUse, handler1, HookPriority.Normal);
registry.register(HookEvent.PreToolUse, handler2, HookPriority.High);
registry.register(HookEvent.PreToolUse, handler3, HookPriority.Low);
const context = {
event: HookEvent.PreToolUse,
timestamp: new Date(),
};
await executor.execute(HookEvent.PreToolUse, context);
expect(executionOrder).toEqual([2, 1, 3]); // High, Normal, Low
});
it('should handle hook errors gracefully', async () => {
const handler1 = vi.fn(async () => ({ success: true }));
const handler2 = vi.fn(async () => {
throw new Error('Hook failed');
});
const handler3 = vi.fn(async () => ({ success: true }));
registry.register(HookEvent.PreToolUse, handler1, HookPriority.High);
registry.register(HookEvent.PreToolUse, handler2, HookPriority.Normal);
registry.register(HookEvent.PreToolUse, handler3, HookPriority.Low);
const context = {
event: HookEvent.PreToolUse,
timestamp: new Date(),
};
const result = await executor.execute(HookEvent.PreToolUse, context, {
continueOnError: true,
});
expect(result.hooksExecuted).toBe(3);
expect(result.hooksFailed).toBe(1);
expect(handler3).toHaveBeenCalled(); // Should continue despite error
});
it('should abort on error when continueOnError is false', async () => {
const handler1 = vi.fn(async () => ({ success: true }));
const handler2 = vi.fn(async () => {
throw new Error('Hook failed');
});
const handler3 = vi.fn(async () => ({ success: true }));
registry.register(HookEvent.PreToolUse, handler1, HookPriority.High);
registry.register(HookEvent.PreToolUse, handler2, HookPriority.Normal);
registry.register(HookEvent.PreToolUse, handler3, HookPriority.Low);
const context = {
event: HookEvent.PreToolUse,
timestamp: new Date(),
};
const result = await executor.execute(HookEvent.PreToolUse, context);
expect(result.aborted).toBe(true);
expect(result.hooksExecuted).toBe(2);
expect(handler3).not.toHaveBeenCalled(); // Should not execute after error
});
it('should abort when hook returns abort flag', async () => {
const handler1 = vi.fn(async () => ({ success: true }));
const handler2 = vi.fn(async () => ({ success: true, abort: true }));
const handler3 = vi.fn(async () => ({ success: true }));
registry.register(HookEvent.PreToolUse, handler1, HookPriority.High);
registry.register(HookEvent.PreToolUse, handler2, HookPriority.Normal);
registry.register(HookEvent.PreToolUse, handler3, HookPriority.Low);
const context = {
event: HookEvent.PreToolUse,
timestamp: new Date(),
};
const result = await executor.execute(HookEvent.PreToolUse, context);
expect(result.aborted).toBe(true);
expect(result.hooksExecuted).toBe(2);
expect(handler3).not.toHaveBeenCalled();
});
it('should merge context modifications', async () => {
const handler1 = vi.fn(async () => ({
success: true,
data: { metadata: { modified: true } },
}));
const handler2 = vi.fn(async (context) => {
expect(context.metadata?.modified).toBe(true);
return { success: true };
});
registry.register(HookEvent.PreToolUse, handler1, HookPriority.High);
registry.register(HookEvent.PreToolUse, handler2, HookPriority.Normal);
const context = {
event: HookEvent.PreToolUse,
timestamp: new Date(),
};
const result = await executor.execute(HookEvent.PreToolUse, context);
expect(result.finalContext?.metadata).toEqual({ modified: true });
});
it('should handle timeout', async () => {
const handler = vi.fn(async () => {
await new Promise(resolve => setTimeout(resolve, 200));
return { success: true };
});
registry.register(HookEvent.PreToolUse, handler);
const context = {
event: HookEvent.PreToolUse,
timestamp: new Date(),
};
const result = await executor.executeWithTimeout(HookEvent.PreToolUse, context, 100);
expect(result.success).toBe(false);
expect(result.hooksFailed).toBe(1);
});
it('should execute hooks in parallel', async () => {
const handler1 = vi.fn(async () => ({ success: true }));
const handler2 = vi.fn(async () => ({ success: true }));
registry.register(HookEvent.PreToolUse, handler1);
registry.register(HookEvent.PostToolUse, handler2);
const contexts = [
{ event: HookEvent.PreToolUse, timestamp: new Date() },
{ event: HookEvent.PostToolUse, timestamp: new Date() },
];
const results = await executor.executeParallel([HookEvent.PreToolUse, HookEvent.PostToolUse], contexts);
expect(results).toHaveLength(2);
expect(results[0].success).toBe(true);
expect(results[1].success).toBe(true);
});
it('should execute hooks sequentially with context chaining', async () => {
const handler1 = vi.fn(async () => ({
success: true,
data: { metadata: { step: 1 } },
}));
const handler2 = vi.fn(async () => ({
success: true,
data: { metadata: { step: 2 } },
}));
registry.register(HookEvent.PreToolUse, handler1);
registry.register(HookEvent.PostToolUse, handler2);
const initialContext = {
event: HookEvent.PreToolUse,
timestamp: new Date(),
};
const result = await executor.executeSequential([HookEvent.PreToolUse, HookEvent.PostToolUse], initialContext);
expect(result.success).toBe(true);
expect(result.hooksExecuted).toBe(2);
});
it('should emit events to event bus', async () => {
const preExecuteHandler = vi.fn();
const postExecuteHandler = vi.fn();
eventBus.on('hooks:pre-execute', preExecuteHandler);
eventBus.on('hooks:post-execute', postExecuteHandler);
const handler = vi.fn(async () => ({ success: true }));
registry.register(HookEvent.PreToolUse, handler);
const context = {
event: HookEvent.PreToolUse,
timestamp: new Date(),
};
await executor.execute(HookEvent.PreToolUse, context);
expect(preExecuteHandler).toHaveBeenCalled();
expect(postExecuteHandler).toHaveBeenCalled();
});
it('should skip disabled hooks', async () => {
const handler = vi.fn(async () => ({ success: true }));
const id = registry.register(HookEvent.PreToolUse, handler, HookPriority.Normal, {
enabled: false,
});
const context = {
event: HookEvent.PreToolUse,
timestamp: new Date(),
};
const result = await executor.execute(HookEvent.PreToolUse, context);
expect(result.hooksExecuted).toBe(0);
expect(handler).not.toHaveBeenCalled();
});
it('should record execution statistics', async () => {
const handler = vi.fn(async () => ({ success: true }));
registry.register(HookEvent.PreToolUse, handler);
const context = {
event: HookEvent.PreToolUse,
timestamp: new Date(),
};
await executor.execute(HookEvent.PreToolUse, context);
const stats = registry.getStats();
expect(stats.totalExecutions).toBe(1);
expect(stats.totalFailures).toBe(0);
});
});
//# sourceMappingURL=hooks.test.js.map