import { beforeAll, describe, expect, it, vi } from "bun:test"; import type { AgentToolContext } from "@oh-my-pi/pi-agent-core"; import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import { initTheme } from "@oh-my-pi/pi-coding-agent/modes/theme/theme"; import type { ToolSession } from "@oh-my-pi/pi-coding-agent/tools"; import { AskTool } from "@oh-my-pi/pi-coding-agent/tools/ask"; import { ToolAbortError } from "@oh-my-pi/pi-coding-agent/tools/tool-errors"; function createSession(overrides: Partial = {}): ToolSession { return { cwd: "/tmp/test", hasUI: true, getSessionFile: () => null, getSessionSpawns: () => "*", settings: Settings.isolated(), ...overrides, }; } function createContext(args: { select: ( prompt: string, options: string[], dialogOptions?: { initialIndex?: number; timeout?: number }, ) => Promise; input?: (prompt: string) => Promise; abort?: () => void; }): AgentToolContext { // AgentToolContext includes many runtime fields; tests only need UI + abort behavior. return { hasUI: true, ui: { select: args.select, input: args.input ?? (async () => undefined), }, abort: args.abort ?? (() => {}), } as unknown as AgentToolContext; } beforeAll(async () => { await initTheme(false); }); describe("AskTool cancellation", () => { it("aborts the turn when the user cancels selection", async () => { const tool = new AskTool(createSession()); const abort = vi.fn(); const context = createContext({ select: async () => undefined, abort, }); expect( tool.execute( "call-1", { questions: [ { id: "confirm", question: "Proceed?", options: [{ label: "yes" }, { label: "no" }], }, ], }, undefined, undefined, context, ), ).rejects.toBeInstanceOf(ToolAbortError); expect(abort).toHaveBeenCalledTimes(1); }); it("does not abort the turn when cancellation is from ask timeout", async () => { const tool = new AskTool( createSession({ settings: Settings.isolated({ "ask.timeout": 0.001 }), }), ); const abort = vi.fn(); const context = createContext({ select: async () => { await Bun.sleep(5); return undefined; }, abort, }); const result = await tool.execute( "call-2", { questions: [ { id: "confirm", question: "Proceed?", options: [{ label: "yes" }, { label: "no" }], }, ], }, undefined, undefined, context, ); expect(result.content[0]?.type).toBe("text"); if (result.content[0]?.type !== "text") { throw new Error("Expected text result"); } expect(result.content[0].text).toContain("User cancelled the selection"); expect(abort).not.toHaveBeenCalled(); }); it("aborts multi-question ask when any question is explicitly cancelled", async () => { const tool = new AskTool(createSession()); const abort = vi.fn(); const context = createContext({ select: async prompt => { if (prompt.includes("First")) return "one"; return undefined; }, abort, }); expect( tool.execute( "call-3", { questions: [ { id: "first", question: "First", options: [{ label: "one" }, { label: "two" }], }, { id: "second", question: "Second", options: [{ label: "alpha" }, { label: "beta" }], }, ], }, undefined, undefined, context, ), ).rejects.toBeInstanceOf(ToolAbortError); expect(abort).toHaveBeenCalledTimes(1); }); });