import { describe, expect, it } from "@effect/vitest"; import * as Effect from "effect/Effect"; import * as Schema from "effect/Schema"; import * as RpcClientError from "effect/unstable/rpc/RpcClientError"; import * as AcpSchema from "./_generated/schema.gen.ts"; import { callRpc, runHandler } from "./_internal/shared.ts"; import * as AcpError from "./errors.ts"; const decodeNestedNumberPayload = Schema.decodeUnknownEffect( Schema.Struct({ profile: Schema.Struct({ token: Schema.Number }) }), ); const encodeUnknownJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); describe("effect-acp errors", () => { it.effect("retains RPC method and cause without deriving the message from the cause", () => { const rootCause = new Error("connection details that must not become the public message"); const failure = new RpcClientError.RpcClientError({ reason: new RpcClientError.RpcClientDefect({ message: rootCause.message, cause: rootCause, }), }); return Effect.gen(function* () { const error = yield* callRpc("session/new", Effect.fail(failure)).pipe(Effect.flip); expect(error).toMatchObject({ _tag: "AcpTransportError", operation: "call-rpc", method: "session/new", cause: failure, }); expect(error.message).toBe("ACP transport operation call-rpc failed for method session/new."); expect(error.message).not.toContain(rootCause.message); }); }); it.effect("preserves typed ACP failures carried by the RPC protocol", () => { const original = new AcpError.AcpTransportError({ detail: "Sign in to the agent.", cause: undefined, }); const failure = new RpcClientError.RpcClientError({ reason: new RpcClientError.RpcClientDefect({ message: "ACP protocol terminated.", cause: original, }), }); return Effect.gen(function* () { const error = yield* callRpc("initialize", Effect.fail(failure)).pipe(Effect.flip); expect(error).toBe(original); }); }); it.effect("preserves protocol request errors as request errors", () => { const failure = AcpSchema.Error.make({ code: -32602, message: "Invalid params", data: { field: "sessionId" }, }); return Effect.gen(function* () { const error = yield* callRpc("session/load", Effect.fail(failure)).pipe(Effect.flip); expect(error).toMatchObject({ _tag: "AcpRequestError", code: -32602, errorMessage: "Invalid params", data: { field: "sessionId" }, method: "session/load", operation: "receive-response", }); }); }); it("does not expose legacy diagnostic detail as the transport message", () => { const cause = new Error("connection refused at a private endpoint"); const error = new AcpError.AcpTransportError({ detail: cause.message, cause, }); expect(error.message).toBe("ACP transport operation failed."); expect(error.cause).toBe(cause); }); it("preserves structured extension handler failures behind stable request errors", () => { const cause = new AcpError.AcpTransportError({ operation: "read-input-stream", cause: new Error("private transport diagnostics"), }); const error = AcpError.AcpRequestError.fromExtensionHandlerError(cause, "x/test"); expect(error).toMatchObject({ code: -32603, method: "x/test", operation: "handle-extension-request", cause, }); expect(error.message).toBe("ACP extension request handler failed for method 'x/test'"); expect(error.message).not.toContain(cause.message); }); it.effect("uses the structured mapper for core handler failures", () => { const cause = new AcpError.AcpTransportError({ operation: "read-input-stream", cause: new Error("private transport diagnostics"), }); return Effect.gen(function* () { const error = yield* runHandler(() => Effect.fail(cause), {}, "fs/read_text_file").pipe( Effect.flip, ); expect(error).toMatchObject({ code: -32603, message: "ACP request handler failed for method 'fs/read_text_file'", }); expect(error.message).not.toContain(cause.message); }); }); it.effect("keeps invalid extension payload values only in the exact schema cause", () => Effect.gen(function* () { const secret = "acp-schema-payload-secret"; const cause = yield* decodeNestedNumberPayload({ profile: { token: secret } }).pipe( Effect.flip, ); const error = AcpError.AcpRequestError.invalidExtensionPayload("x/private", cause); const { cause: directCause, ...directDiagnostics } = error; expect(directCause).toBe(cause); expect(error).toMatchObject({ method: "x/private", operation: "decode-extension-request-payload", maximumPathDepth: 2, }); expect(error.issueCount).toBeGreaterThan(0); expect(error.issueKinds).toContain("Pointer"); expect(error.message).toBe("Invalid payload for ACP extension method 'x/private'."); expect(error.message).not.toContain(secret); expect(encodeUnknownJson(directDiagnostics)).not.toContain(secret); expect(encodeUnknownJson(error.toProtocolError())).not.toContain(secret); const protocolError = AcpError.AcpProtocolParseError.fromSchemaError( "decode-notification-payload", "x/private", cause, ); const { cause: protocolCause, ...protocolDiagnostics } = protocolError; expect(protocolCause).toBe(cause); expect(protocolError).toMatchObject({ method: "x/private", operation: "decode-notification-payload", maximumPathDepth: 2, }); expect(protocolError.message).not.toContain(secret); expect(encodeUnknownJson(protocolDiagnostics)).not.toContain(secret); expect("detail" in protocolError).toBe(false); }), ); });