diff --git a/src/acp.test.ts b/src/acp.test.ts index 1bf4def..43eaca1 100644 --- a/src/acp.test.ts +++ b/src/acp.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect, beforeEach } from "vitest"; +import { describe, it, expect, beforeEach, vi } from "vitest"; import { Agent, ClientSideConnection, @@ -24,6 +24,8 @@ import { SessionNotification, PROTOCOL_VERSION, ndJsonStream, + RequestError, + ErrorCode, } from "./acp.js"; describe("Connection", () => { @@ -1105,4 +1107,52 @@ describe("Connection", () => { }); expect(loadResponse).toEqual({}); }); + + it("logs RESOURCE_NOT_FOUND at debug level", async () => { + const debugSpy = vi.spyOn(console, "debug").mockImplementation(() => {}); + const errorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + + const client: Client = { + readTextFile: () => Promise.reject(RequestError.resourceNotFound()), + writeTextFile: () => Promise.resolve({}), + requestPermission: () => + Promise.resolve({ outcome: { outcome: "selected", optionId: "allow" } }), + sessionUpdate: () => Promise.resolve(), + }; + + const agent: Agent = { + initialize: () => + Promise.resolve({ + protocolVersion: PROTOCOL_VERSION, + agentCapabilities: { loadSession: false }, + }), + newSession: () => Promise.resolve({ sessionId: "s1" }), + authenticate: () => Promise.resolve(), + prompt: () => Promise.resolve({ stopReason: "end_turn" }), + cancel: () => Promise.resolve(), + }; + + new ClientSideConnection( + () => client, + ndJsonStream(clientToAgent.writable, agentToClient.readable), + ); + const conn = new AgentSideConnection( + () => agent, + ndJsonStream(agentToClient.writable, clientToAgent.readable), + ); + + await expect( + conn.readTextFile({ path: "/test.txt", sessionId: "s1" }), + ).rejects.toThrow(); + + expect(debugSpy).toHaveBeenCalledWith( + "Error handling request", + expect.anything(), + expect.objectContaining({ code: ErrorCode.RESOURCE_NOT_FOUND }), + ); + expect(errorSpy).not.toHaveBeenCalled(); + + debugSpy.mockRestore(); + errorSpy.mockRestore(); + }); }); diff --git a/src/acp.ts b/src/acp.ts index 7abacb9..aacb0a0 100644 --- a/src/acp.ts +++ b/src/acp.ts @@ -882,6 +882,16 @@ class Connection { } } + #logError(context: string, message: AnyMessage, error: ErrorResponse) { + // RESOURCE_NOT_FOUND is expected when checking file existence + // before write, since ACP has no stat API + if (error.code === ErrorCode.RESOURCE_NOT_FOUND) { + console.debug(context, message, error); + } else { + console.error(context, message, error); + } + } + async #processMessage(message: AnyMessage) { if ("method" in message && "id" in message) { // It's a request @@ -890,7 +900,7 @@ class Connection { message.params, ); if ("error" in response) { - console.error("Error handling request", message, response.error); + this.#logError("Error handling request", message, response.error); } await this.#sendMessage({ @@ -905,7 +915,7 @@ class Connection { message.params, ); if ("error" in response) { - console.error("Error handling notification", message, response.error); + this.#logError("Error handling notification", message, response.error); } } else if ("id" in message) { // It's a response @@ -1038,6 +1048,19 @@ class Connection { } } +/** + * JSON-RPC error codes used by the ACP protocol. + */ +export const ErrorCode = { + PARSE_ERROR: -32700, + INVALID_REQUEST: -32600, + METHOD_NOT_FOUND: -32601, + INVALID_PARAMS: -32602, + INTERNAL_ERROR: -32603, + AUTH_REQUIRED: -32000, + RESOURCE_NOT_FOUND: -32002, +} as const; + /** * JSON-RPC error object. *