1import { assert, it } from "@effect/vitest";
2import {
3 NodeId,
4 ProviderDriverKind,
5 ProviderThreadId,
6 ProviderTurnId,
7 RunId,
8 ThreadId,
9} from "@t3tools/contracts";
10import * as Cause from "effect/Cause";
11import * as DateTime from "effect/DateTime";
12import * as Effect from "effect/Effect";
13
14import {
15 makeProviderFailure,
16 makeProviderFailureTurnItem,
17 MAX_PROVIDER_FAILURE_CODE_LENGTH,
18 MAX_PROVIDER_FAILURE_MESSAGE_LENGTH,
19 ContextHandoffBudgetError,
20} from "./failure.ts";
21import * as IdAllocator from "./IdAllocator.ts";
22import * as ProviderAdapter from "@t3tools/provider-core/server/ProviderAdapter";
23
24it("redacts credentials and URL secrets from provider failures", () => {
25 const failure = makeProviderFailure({
26 message:
27 'request failed: Authorization: Bearer bearer-secret https://user:pass@example.test/path?access_token=url-secret#fragment {"token":"json-secret"} api_key=key-secret sk-abcdefghijklmnop',
28 code: "provider_rejected",
29 class: "provider_error",
30 });
31
32 assert.equal(failure.class, "provider_error");
33 assert.equal(failure.code, "provider_rejected");
34 assert.include(failure.message, "[REDACTED]");
35 assert.include(failure.message, "https://example.test/path");
36 assert.notInclude(failure.message, "bearer-secret");
37 assert.notInclude(failure.message, "user:pass");
38 assert.notInclude(failure.message, "url-secret");
39 assert.notInclude(failure.message, "json-secret");
40 assert.notInclude(failure.message, "key-secret");
41 assert.notInclude(failure.message, "sk-abcdefghijklmnop");
42});
43
44it("replaces unsafe control characters without stripping whitespace", () => {
45 const failure = makeProviderFailure({ message: "before\u0000\u0007\t\nafter\u007f" });
46
47 assert.equal(failure.message, "before \t\nafter");
48});
49
50it("bounds provider-controlled failure strings", () => {
51 const failure = makeProviderFailure({
52 message: "m".repeat(MAX_PROVIDER_FAILURE_MESSAGE_LENGTH + 500),
53 code: "c".repeat(MAX_PROVIDER_FAILURE_CODE_LENGTH + 50),
54 });
55
56 assert.equal(failure.message.length, MAX_PROVIDER_FAILURE_MESSAGE_LENGTH);
57 assert.equal(failure.code?.length, MAX_PROVIDER_FAILURE_CODE_LENGTH);
58 assert.match(failure.message, /…$/u);
59 assert.match(failure.code ?? "", /…$/u);
60});
61
62it("does not split a surrogate pair at the truncation boundary", () => {
63 const failure = makeProviderFailure({
64 message: `${"a".repeat(MAX_PROVIDER_FAILURE_MESSAGE_LENGTH - 2)}🚀tail`,
65 });
66
67 assert.equal(failure.message.length, MAX_PROVIDER_FAILURE_MESSAGE_LENGTH - 1);
68 assert.equal(failure.message.at(-1), "…");
69 assert.notMatch(failure.message.slice(0, -1), /[\uD800-\uDBFF]$/u);
70});
71
72it("does not expose arbitrary cause messages and preserves explicit messages", () => {
73 const cause = new Error("Adapter failed", {
74 cause: new Error("Session expired. Sign in again."),
75 });
76 for (const value of [
77 cause,
78 Cause.fail(cause),
79 "Session expired. Sign in again.",
80 { message: "Session expired. Sign in again." },
81 ]) {
82 assert.equal(makeProviderFailure({ cause: value }).message, "Provider turn failed.");
83 assert.equal(
84 makeProviderFailure({ cause: value, message: "Provider connection closed." }).message,
85 "Provider connection closed.",
86 );
87 }
88});
89
90it("preserves actionable handoff errors wrapped by turn startup", () => {
91 const cause = new ProviderAdapter.ProviderAdapterTurnStartError({
92 driver: ProviderDriverKind.make("codex"),
93 threadId: ThreadId.make("thread:handoff-error"),
94 providerThreadId: ProviderThreadId.make("provider-thread:handoff-error"),
95 runId: RunId.make("run:handoff-error"),
96 cause: new ContextHandoffBudgetError(),
97 });
98 assert.equal(
99 makeProviderFailure({ cause: Cause.fail(cause) }).message,
100 new ContextHandoffBudgetError().message,
101 );
102});
103
104it("does not expose defect text nested inside a known error category", () => {
105 const failure = makeProviderFailure({
106 cause: {
107 _tag: "ProviderAdapterEventStreamError",
108 cause: new Error(
109 `Session rejected: Bearer nested-secret https://user:pass@example.test/path?token=secret ${"x".repeat(5000)}`,
110 ),
111 },
112 });
113 assert.include(failure.message, "provider event stream closed unexpectedly");
114 assert.notInclude(failure.message, "Session rejected:");
115 assert.notInclude(failure.message, "nested-secret");
116 assert.notInclude(failure.message, "user:pass");
117 assert.notInclude(failure.message, "token=secret");
118 assert.isBelow(failure.message.length, MAX_PROVIDER_FAILURE_MESSAGE_LENGTH);
119});
120
121it("handles cyclic causes and throwing accessors", () => {
122 const cyclic: { message: string; cause?: unknown } = {
123 message: "Provider disconnected. Retry the turn.",
124 };
125 cyclic.cause = cyclic;
126 assert.equal(makeProviderFailure({ cause: cyclic }).message, "Provider turn failed.");
127 assert.equal(
128 makeProviderFailure({
129 cause: {
130 get message() {
131 throw new Error("unreadable");
132 },
133 get cause() {
134 throw new Error("unreadable");
135 },
136 },
137 }).message,
138 "Provider turn failed.",
139 );
140});
141
142it("falls back when inspecting a provider cause throws", () => {
143 const cause = new Proxy(
144 {},
145 {
146 has() {
147 throw new Error("unreadable provider cause");
148 },
149 },
150 );
151 assert.equal(makeProviderFailure({ cause }).message, "Provider turn failed.");
152});
153
154it("does not serialize arbitrary provider causes", () => {
155 const failure = makeProviderFailure({
156 cause: {
157 payload: { authorization: "Bearer nested-secret" },
158 stack: "private provider stack",
159 },
160 class: "transport_error",
161 });
162
163 assert.deepEqual(failure, {
164 class: "transport_error",
165 message: "Provider turn failed.",
166 code: null,
167 retryable: null,
168 });
169});
170
171it.effect("keys terminal failure items by provider turn across retries and fallback paths", () =>
172 Effect.gen(function* () {
173 const idAllocator = yield* IdAllocator.IdAllocatorV2;
174 const driver = ProviderDriverKind.make("codex");
175 const runId = RunId.make("run:provider-failure-id");
176 const base = {
177 idAllocator,
178 driver,
179 threadId: ThreadId.make("thread:provider-failure-id"),
180 runId,
181 nodeId: NodeId.make("node:provider-failure-id"),
182 providerThreadId: ProviderThreadId.make("provider-thread:provider-failure-id"),
183 itemOrdinal: 101,
184 failure: makeProviderFailure({ message: "Provider failed" }),
185 occurredAt: DateTime.makeUnsafe("2026-06-22T12:00:00.000Z"),
186 } as const;
187 const firstTurnId = ProviderTurnId.make("provider-turn:provider-failure-id:first");
188 const secondTurnId = ProviderTurnId.make("provider-turn:provider-failure-id:second");
189
190 const firstAttempt = makeProviderFailureTurnItem({
191 ...base,
192 providerTurnId: firstTurnId,
193 });
194 const retriedAttempt = makeProviderFailureTurnItem({
195 ...base,
196 providerTurnId: secondTurnId,
197 });
198 const ingestorFallback = makeProviderFailureTurnItem({
199 ...base,
200 runId: null,
201 nodeId: null,
202 providerTurnId: firstTurnId,
203 });
204
205 assert.notEqual(firstAttempt.id, retriedAttempt.id);
206 assert.equal(firstAttempt.id, ingestorFallback.id);
207 assert.equal(firstAttempt.ordinal, 101);
208 }).pipe(Effect.provide(IdAllocator.layer)),
209);