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); }), );});
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);
}),
);
});