import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { clearInternalHooks, createInternalHookEvent, getRegisteredEventKeys, registerInternalHook, triggerInternalHook, unregisterInternalHook, type InternalHookEvent, } from './internal-hooks.js'; describe('internal-hooks', () => { beforeEach(() => { clearInternalHooks(); }); afterEach(() => { clearInternalHooks(); }); describe('registerInternalHook', () => { it('should register a hook handler', () => { const handler = vi.fn(); registerInternalHook('command:new', handler); const keys = getRegisteredEventKeys(); expect(keys).toContain('command:new'); }); it('should allow multiple handlers for the same event', () => { const handler1 = vi.fn(); const handler2 = vi.fn(); registerInternalHook('command:new', handler1); registerInternalHook('command:new', handler2); const keys = getRegisteredEventKeys(); expect(keys).toContain('command:new'); }); }); describe('unregisterInternalHook', () => { it('should unregister a specific handler', () => { const handler1 = vi.fn(); const handler2 = vi.fn(); registerInternalHook('command:new', handler1); registerInternalHook('command:new', handler2); unregisterInternalHook('command:new', handler1); const event = createInternalHookEvent('command', 'new', 'test-session'); void triggerInternalHook(event); expect(handler1).not.toHaveBeenCalled(); expect(handler2).toHaveBeenCalled(); }); it('should clean up empty handler arrays', () => { const handler = vi.fn(); registerInternalHook('command:new', handler); unregisterInternalHook('command:new', handler); const keys = getRegisteredEventKeys(); expect(keys).not.toContain('command:new'); }); }); describe('triggerInternalHook', () => { it('should trigger handlers for general event type', async () => { const handler = vi.fn(); registerInternalHook('command', handler); const event = createInternalHookEvent('command', 'new', 'test-session'); await triggerInternalHook(event); expect(handler).toHaveBeenCalledWith(event); }); it('should trigger handlers for specific event action', async () => { const handler = vi.fn(); registerInternalHook('command:new', handler); const event = createInternalHookEvent('command', 'new', 'test-session'); await triggerInternalHook(event); expect(handler).toHaveBeenCalledWith(event); }); it('should trigger both general and specific handlers', async () => { const generalHandler = vi.fn(); const specificHandler = vi.fn(); registerInternalHook('command', generalHandler); registerInternalHook('command:new', specificHandler); const event = createInternalHookEvent('command', 'new', 'test-session'); await triggerInternalHook(event); expect(generalHandler).toHaveBeenCalledWith(event); expect(specificHandler).toHaveBeenCalledWith(event); }); it('should handle async handlers', async () => { const handler = vi.fn(async () => { await new Promise((resolve) => setTimeout(resolve, 10)); }); registerInternalHook('command:new', handler); const event = createInternalHookEvent('command', 'new', 'test-session'); await triggerInternalHook(event); expect(handler).toHaveBeenCalledWith(event); }); it('should catch and log errors from handlers', async () => { const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}); const errorHandler = vi.fn(() => { throw new Error('Handler failed'); }); const successHandler = vi.fn(); registerInternalHook('command:new', errorHandler); registerInternalHook('command:new', successHandler); const event = createInternalHookEvent('command', 'new', 'test-session'); await triggerInternalHook(event); expect(errorHandler).toHaveBeenCalled(); expect(successHandler).toHaveBeenCalled(); expect(consoleError).toHaveBeenCalledWith( expect.stringContaining('Internal hook error'), expect.stringContaining('Handler failed') ); consoleError.mockRestore(); }); it('should not throw if no handlers are registered', async () => { const event = createInternalHookEvent('command', 'new', 'test-session'); await expect(triggerInternalHook(event)).resolves.not.toThrow(); }); }); describe('createInternalHookEvent', () => { it('should create a properly formatted event', () => { const event = createInternalHookEvent('command', 'new', 'test-session', { foo: 'bar', }); expect(event.type).toBe('command'); expect(event.action).toBe('new'); expect(event.sessionKey).toBe('test-session'); expect(event.context).toEqual({ foo: 'bar' }); expect(event.timestamp).toBeInstanceOf(Date); }); it('should use empty context if not provided', () => { const event = createInternalHookEvent('command', 'new', 'test-session'); expect(event.context).toEqual({}); }); }); describe('getRegisteredEventKeys', () => { it('should return all registered event keys', () => { registerInternalHook('command:new', vi.fn()); registerInternalHook('command:stop', vi.fn()); registerInternalHook('session:start', vi.fn()); const keys = getRegisteredEventKeys(); expect(keys).toContain('command:new'); expect(keys).toContain('command:stop'); expect(keys).toContain('session:start'); }); it('should return empty array when no handlers are registered', () => { const keys = getRegisteredEventKeys(); expect(keys).toEqual([]); }); }); describe('clearInternalHooks', () => { it('should remove all registered handlers', () => { registerInternalHook('command:new', vi.fn()); registerInternalHook('command:stop', vi.fn()); clearInternalHooks(); const keys = getRegisteredEventKeys(); expect(keys).toEqual([]); }); }); describe('integration', () => { it('should handle a complete hook lifecycle', async () => { const results: InternalHookEvent[] = []; const handler = vi.fn((event: InternalHookEvent) => { results.push(event); }); // Register registerInternalHook('command:new', handler); // Trigger const event1 = createInternalHookEvent('command', 'new', 'session-1'); await triggerInternalHook(event1); const event2 = createInternalHookEvent('command', 'new', 'session-2'); await triggerInternalHook(event2); // Verify expect(results).toHaveLength(2); expect(results[0].sessionKey).toBe('session-1'); expect(results[1].sessionKey).toBe('session-2'); // Unregister unregisterInternalHook('command:new', handler); // Trigger again - should not call handler const event3 = createInternalHookEvent('command', 'new', 'session-3'); await triggerInternalHook(event3); expect(results).toHaveLength(2); }); }); });