import { expect, test, vi } from "bun:test"; import type { CustomTool, CustomToolContext } from "../src/extensibility/custom-tools/types"; import { MCPManager } from "../src/mcp/manager"; import type { MCPRequestOptions, MCPServerConnection, MCPTransport } from "../src/mcp/types"; import { createMCPProxyTools } from "../src/task/executor"; import { ToolAbortError } from "../src/tools/tool-errors"; function createFakeConnection() { let capturedSignal: AbortSignal | undefined; const { promise: requestPromise, reject } = Promise.withResolvers(); let isRequestCalled = false; const transport: MCPTransport = { async request(_method: string, _params?: Record, options?: MCPRequestOptions) { isRequestCalled = true; capturedSignal = options?.signal; if (capturedSignal?.aborted) { reject(new Error("aborted")); return requestPromise; } capturedSignal?.addEventListener("abort", () => { reject(new Error("aborted")); }); return requestPromise; }, async notify() {}, async close() {}, connected: true, }; const connection: MCPServerConnection = { name: "test-server", config: { command: "test", args: [] }, transport, serverInfo: { name: "test", version: "1" }, capabilities: {}, }; return { connection, getCapturedSignal: () => capturedSignal, requestPromise, rejectRequest: reject, requestCalled: () => isRequestCalled, }; } test("MCP proxy tool aborts underlying operation on caller abort", async () => { const fake = createFakeConnection(); const manager = new MCPManager(process.cwd()); const toolsData: CustomTool[] = [ { name: "test_tool", label: "Test Tool", description: "A test tool", strict: false, mcpToolName: "test_tool", mcpServerName: "test-server", parameters: { type: "object", properties: {} }, execute: async () => ({ content: [] }), } as CustomTool, ]; vi.spyOn(manager, "getTools").mockReturnValue(toolsData); vi.spyOn(manager, "waitForConnection").mockResolvedValue(fake.connection); const tools = createMCPProxyTools(manager); const proxyTool = tools[0]; if (!proxyTool?.execute) { expect.unreachable("Tool execute method missing"); return; } const ac = new AbortController(); const executePromise = proxyTool.execute("call_1", {}, () => {}, {} as CustomToolContext, ac.signal); // Let the promise reach transport.request await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); expect(fake.requestCalled()).toBe(true); const capturedSignal = fake.getCapturedSignal(); expect(capturedSignal).toBeDefined(); if (!capturedSignal) return; expect(capturedSignal.aborted).toBe(false); ac.abort(); try { await executePromise; expect.unreachable("executePromise should throw ToolAbortError"); } catch (e: unknown) { expect(e instanceof ToolAbortError).toBe(true); } expect(capturedSignal.aborted).toBe(true); }); test("MCP proxy tool aborts underlying operation on timeout", async () => { vi.useFakeTimers(); try { const fake = createFakeConnection(); const manager = new MCPManager(process.cwd()); const toolsData: CustomTool[] = [ { name: "test_tool", label: "Test Tool", description: "A test tool", strict: false, mcpToolName: "test_tool", mcpServerName: "test-server", parameters: { type: "object", properties: {} }, execute: async () => ({ content: [] }), } as CustomTool, ]; vi.spyOn(manager, "getTools").mockReturnValue(toolsData); vi.spyOn(manager, "waitForConnection").mockResolvedValue(fake.connection); const tools = createMCPProxyTools(manager); const proxyTool = tools[0]; if (!proxyTool?.execute) { expect.unreachable("Tool execute method missing"); return; } const executePromise = proxyTool.execute("call_1", {}, () => {}, {} as CustomToolContext, undefined); // Let the promise reach transport.request await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); expect(fake.requestCalled()).toBe(true); const capturedSignal = fake.getCapturedSignal(); expect(capturedSignal).toBeDefined(); if (!capturedSignal) return; expect(capturedSignal.aborted).toBe(false); // MCP_CALL_TIMEOUT_MS is 60_000 vi.advanceTimersByTime(65_000); // On timeout, the tool returns an error content array rather than throwing ToolAbortError const result = await executePromise; expect(result.content[0]?.type).toBe("text"); if (result.content[0]?.type !== "text") { expect.unreachable("Expected text content block"); return; } expect(result.content[0].text).toContain("MCP error: MCP tool call timed out"); expect(capturedSignal.aborted).toBe(true); } finally { vi.useRealTimers(); } });