import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { clearInternalHooks, createInternalHookEvent, getRegisteredEventKeys, isAgentBootstrapEvent, registerInternalHook, triggerInternalHook, unregisterInternalHook, type AgentBootstrapHookContext, type InternalHookEvent, } from "./internal-hooks.js"; describe("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("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("isAgentBootstrapEvent", () => { it("returns true for agent:bootstrap events with expected context", () => { const context: AgentBootstrapHookContext = { workspaceDir: "/tmp", bootstrapFiles: [], }; const event = createInternalHookEvent("agent", "bootstrap", "test-session", context); expect(isAgentBootstrapEvent(event)).toBe(true); }); it("returns false for non-bootstrap events", () => { const event = createInternalHookEvent("command", "new", "test-session"); expect(isAgentBootstrapEvent(event)).toBe(false); }); }); 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); }); }); });