Skip to content
Source
Sign inSign up

errors.test.ts

155 lines5.7 KB

1import { describe, expect, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import * as Schema from "effect/Schema";
4import * as RpcClientError from "effect/rpc/RpcClientError";
5​
6import { callRpc } from "./_internal/shared.ts";
7import * as AcpError from "./errors.ts";
8​
9const decodeNestedNumberPayload = Schema.decodeUnknownEffect(
10 Schema.Struct({ profile: Schema.Struct({ token: Schema.Number }) }),
11);
12const encodeUnknownJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown));
13​
14describe("effect-acp errors", () => {
15 it.effect("retains RPC method and cause without deriving the message from the cause", () => {
16 const rootCause = new Error("connection details that must not become the public message");
17 const failure = new RpcClientError.RpcClientError({
18 reason: new RpcClientError.RpcClientDefect({
19 message: rootCause.message,
20 cause: rootCause,
21 }),
22 });
23​
24 return Effect.gen(function* () {
25 const error = yield* callRpc("session/new", Effect.fail(failure)).pipe(Effect.flip);
26​
27 expect(error).toMatchObject({
28 _tag: "AcpTransportError",
29 operation: "call-rpc",
30 method: "session/new",
31 cause: failure,
32 });
33 expect(error.message).toBe("ACP transport operation call-rpc failed for method session/new.");
34 expect(error.message).not.toContain(rootCause.message);
35 });
36 });
37​
38 it.effect("preserves typed ACP failures carried by the RPC protocol", () => {
39 const original = new AcpError.AcpTransportError({
40 detail: "Sign in to the agent.",
41 cause: undefined,
42 });
43 const failure = new RpcClientError.RpcClientError({
44 reason: new RpcClientError.RpcClientDefect({
45 message: "ACP protocol terminated.",
46 cause: original,
47 }),
48 });
49​
50 return Effect.gen(function* () {
51 const error = yield* callRpc("initialize", Effect.fail(failure)).pipe(Effect.flip);
52 expect(error).toBe(original);
53 });
54 });
55​
56 it("includes the authored detail but not the cause in the transport message", () => {
57 const cause = new Error("connection refused at a private endpoint");
58 const error = new AcpError.AcpTransportError({
59 detail: "ACP replay did not consume all frames",
60 cause,
61 });
62​
63 expect(error.message).toBe(
64 "ACP transport operation failed: ACP replay did not consume all frames.",
65 );
66 expect(error.cause).toBe(cause);
67 });
68​
69 it("preserves structured extension handler failures behind stable request errors", () => {
70 const cause = new AcpError.AcpTransportError({
71 operation: "read-input-stream",
72 cause: new Error("private transport diagnostics"),
73 });
74 const error = AcpError.AcpRequestError.fromExtensionHandlerError(cause, "x/test");
75​
76 expect(error).toMatchObject({
77 code: -32603,
78 method: "x/test",
79 operation: "handle-extension-request",
80 cause,
81 });
82 expect(error.message).toBe("ACP extension request handler failed for method 'x/test'");
83 expect(error.message).not.toContain(cause.message);
84 });
85​
86 it.effect("keeps invalid extension payload values only in the exact schema cause", () =>
87 Effect.gen(function* () {
88 const secret = "acp-schema-payload-secret";
89 const cause = yield* decodeNestedNumberPayload({ profile: { token: secret } }).pipe(
90 Effect.flip,
91 );
92 const error = AcpError.AcpRequestError.invalidExtensionPayload("x/private", cause);
93 const { cause: directCause, ...directDiagnostics } = error;
94​
95 expect(directCause).toBe(cause);
96 expect(error).toMatchObject({
97 method: "x/private",
98 operation: "decode-extension-request-payload",
99 maximumPathDepth: 2,
100 });
101 expect(error.issueCount).toBeGreaterThan(0);
102 expect(error.issueKinds).toContain("Pointer");
103 expect(error.message).toBe("Invalid payload for ACP extension method 'x/private'.");
104 expect(error.message).not.toContain(secret);
105 expect(encodeUnknownJson(directDiagnostics)).not.toContain(secret);
106 expect(encodeUnknownJson(error.toProtocolError())).not.toContain(secret);
107​
108 const protocolError = AcpError.AcpProtocolParseError.fromSchemaError(
109 "decode-notification-payload",
110 "x/private",
111 cause,
112 );
113 const { cause: protocolCause, ...protocolDiagnostics } = protocolError;
114 expect(protocolCause).toBe(cause);
115 expect(protocolError).toMatchObject({
116 method: "x/private",
117 operation: "decode-notification-payload",
118 maximumPathDepth: 2,
119 });
120 expect(protocolError.message).not.toContain(secret);
121 expect(encodeUnknownJson(protocolDiagnostics)).not.toContain(secret);
122 expect("detail" in protocolError).toBe(false);
123 }),
124 );
125​
126 it.effect("summarizes schema issues by path without payload values", () =>
127 Effect.gen(function* () {
128 const secret = "acp-summary-secret";
129 const decodeStruct = Schema.decodeUnknownEffect(
130 Schema.Struct({
131 update: Schema.Struct({ used: Schema.Number, size: Schema.Number }),
132 env: Schema.Record(Schema.String, Schema.Number),
133 }),
134 );
135​
136 const cause = yield* decodeStruct(
137 { update: { size: secret }, env: { [`${secret} key`]: secret } },
138 { errors: "all" },
139 ).pipe(Effect.flip);
140 const error = AcpError.AcpProtocolParseError.fromSchemaError(
141 "decode-notification-payload",
142 "session/update",
143 cause,
144 );
145​
146 expect(error.issueSummary).toBe(
147 "update.used missing, update.size wrong type, env.* wrong type",
148 );
149 expect(error.message).toBe(
150 "ACP protocol operation 'decode-notification-payload' failed for method 'session/update': update.used missing, update.size wrong type, env.* wrong type.",
151 );
152 expect(error.message).not.toContain(secret);
153 }),
154 );
155});