import type { RequestPermissionRequest } from "@agentclientprotocol/sdk"; import { describe, expect, it, vi } from "vitest"; import { resolvePermissionRequest } from "./client.js"; import { extractAttachmentsFromPrompt, extractTextFromPrompt } from "./event-mapper.js"; function makePermissionRequest( overrides: Partial = {}, ): RequestPermissionRequest { const { toolCall: toolCallOverride, options: optionsOverride, ...restOverrides } = overrides; const base: RequestPermissionRequest = { sessionId: "session-1", toolCall: { toolCallId: "tool-1", title: "read: src/index.ts", status: "pending", }, options: [ { kind: "allow_once", name: "Allow once", optionId: "allow" }, { kind: "reject_once", name: "Reject once", optionId: "reject" }, ], }; return { ...base, ...restOverrides, toolCall: toolCallOverride ? { ...base.toolCall, ...toolCallOverride } : base.toolCall, options: optionsOverride ?? base.options, }; } describe("resolvePermissionRequest", () => { it("auto-approves safe tools without prompting", async () => { const prompt = vi.fn(async () => true); const res = await resolvePermissionRequest(makePermissionRequest(), { prompt, log: () => {} }); expect(res).toEqual({ outcome: { outcome: "selected", optionId: "allow" } }); expect(prompt).not.toHaveBeenCalled(); }); it("prompts for dangerous tool names inferred from title", async () => { const prompt = vi.fn(async () => true); const res = await resolvePermissionRequest( makePermissionRequest({ toolCall: { toolCallId: "tool-2", title: "exec: uname -a", status: "pending" }, }), { prompt, log: () => {} }, ); expect(prompt).toHaveBeenCalledTimes(1); expect(prompt).toHaveBeenCalledWith("exec", "exec: uname -a"); expect(res).toEqual({ outcome: { outcome: "selected", optionId: "allow" } }); }); it("prompts for non-read/search tools (write)", async () => { const prompt = vi.fn(async () => true); const res = await resolvePermissionRequest( makePermissionRequest({ toolCall: { toolCallId: "tool-w", title: "write: /tmp/pwn", status: "pending" }, }), { prompt, log: () => {} }, ); expect(prompt).toHaveBeenCalledTimes(1); expect(prompt).toHaveBeenCalledWith("write", "write: /tmp/pwn"); expect(res).toEqual({ outcome: { outcome: "selected", optionId: "allow" } }); }); it("auto-approves search without prompting", async () => { const prompt = vi.fn(async () => true); const res = await resolvePermissionRequest( makePermissionRequest({ toolCall: { toolCallId: "tool-s", title: "search: foo", status: "pending" }, }), { prompt, log: () => {} }, ); expect(res).toEqual({ outcome: { outcome: "selected", optionId: "allow" } }); expect(prompt).not.toHaveBeenCalled(); }); it("prompts for fetch even when tool name is known", async () => { const prompt = vi.fn(async () => false); const res = await resolvePermissionRequest( makePermissionRequest({ toolCall: { toolCallId: "tool-f", title: "fetch: https://example.com", status: "pending" }, }), { prompt, log: () => {} }, ); expect(prompt).toHaveBeenCalledTimes(1); expect(res).toEqual({ outcome: { outcome: "selected", optionId: "reject" } }); }); it("prompts when tool name contains read/search substrings but isn't a safe kind", async () => { const prompt = vi.fn(async () => false); const res = await resolvePermissionRequest( makePermissionRequest({ toolCall: { toolCallId: "tool-t", title: "thread: reply", status: "pending" }, }), { prompt, log: () => {} }, ); expect(prompt).toHaveBeenCalledTimes(1); expect(res).toEqual({ outcome: { outcome: "selected", optionId: "reject" } }); }); it("uses allow_always and reject_always when once options are absent", async () => { const options: RequestPermissionRequest["options"] = [ { kind: "allow_always", name: "Always allow", optionId: "allow-always" }, { kind: "reject_always", name: "Always reject", optionId: "reject-always" }, ]; const prompt = vi.fn(async () => false); const res = await resolvePermissionRequest( makePermissionRequest({ toolCall: { toolCallId: "tool-3", title: "gateway: reload", status: "pending" }, options, }), { prompt, log: () => {} }, ); expect(res).toEqual({ outcome: { outcome: "selected", optionId: "reject-always" } }); }); it("prompts when tool identity is unknown and can still approve", async () => { const prompt = vi.fn(async () => true); const res = await resolvePermissionRequest( makePermissionRequest({ toolCall: { toolCallId: "tool-4", title: "Modifying critical configuration file", status: "pending", }, }), { prompt, log: () => {} }, ); expect(prompt).toHaveBeenCalledWith(undefined, "Modifying critical configuration file"); expect(res).toEqual({ outcome: { outcome: "selected", optionId: "allow" } }); }); it("returns cancelled when no permission options are present", async () => { const prompt = vi.fn(async () => true); const res = await resolvePermissionRequest(makePermissionRequest({ options: [] }), { prompt, log: () => {}, }); expect(prompt).not.toHaveBeenCalled(); expect(res).toEqual({ outcome: { outcome: "cancelled" } }); }); }); describe("acp event mapper", () => { it("extracts text and resource blocks into prompt text", () => { const text = extractTextFromPrompt([ { type: "text", text: "Hello" }, { type: "resource", resource: { text: "File contents" } }, { type: "resource_link", uri: "https://example.com", title: "Spec" }, { type: "image", data: "abc", mimeType: "image/png" }, ]); expect(text).toBe("Hello\nFile contents\n[Resource link (Spec)] https://example.com"); }); it("extracts image blocks into gateway attachments", () => { const attachments = extractAttachmentsFromPrompt([ { type: "image", data: "abc", mimeType: "image/png" }, { type: "image", data: "", mimeType: "image/png" }, { type: "text", text: "ignored" }, ]); expect(attachments).toEqual([ { type: "image", mimeType: "image/png", content: "abc", }, ]); }); });