import { describe, expect, it, vi } from "vitest"; import { createHookRunner } from "./hooks.js"; import { createMockPluginRegistry, TEST_PLUGIN_AGENT_CTX } from "./hooks.test-helpers.js"; const EVENT = { cleanedBody: "hello world" }; describe("before_agent_reply hook runner (claiming pattern)", () => { it("returns the result when a plugin claims with { handled: true }", async () => { const handler = vi.fn().mockResolvedValue({ handled: true, reply: { text: "intercepted" }, reason: "test-claim", }); const registry = createMockPluginRegistry([{ hookName: "before_agent_reply", handler }]); const runner = createHookRunner(registry); const result = await runner.runBeforeAgentReply(EVENT, TEST_PLUGIN_AGENT_CTX); expect(result).toEqual({ handled: true, reply: { text: "intercepted" }, reason: "test-claim", }); expect(handler).toHaveBeenCalledWith(EVENT, TEST_PLUGIN_AGENT_CTX); }); it("returns undefined when no hooks are registered", async () => { const registry = createMockPluginRegistry([]); const runner = createHookRunner(registry); const result = await runner.runBeforeAgentReply(EVENT, TEST_PLUGIN_AGENT_CTX); expect(result).toBeUndefined(); }); it("stops at first { handled: true } — second handler is not called", async () => { const first = vi.fn().mockResolvedValue({ handled: true, reply: { text: "first" } }); const second = vi.fn().mockResolvedValue({ handled: true, reply: { text: "second" } }); const registry = createMockPluginRegistry([ { hookName: "before_agent_reply", handler: first }, { hookName: "before_agent_reply", handler: second }, ]); const runner = createHookRunner(registry); const result = await runner.runBeforeAgentReply(EVENT, TEST_PLUGIN_AGENT_CTX); expect(result).toEqual({ handled: true, reply: { text: "first" } }); expect(first).toHaveBeenCalledTimes(1); expect(second).not.toHaveBeenCalled(); }); it("returns { handled: true } without reply (swallow pattern)", async () => { const handler = vi.fn().mockResolvedValue({ handled: true }); const registry = createMockPluginRegistry([{ hookName: "before_agent_reply", handler }]); const runner = createHookRunner(registry); const result = await runner.runBeforeAgentReply(EVENT, TEST_PLUGIN_AGENT_CTX); expect(result).toEqual({ handled: true }); expect(result?.reply).toBeUndefined(); }); it("skips a declining plugin (returns void) and lets the next one claim", async () => { const decliner = vi.fn().mockResolvedValue(undefined); const claimer = vi.fn().mockResolvedValue({ handled: true, reply: { text: "claimed" }, }); const registry = createMockPluginRegistry([ { hookName: "before_agent_reply", handler: decliner }, { hookName: "before_agent_reply", handler: claimer }, ]); const runner = createHookRunner(registry); const result = await runner.runBeforeAgentReply(EVENT, TEST_PLUGIN_AGENT_CTX); expect(result).toEqual({ handled: true, reply: { text: "claimed" } }); expect(decliner).toHaveBeenCalledTimes(1); expect(claimer).toHaveBeenCalledTimes(1); }); it("returns undefined when all plugins decline", async () => { const first = vi.fn().mockResolvedValue(undefined); const second = vi.fn().mockResolvedValue(undefined); const registry = createMockPluginRegistry([ { hookName: "before_agent_reply", handler: first }, { hookName: "before_agent_reply", handler: second }, ]); const runner = createHookRunner(registry); const result = await runner.runBeforeAgentReply(EVENT, TEST_PLUGIN_AGENT_CTX); expect(result).toBeUndefined(); }); it("catches errors with catchErrors: true and continues to next handler", async () => { const logger = { warn: vi.fn(), error: vi.fn() }; const failing = vi.fn().mockRejectedValue(new Error("boom")); const claimer = vi.fn().mockResolvedValue({ handled: true, reply: { text: "ok" } }); const registry = createMockPluginRegistry([ { hookName: "before_agent_reply", handler: failing }, { hookName: "before_agent_reply", handler: claimer }, ]); const runner = createHookRunner(registry, { logger }); const result = await runner.runBeforeAgentReply(EVENT, TEST_PLUGIN_AGENT_CTX); expect(result).toEqual({ handled: true, reply: { text: "ok" } }); expect(logger.error).toHaveBeenCalledWith( expect.stringContaining("before_agent_reply handler from test-plugin failed: Error: boom"), ); }); it("hasHooks reports correctly for before_agent_reply", () => { const registry = createMockPluginRegistry([ { hookName: "before_agent_reply", handler: vi.fn() }, ]); const runner = createHookRunner(registry); expect(runner.hasHooks("before_agent_reply")).toBe(true); expect(runner.hasHooks("before_agent_start")).toBe(false); }); });