From 89bfe0c94451dc173a59036e7c0f5776a3dec8d3 Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Thu, 12 Feb 2026 19:39:23 -0600 Subject: [PATCH] fix: add adapter-path after_tool_call coverage (follow-up to #15012) (#15105) --- ...definition-adapter.after-tool-call.test.ts | 113 ++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 src/agents/pi-tool-definition-adapter.after-tool-call.test.ts diff --git a/src/agents/pi-tool-definition-adapter.after-tool-call.test.ts b/src/agents/pi-tool-definition-adapter.after-tool-call.test.ts new file mode 100644 index 00000000000..7e7c74a35eb --- /dev/null +++ b/src/agents/pi-tool-definition-adapter.after-tool-call.test.ts @@ -0,0 +1,113 @@ +import type { AgentTool } from "@mariozechner/pi-agent-core"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { toToolDefinitions } from "./pi-tool-definition-adapter.js"; + +const hookMocks = vi.hoisted(() => ({ + runner: { + hasHooks: vi.fn(() => false), + runAfterToolCall: vi.fn(async () => {}), + }, + runBeforeToolCallHook: vi.fn(async ({ params }: { params: unknown }) => ({ + blocked: false, + params, + })), +})); + +vi.mock("../plugins/hook-runner-global.js", () => ({ + getGlobalHookRunner: () => hookMocks.runner, +})); + +vi.mock("./pi-tools.before-tool-call.js", () => ({ + runBeforeToolCallHook: hookMocks.runBeforeToolCallHook, +})); + +describe("pi tool definition adapter after_tool_call", () => { + beforeEach(() => { + hookMocks.runner.hasHooks.mockReset(); + hookMocks.runner.runAfterToolCall.mockReset(); + hookMocks.runner.runAfterToolCall.mockResolvedValue(undefined); + hookMocks.runBeforeToolCallHook.mockReset(); + hookMocks.runBeforeToolCallHook.mockImplementation(async ({ params }) => ({ + blocked: false, + params, + })); + }); + + it("dispatches after_tool_call once on successful adapter execution", async () => { + hookMocks.runner.hasHooks.mockImplementation((name: string) => name === "after_tool_call"); + hookMocks.runBeforeToolCallHook.mockResolvedValue({ + blocked: false, + params: { mode: "safe" }, + }); + const tool = { + name: "read", + label: "Read", + description: "reads", + parameters: {}, + execute: vi.fn(async () => ({ content: [], details: { ok: true } })), + } satisfies AgentTool; + + const defs = toToolDefinitions([tool]); + const result = await defs[0].execute("call-ok", { path: "/tmp/file" }, undefined, undefined); + + expect(result.details).toMatchObject({ ok: true }); + expect(hookMocks.runner.runAfterToolCall).toHaveBeenCalledTimes(1); + expect(hookMocks.runner.runAfterToolCall).toHaveBeenCalledWith( + { + toolName: "read", + params: { mode: "safe" }, + result, + }, + { toolName: "read" }, + ); + }); + + it("dispatches after_tool_call once on adapter error with normalized tool name", async () => { + hookMocks.runner.hasHooks.mockImplementation((name: string) => name === "after_tool_call"); + const tool = { + name: "bash", + label: "Bash", + description: "throws", + parameters: {}, + execute: vi.fn(async () => { + throw new Error("boom"); + }), + } satisfies AgentTool; + + const defs = toToolDefinitions([tool]); + const result = await defs[0].execute("call-err", { cmd: "ls" }, undefined, undefined); + + expect(result.details).toMatchObject({ + status: "error", + tool: "exec", + error: "boom", + }); + expect(hookMocks.runner.runAfterToolCall).toHaveBeenCalledTimes(1); + expect(hookMocks.runner.runAfterToolCall).toHaveBeenCalledWith( + { + toolName: "exec", + params: { cmd: "ls" }, + error: "boom", + }, + { toolName: "exec" }, + ); + }); + + it("does not break execution when after_tool_call hook throws", async () => { + hookMocks.runner.hasHooks.mockImplementation((name: string) => name === "after_tool_call"); + hookMocks.runner.runAfterToolCall.mockRejectedValue(new Error("hook failed")); + const tool = { + name: "read", + label: "Read", + description: "reads", + parameters: {}, + execute: vi.fn(async () => ({ content: [], details: { ok: true } })), + } satisfies AgentTool; + + const defs = toToolDefinitions([tool]); + const result = await defs[0].execute("call-ok2", { path: "/tmp/file" }, undefined, undefined); + + expect(result.details).toMatchObject({ ok: true }); + expect(hookMocks.runner.runAfterToolCall).toHaveBeenCalledTimes(1); + }); +});