1import { assert, it } from "@effect/vitest";
2import * as Effect from "effect/Effect";
3import * as Schema from "effect/Schema";
4
5import * as CodexError from "../errors.ts";
6import * as Shared from "./shared.ts";
7
8const decodeNestedNumberPayload = Schema.decodeUnknownEffect(
9 Schema.Struct({ profile: Schema.Struct({ token: Schema.Number }) }),
10);
11const encodeUnknownJson = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown));
12
13it.effect("preserves schema decode diagnostics without deriving the message from the cause", () =>
14 Effect.gen(function* () {
15 const error = yield* Shared.decodeOptionalPayload("thread/start", Schema.String, 42).pipe(
16 Effect.flip,
17 );
18
19 assert.instanceOf(error, CodexError.CodexAppServerRequestError);
20 assert.equal(error.code, -32602);
21 assert.equal(error.method, "thread/start");
22 assert.equal(error.operation, "decode-payload");
23 assert.equal(
24 error.message,
25 "Invalid payload for method 'thread/start' during 'decode-payload'",
26 );
27 assert.isTrue(Schema.isSchemaError(error.cause));
28
29 const protocolError = error.toProtocolError();
30 assert.equal(protocolError.code, -32602);
31 assert.equal(protocolError.message, error.message);
32 assert.property(protocolError, "data");
33 assert.notProperty(protocolError, "method");
34 assert.notProperty(protocolError, "operation");
35 assert.notProperty(protocolError, "cause");
36 }),
37);
38
39it.effect("preserves schema encode diagnostics", () =>
40 Effect.gen(function* () {
41 const error = yield* Shared.encodeOptionalPayload(
42 "thread/start",
43 Schema.Number,
44 "not-a-number" as never,
45 ).pipe(Effect.flip);
46
47 assert.equal(error.method, "thread/start");
48 assert.equal(error.operation, "encode-payload");
49 assert.equal(
50 error.message,
51 "Invalid payload for method 'thread/start' during 'encode-payload'",
52 );
53 assert.isTrue(Schema.isSchemaError(error.cause));
54 }),
55);
56
57it.effect("does not invent a cause when a method has no payload schema", () =>
58 Effect.gen(function* () {
59 const secret = "unexpected-payload-secret";
60 const error = yield* Shared.decodeOptionalPayload<never, never>("initialized", undefined, {
61 token: secret,
62 }).pipe(Effect.flip);
63
64 assert.equal(error.method, "initialized");
65 assert.equal(error.operation, "decode-payload");
66 assert.equal(error.payloadKind, "object");
67 assert.deepEqual(error.data, { payloadKind: "object" });
68 assert.isUndefined(error.cause);
69 assert.notInclude(error.message, secret);
70 assert.notInclude(encodeUnknownJson(error.toProtocolError()), secret);
71 }),
72);
73
74it.effect("keeps invalid payload values only in the exact schema cause", () =>
75 Effect.gen(function* () {
76 const secret = "codex-schema-payload-secret";
77 const cause = yield* decodeNestedNumberPayload({ profile: { token: secret } }).pipe(
78 Effect.flip,
79 );
80 const error = CodexError.CodexAppServerRequestError.invalidPayload(
81 "thread/start",
82 "decode-payload",
83 cause,
84 );
85 const { cause: directCause, ...directDiagnostics } = error;
86
87 assert.strictEqual(directCause, cause);
88 assert.equal(error.method, "thread/start");
89 assert.equal(error.operation, "decode-payload");
90 assert.equal(error.maximumPathDepth, 2);
91 assert.isAbove(error.issueCount ?? 0, 0);
92 assert.include(error.issueKinds ?? [], "Pointer");
93 assert.notInclude(error.message, secret);
94 assert.notInclude(encodeUnknownJson(directDiagnostics), secret);
95 assert.notInclude(encodeUnknownJson(error.toProtocolError()), secret);
96 }),
97);
98
99it.effect("retains the request-handler error as the internal error cause", () =>
100 Effect.gen(function* () {
101 const rootCause = new Error("socket closed");
102 const source = new CodexError.CodexAppServerTransportError({
103 operation: "read-input-stream",
104 cause: rootCause,
105 });
106 const error = yield* Shared.runHandler(
107 (_payload: void) => Effect.fail(source),
108 undefined,
109 "thread/start",
110 ).pipe(Effect.flip);
111
112 assert.equal(error.code, -32603);
113 assert.equal(error.method, "thread/start");
114 assert.equal(error.operation, "handle-request");
115 assert.equal(
116 error.message,
117 "Codex App Server request handler failed for method 'thread/start'",
118 );
119 assert.strictEqual(error.cause, source);
120 assert.strictEqual(source.cause, rootCause);
121 assert.notInclude(error.message, source.message);
122 }),
123);
124
125it.effect("passes request errors through without adding a wrapper", () =>
126 Effect.gen(function* () {
127 const source = CodexError.CodexAppServerRequestError.invalidParams("Invalid thread id");
128 const error = yield* Shared.runHandler(
129 (_payload: void) => Effect.fail(source),
130 undefined,
131 "thread/start",
132 ).pipe(Effect.flip);
133
134 assert.strictEqual(error, source);
135 }),
136);
137
138it.effect("retains the full notification payload decode cause chain", () =>
139 Effect.gen(function* () {
140 const error = yield* Shared.decodeNotificationPayload(
141 "item/agentMessage/delta",
142 Schema.String,
143 42,
144 ).pipe(Effect.flip);
145
146 assert.equal(error.method, "item/agentMessage/delta");
147 assert.equal(error.operation, "decode-notification-payload");
148 assert.instanceOf(error.cause, CodexError.CodexAppServerRequestError);
149 assert.isTrue(Schema.isSchemaError(error.cause.cause));
150 }),
151);