Files
clawdbot/src/hooks/internal-hooks.test.ts
2026-01-18 06:15:24 +00:00

248 lines
7.7 KiB
TypeScript

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);
});
});
});