import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import * as fs from "node:fs/promises"; import * as os from "node:os"; import * as path from "node:path"; import { create } from "@bufbuild/protobuf"; import type { AgentEvent, AgentTool, AgentToolContext } from "@oh-my-pi/pi-agent-core"; import { ReadArgsSchema, ShellArgsSchema } from "@oh-my-pi/pi-catalog/discovery/cursor-gen/agent_pb"; import { Settings } from "@oh-my-pi/pi-coding-agent/config/settings"; import { CursorExecHandlers } from "@oh-my-pi/pi-coding-agent/cursor"; import type { ExtensionRunner } from "@oh-my-pi/pi-coding-agent/extensibility/extensions"; import { ExtensionToolWrapper } from "@oh-my-pi/pi-coding-agent/extensibility/extensions"; import { GrepTool, type ToolSession } from "@oh-my-pi/pi-coding-agent/tools"; import { removeWithRetries } from "@oh-my-pi/pi-utils"; import { type } from "arktype"; function createTestSession(cwd: string, overrides: Partial = {}): ToolSession { return { cwd, hasUI: false, getSessionFile: () => null, getSessionSpawns: () => "*", settings: Settings.isolated(), ...overrides, }; } describe("CursorExecHandlers.grep bridge", () => { let cwd: string; let searchTool: GrepTool; let handlers: CursorExecHandlers; beforeEach(async () => { cwd = await fs.mkdtemp(path.join(os.tmpdir(), "cursor-exec-test-")); await Bun.write(path.join(cwd, "sample.txt"), "Hello World\nhello world\n"); searchTool = new GrepTool(createTestSession(cwd)); handlers = new CursorExecHandlers({ cwd, tools: new Map([["grep", searchTool as any]]), }); }); afterEach(async () => { await removeWithRetries(cwd); }); it("maps caseInsensitive parameter correctly through the grep bridge", async () => { // 1. By default/omitted caseInsensitive, should be case-sensitive (match count 1 for "hello") const defaultResult = await handlers.grep({ toolCallId: "call-1", path: cwd, pattern: "hello", } as any); expect((defaultResult.details as { matchCount?: number } | undefined)?.matchCount).toBe(1); // 2. If caseInsensitive: true, should be case-insensitive (match count 2 for "hello") const insensitiveResult = await handlers.grep({ toolCallId: "call-2", path: cwd, pattern: "hello", caseInsensitive: true, } as any); expect((insensitiveResult.details as { matchCount?: number } | undefined)?.matchCount).toBe(2); // 3. If caseInsensitive: false, should be case-sensitive (match count 1 for "hello") const sensitiveResult = await handlers.grep({ toolCallId: "call-3", path: cwd, pattern: "hello", caseInsensitive: false, } as any); expect((sensitiveResult.details as { matchCount?: number } | undefined)?.matchCount).toBe(1); }); }); describe("CursorExecHandlers error results", () => { const rewrittenErrorTool = (name: string): AgentTool => ({ name, label: name, description: "returns a rewritten tool failure", parameters: type({}), execute: async () => ({ content: [{ type: "text", text: "Enriched recovery guidance" }], details: { enriched: true }, isError: true, }), }); it("propagates returned isError through the standard exec bridge", async () => { const events: AgentEvent[] = []; const handlers = new CursorExecHandlers({ cwd: ".", tools: new Map([["read", rewrittenErrorTool("read")]]), emitEvent: event => events.push(event), }); const result = await handlers.read(create(ReadArgsSchema, { toolCallId: "call-read", path: "ignored" })); expect(result.isError).toBe(true); expect(result.content).toEqual([{ type: "text", text: "Enriched recovery guidance" }]); const end = events.find(event => event.type === "tool_execution_end"); expect(end?.isError).toBe(true); }); it("propagates returned isError through the shell stream bridge", async () => { const events: AgentEvent[] = []; const stdout: string[] = []; const handlers = new CursorExecHandlers({ cwd: ".", tools: new Map([["bash", rewrittenErrorTool("bash")]]), emitEvent: event => events.push(event), }); const result = await handlers.shellStream( create(ShellArgsSchema, { toolCallId: "call-shell", command: "ignored" }), { onStdout: data => stdout.push(data), onStderr: () => {}, }, ); expect(result.isError).toBe(true); expect(result.content).toEqual([{ type: "text", text: "Enriched recovery guidance" }]); expect(stdout).toEqual(["Enriched recovery guidance"]); const end = events.find(event => event.type === "tool_execution_end"); expect(end?.isError).toBe(true); }); }); describe("CursorExecHandlers mounted tool bridge", () => { it("executes MCP tools resolved from the xd:// registry", async () => { const mountedTool: AgentTool = { name: "mcp__fixture_report", label: "Fixture Report", description: "reports a fixture result", parameters: type({}), async execute() { return { content: [{ type: "text", text: "reported" }], details: {} }; }, }; const handlers = new CursorExecHandlers({ cwd: ".", tools: new Map(), getTool: name => (name === mountedTool.name ? mountedTool : undefined), }); const result = await handlers.mcp({ name: mountedTool.name, providerIdentifier: "pi-agent", toolName: mountedTool.name, toolCallId: "call-mounted", args: {}, rawArgs: {}, }); expect(result.isError).toBe(false); expect(result.content).toEqual([{ type: "text", text: "reported" }]); }); it("routes wrapped mounted devices through the approval gate", async () => { let executed = false; const device: AgentTool = { name: "ast_edit", label: "AST Edit", description: "structural edit device", parameters: type({}), async execute() { executed = true; return { content: [{ type: "text", text: "edited" }], details: {} }; }, }; // The deny path throws inside resolveApproval before the runner is touched, // so a bare runner stub suffices to prove the gate runs. const wrapped = new ExtensionToolWrapper(device, {} as unknown as ExtensionRunner); const settings = Settings.isolated({ "tools.approval": { ast_edit: "deny" } }); const handlers = new CursorExecHandlers({ cwd: ".", tools: new Map(), getTool: name => (name === device.name ? (wrapped as unknown as AgentTool) : undefined), getToolContext: () => ({ settings }) as AgentToolContext, }); const result = await handlers.mcp({ name: device.name, providerIdentifier: "pi-agent", toolName: device.name, toolCallId: "call-denied", args: {}, rawArgs: {}, }); expect(result.isError).toBe(true); expect(executed).toBe(false); expect(result.content.find(block => block.type === "text")?.text).toContain("blocked by user policy"); }); });