From 46c684fe7c7516cc2fdf8c81e88fb101daeb83f8 Mon Sep 17 00:00:00 2001 From: 3xq9 <226531072+3xq9@users.noreply.github.com> Date: Wed, 29 Jul 2026 00:07:32 +0200 Subject: [PATCH] test(ai): cover run_subagent tool run_subagent is the last untested tool on the AI tool surface. Lock that it maps the subagent result fields (summary, stepCount, durationMs) alongside the requested type and description on success, and that a failing subagent yields an error result carrying the type instead of throwing. --- src/modules/ai/tools/subagent.test.ts | 78 +++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 src/modules/ai/tools/subagent.test.ts diff --git a/src/modules/ai/tools/subagent.test.ts b/src/modules/ai/tools/subagent.test.ts new file mode 100644 index 000000000..a1a43d207 --- /dev/null +++ b/src/modules/ai/tools/subagent.test.ts @@ -0,0 +1,78 @@ +import type { ToolExecutionOptions } from "ai"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { ToolContext } from "./context"; + +const runSubagentMock = vi.hoisted(() => vi.fn()); + +vi.mock("../agents/runSubagent", () => ({ runSubagent: runSubagentMock })); +vi.mock("../store/chatStore", () => ({ + useChatStore: { + getState: () => ({ + apiKeys: {}, + selectedModelId: "model", + patchAgentMeta: vi.fn(), + }), + }, +})); + +import { buildSubagentTools } from "./subagent"; + +const toolOptions: ToolExecutionOptions = { + toolCallId: "tool-call", + messages: [], +}; + +function makeContext(): ToolContext { + return { + getCwd: () => "/workspace", + getWorkspaceRoot: () => "/workspace", + getTerminalContext: () => null, + isActiveTerminalPrivate: () => false, + injectIntoActivePty: () => false, + openPreview: () => false, + spawnAgent: () => null, + readAgentOutput: () => null, + readCache: new Map(), + getSessionId: () => "session", + } as unknown as ToolContext; +} + +// biome-ignore lint/suspicious/noExplicitAny: tool results are heterogeneous. +type Result = Record; + +async function run(input: Record): Promise { + const execute = buildSubagentTools(makeContext()).run_subagent.execute; + if (!execute) throw new Error("run_subagent has no execute"); + return (await execute(input as never, toolOptions)) as unknown as Result; +} + +beforeEach(() => vi.clearAllMocks()); + +describe("run_subagent", () => { + it("maps the subagent result fields on success", async () => { + runSubagentMock.mockResolvedValue({ + summary: "done", + stepCount: 4, + durationMs: 1200, + }); + const r = await run({ + type: "reviewer", + prompt: "review it", + description: "card", + }); + expect(r).toEqual({ + type: "reviewer", + description: "card", + summary: "done", + stepCount: 4, + durationMs: 1200, + }); + }); + + it("returns an error result instead of throwing when the subagent fails", async () => { + runSubagentMock.mockRejectedValue(new Error("boom")); + const r = await run({ type: "reviewer", prompt: "review it" }); + expect(r.type).toBe("reviewer"); + expect(r.error).toContain("boom"); + }); +});