1import { assert, describe, it } from "@effect/vitest"
2import { Duration, Effect, Schema } from "effect"
3import { AiError } from "effect/ai"
4
5describe("AiError", () => {
6 describe("reason types", () => {
7 describe("RateLimitError", () => {
8 it("should be retryable", () => {
9 const error = new AiError.RateLimitError({})
10 assert.isTrue(error.isRetryable)
11 })
12
13 it("should format message with retryAfter", () => {
14 const error = new AiError.RateLimitError({
15 retryAfter: Duration.seconds(60)
16 })
17 assert.match(error.message, /Retry after/)
18 })
19
20 it("should have _tag set correctly", () => {
21 const error = new AiError.RateLimitError({})
22 assert.strictEqual(error._tag, "RateLimitError")
23 })
24 })
25
26 describe("QuotaExhaustedError", () => {
27 it("should not be retryable", () => {
28 const error = new AiError.QuotaExhaustedError({})
29 assert.isFalse(error.isRetryable)
30 })
31
32 it("should format message correctly", () => {
33 const error = new AiError.QuotaExhaustedError({})
34 assert.match(error.message, /Quota exhausted/)
35 })
36
37 it("should have _tag set correctly", () => {
38 const error = new AiError.QuotaExhaustedError({})
39 assert.strictEqual(error._tag, "QuotaExhaustedError")
40 })
41 })
42
43 describe("AuthenticationError", () => {
44 it("should not be retryable", () => {
45 const error = new AiError.AuthenticationError({ kind: "InvalidKey" })
46 assert.isFalse(error.isRetryable)
47 })
48
49 it("should format message based on kind", () => {
50 const invalidKey = new AiError.AuthenticationError({ kind: "InvalidKey" })
51 assert.match(invalidKey.message, /InvalidKey/)
52
53 const expiredKey = new AiError.AuthenticationError({ kind: "ExpiredKey" })
54 assert.match(expiredKey.message, /ExpiredKey/)
55
56 const missingKey = new AiError.AuthenticationError({ kind: "MissingKey" })
57 assert.match(missingKey.message, /MissingKey/)
58
59 const insufficientPermissions = new AiError.AuthenticationError({ kind: "InsufficientPermissions" })
60 assert.match(insufficientPermissions.message, /InsufficientPermissions/)
61 })
62
63 it("should have _tag set correctly", () => {
64 const error = new AiError.AuthenticationError({ kind: "Unknown" })
65 assert.strictEqual(error._tag, "AuthenticationError")
66 })
67
68 it("should render the description after the kind suggestion when present", () => {
69 const error = new AiError.AuthenticationError({
70 kind: "InsufficientPermissions",
71 description: "anthropic.claude-sonnet-5 is not available for this account"
72 })
73 assert.strictEqual(
74 error.message,
75 "InsufficientPermissions: Your API key lacks required permissions. anthropic.claude-sonnet-5 is not available for this account"
76 )
77 })
78
79 it("should render the message unchanged when description is absent", () => {
80 const error = new AiError.AuthenticationError({ kind: "InsufficientPermissions" })
81 assert.strictEqual(
82 error.message,
83 "InsufficientPermissions: Your API key lacks required permissions"
84 )
85 assert.isUndefined(error.description)
86 })
87
88 it("should round-trip the description through the schema", () => {
89 const error = new AiError.AuthenticationError({
90 kind: "ExpiredKey",
91 description: "The security token included in the request is expired"
92 })
93 const encoded = Schema.encodeUnknownSync(AiError.AuthenticationError)(error)
94 const decoded = Schema.decodeUnknownSync(AiError.AuthenticationError)(encoded)
95 assert.strictEqual(decoded.description, "The security token included in the request is expired")
96 })
97 })
98
99 describe("ContentPolicyError", () => {
100 it("should not be retryable", () => {
101 const error = new AiError.ContentPolicyError({ description: "hate speech detected" })
102 assert.isFalse(error.isRetryable)
103 })
104
105 it("should format message with description", () => {
106 const error = new AiError.ContentPolicyError({
107 description: "Input contains prohibited content"
108 })
109 assert.match(error.message, /Content policy violation/)
110 assert.match(error.message, /Input contains prohibited content/)
111 })
112
113 it("should have _tag set correctly", () => {
114 const error = new AiError.ContentPolicyError({ description: "violation" })
115 assert.strictEqual(error._tag, "ContentPolicyError")
116 })
117 })
118
119 describe("InvalidRequestError", () => {
120 it("should not be retryable", () => {
121 const error = new AiError.InvalidRequestError({
122 parameter: "temperature",
123 constraint: "must be between 0 and 2"
124 })
125 assert.isFalse(error.isRetryable)
126 })
127
128 it("should format message with parameter details", () => {
129 const error = new AiError.InvalidRequestError({
130 parameter: "temperature",
131 constraint: "must be between 0 and 2",
132 description: "Value 5 is invalid"
133 })
134 assert.match(error.message, /parameter 'temperature'/)
135 assert.match(error.message, /must be between 0 and 2/)
136 assert.match(error.message, /Value 5 is invalid/)
137 })
138
139 it("should have _tag set correctly", () => {
140 const error = new AiError.InvalidRequestError({})
141 assert.strictEqual(error._tag, "InvalidRequestError")
142 })
143 })
144
145 describe("InternalProviderError", () => {
146 it("should be retryable", () => {
147 const error = new AiError.InternalProviderError({
148 description: "Server error"
149 })
150 assert.isTrue(error.isRetryable)
151 })
152
153 it("should format message with description", () => {
154 const error = new AiError.InternalProviderError({
155 description: "Unexpected server failure"
156 })
157 assert.match(error.message, /Internal provider error/)
158 assert.match(error.message, /Unexpected server failure/)
159 })
160
161 it("should have _tag set correctly", () => {
162 const error = new AiError.InternalProviderError({
163 description: "Server error"
164 })
165 assert.strictEqual(error._tag, "InternalProviderError")
166 })
167 })
168
169 describe("InvalidOutputError", () => {
170 it("should be retryable", () => {
171 const error = new AiError.InvalidOutputError({
172 description: "Invalid JSON structure"
173 })
174 assert.isTrue(error.isRetryable)
175 })
176
177 it("should format message with description", () => {
178 const error = new AiError.InvalidOutputError({
179 description: "Expected string but got number"
180 })
181 assert.match(error.message, /Invalid output/)
182 assert.match(error.message, /Expected string but got number/)
183 })
184
185 it.effect("should create from SchemaError", () =>
186 Effect.gen(function*() {
187 const TestSchema = Schema.Struct({ name: Schema.String })
188 const result = yield* Effect.exit(
189 Schema.decodeUnknownEffect(TestSchema)({ name: 123 })
190 )
191
192 if (result._tag === "Failure") {
193 const cause = result.cause
194 if ("error" in cause && Schema.isSchemaError(cause.error)) {
195 const parseError = AiError.InvalidOutputError.fromSchemaError(cause.error)
196 assert.strictEqual(parseError._tag, "InvalidOutputError")
197 assert.isString(parseError.description)
198 }
199 }
200 }))
201
202 it("should have _tag set correctly", () => {
203 const error = new AiError.InvalidOutputError({
204 description: "Test error"
205 })
206 assert.strictEqual(error._tag, "InvalidOutputError")
207 })
208 })
209
210 describe("UnknownError", () => {
211 it("should not be retryable", () => {
212 const error = new AiError.UnknownError({})
213 assert.isFalse(error.isRetryable)
214 })
215
216 it("should format message with description", () => {
217 const error = new AiError.UnknownError({
218 description: "Something unexpected happened"
219 })
220 assert.strictEqual(error.message, "Something unexpected happened")
221 })
222
223 it("should use default message without description", () => {
224 const error = new AiError.UnknownError({})
225 assert.strictEqual(error.message, "Unknown error")
226 })
227
228 it("should have _tag set correctly", () => {
229 const error = new AiError.UnknownError({})
230 assert.strictEqual(error._tag, "UnknownError")
231 })
232 })
233
234 describe("ToolNotFoundError", () => {
235 it("should be retryable", () => {
236 const error = new AiError.ToolNotFoundError({
237 toolName: "UnknownTool",
238 availableTools: ["GetWeather", "GetTime"]
239 })
240 assert.isTrue(error.isRetryable)
241 })
242
243 it("should format message with available tools", () => {
244 const error = new AiError.ToolNotFoundError({
245 toolName: "UnknownTool",
246 availableTools: ["GetWeather", "GetTime"]
247 })
248 assert.match(error.message, /Tool 'UnknownTool' not found/)
249 assert.match(error.message, /Available tools: GetWeather, GetTime/)
250 })
251
252 it("should format message with no available tools", () => {
253 const error = new AiError.ToolNotFoundError({
254 toolName: "UnknownTool",
255 availableTools: []
256 })
257 assert.match(error.message, /Available tools: none/)
258 })
259
260 it("should have _tag set correctly", () => {
261 const error = new AiError.ToolNotFoundError({
262 toolName: "Test",
263 availableTools: []
264 })
265 assert.strictEqual(error._tag, "ToolNotFoundError")
266 })
267 })
268
269 describe("ToolParameterValidationError", () => {
270 it("should be retryable", () => {
271 const error = new AiError.ToolParameterValidationError({
272 toolName: "GetWeather",
273 description: "Expected string"
274 })
275 assert.isTrue(error.isRetryable)
276 })
277
278 it("should format message with tool name", () => {
279 const error = new AiError.ToolParameterValidationError({
280 toolName: "GetWeather",
281 description: "Expected string"
282 })
283 assert.match(error.message, /Invalid parameters for tool 'GetWeather'/)
284 })
285
286 it("should format message with validation message", () => {
287 const error = new AiError.ToolParameterValidationError({
288 toolName: "GetWeather",
289 description: "Expected string, got number"
290 })
291 assert.match(error.message, /Expected string, got number/)
292 })
293
294 it("should construct when validated params contain non-JSON values", () => {
295 const error = new AiError.ToolParameterValidationError({
296 toolName: "GetWeather",
297 description: `Expected finite number, got NaN at ["city"]`
298 })
299 assert.strictEqual(error._tag, "ToolParameterValidationError")
300 assert.match(error.message, /NaN/)
301 })
302
303 it("should have _tag set correctly", () => {
304 const error = new AiError.ToolParameterValidationError({
305 toolName: "Test",
306 description: "Error"
307 })
308 assert.strictEqual(error._tag, "ToolParameterValidationError")
309 })
310 })
311
312 describe("InvalidToolResultError", () => {
313 it("should not be retryable", () => {
314 const error = new AiError.InvalidToolResultError({
315 toolName: "GetWeather",
316 description: "missing required field"
317 })
318 assert.isFalse(error.isRetryable)
319 })
320
321 it("should format message with tool name", () => {
322 const error = new AiError.InvalidToolResultError({
323 toolName: "GetWeather",
324 description: "missing required field"
325 })
326 assert.match(error.message, /Tool 'GetWeather' returned invalid result/)
327 })
328
329 it("should format message with description", () => {
330 const error = new AiError.InvalidToolResultError({
331 toolName: "GetWeather",
332 description: "missing 'temperature' field"
333 })
334 assert.match(error.message, /missing 'temperature' field/)
335 })
336
337 it("should have _tag set correctly", () => {
338 const error = new AiError.InvalidToolResultError({
339 toolName: "Test",
340 description: "invalid result"
341 })
342 assert.strictEqual(error._tag, "InvalidToolResultError")
343 })
344 })
345
346 describe("ToolResultEncodingError", () => {
347 it("should not be retryable", () => {
348 const error = new AiError.ToolResultEncodingError({
349 toolName: "GetWeather",
350 toolResult: { temp: 72 },
351 description: "Cannot encode"
352 })
353 assert.isFalse(error.isRetryable)
354 })
355
356 it("should format message with tool name", () => {
357 const error = new AiError.ToolResultEncodingError({
358 toolName: "GetWeather",
359 toolResult: { temp: 72 },
360 description: "Cannot encode"
361 })
362 assert.match(error.message, /Failed to encode result for tool 'GetWeather'/)
363 })
364
365 it("should format message with validation message", () => {
366 const error = new AiError.ToolResultEncodingError({
367 toolName: "GetWeather",
368 toolResult: { circular: "ref" },
369 description: "Cannot encode circular reference"
370 })
371 assert.match(error.message, /Cannot encode circular reference/)
372 })
373
374 it("should have _tag set correctly", () => {
375 const error = new AiError.ToolResultEncodingError({
376 toolName: "Test",
377 toolResult: {},
378 description: "Error"
379 })
380 assert.strictEqual(error._tag, "ToolResultEncodingError")
381 })
382 })
383
384 describe("ToolConfigurationError", () => {
385 it("should not be retryable", () => {
386 const error = new AiError.ToolConfigurationError({
387 toolName: "OpenAiCodeInterpreter",
388 description: "Invalid container ID format"
389 })
390 assert.isFalse(error.isRetryable)
391 })
392
393 it("should format message with tool name", () => {
394 const error = new AiError.ToolConfigurationError({
395 toolName: "OpenAiCodeInterpreter",
396 description: "Invalid container ID format"
397 })
398 assert.match(error.message, /Invalid configuration for tool 'OpenAiCodeInterpreter'/)
399 })
400
401 it("should format message with description", () => {
402 const error = new AiError.ToolConfigurationError({
403 toolName: "OpenAiWebSearch",
404 description: "search_context_size must be between 1 and 10"
405 })
406 assert.match(error.message, /search_context_size must be between 1 and 10/)
407 })
408
409 it("should have _tag set correctly", () => {
410 const error = new AiError.ToolConfigurationError({
411 toolName: "Test",
412 description: "Error"
413 })
414 assert.strictEqual(error._tag, "ToolConfigurationError")
415 })
416 })
417
418 describe("StructuredOutputError", () => {
419 it("should be retryable", () => {
420 const error = new AiError.StructuredOutputError({
421 description: "Invalid JSON structure",
422 responseText: "{\"invalid\":}"
423 })
424 assert.isTrue(error.isRetryable)
425 })
426
427 it("should format message with description", () => {
428 const error = new AiError.StructuredOutputError({
429 description: "Expected a valid JSON object",
430 responseText: "{\"test\":true}"
431 })
432 assert.match(error.message, /Structured output validation failed/)
433 assert.match(error.message, /Expected a valid JSON object/)
434 })
435
436 it("should have _tag set correctly", () => {
437 const error = new AiError.StructuredOutputError({
438 description: "Test error",
439 responseText: "{\"foo\":\"bar\"}"
440 })
441 assert.strictEqual(error._tag, "StructuredOutputError")
442 })
443
444 it.effect("should create from SchemaError", () =>
445 Effect.gen(function*() {
446 const TestSchema = Schema.Struct({ name: Schema.String })
447 const result = yield* Effect.exit(
448 Schema.decodeUnknownEffect(TestSchema)({ name: 123 })
449 )
450
451 if (result._tag === "Failure") {
452 const cause = result.cause
453 if ("error" in cause && Schema.isSchemaError(cause.error)) {
454 const parseError = AiError.StructuredOutputError.fromSchemaError(cause.error, "{\"name\":123}")
455 assert.strictEqual(parseError._tag, "StructuredOutputError")
456 assert.isString(parseError.description)
457 assert.strictEqual(parseError.responseText, "{\"name\":123}")
458 }
459 }
460 }))
461 })
462
463 describe("UnsupportedSchemaError", () => {
464 it("should not be retryable", () => {
465 const error = new AiError.UnsupportedSchemaError({
466 description: "Unions are not supported"
467 })
468 assert.isFalse(error.isRetryable)
469 })
470
471 it("should format message with description", () => {
472 const error = new AiError.UnsupportedSchemaError({
473 description: "Unions are not supported in Anthropic structured output"
474 })
475 assert.match(error.message, /Unsupported schema/)
476 assert.match(error.message, /Unions are not supported in Anthropic structured output/)
477 })
478
479 it("should have _tag set correctly", () => {
480 const error = new AiError.UnsupportedSchemaError({
481 description: "Test error"
482 })
483 assert.strictEqual(error._tag, "UnsupportedSchemaError")
484 })
485 })
486 })
487
488 describe("delegation", () => {
489 it("should delegate isRetryable to reason", () => {
490 const retryableError = new AiError.AiError({
491 module: "OpenAI",
492 method: "completion",
493 reason: new AiError.RateLimitError({})
494 })
495 assert.isTrue(retryableError.isRetryable)
496
497 const nonRetryableError = new AiError.AiError({
498 module: "OpenAI",
499 method: "completion",
500 reason: new AiError.AuthenticationError({ kind: "InvalidKey" })
501 })
502 assert.isFalse(nonRetryableError.isRetryable)
503 })
504
505 it("should delegate retryAfter to reason", () => {
506 const errorWithRetryAfter = new AiError.AiError({
507 module: "OpenAI",
508 method: "completion",
509 reason: new AiError.RateLimitError({
510 retryAfter: Duration.seconds(60)
511 })
512 })
513 assert.isDefined(errorWithRetryAfter.retryAfter)
514
515 const errorWithoutRetryAfter = new AiError.AiError({
516 module: "OpenAI",
517 method: "completion",
518 reason: new AiError.AuthenticationError({ kind: "InvalidKey" })
519 })
520 assert.isUndefined(errorWithoutRetryAfter.retryAfter)
521 })
522
523 it("should format message with module, method, and reason", () => {
524 const error = new AiError.AiError({
525 module: "OpenAI",
526 method: "completion",
527 reason: new AiError.RateLimitError({})
528 })
529 assert.match(error.message, /OpenAI\.completion:/)
530 assert.match(error.message, /Rate limit exceeded/)
531 })
532
533 it("should have _tag set correctly", () => {
534 const error = new AiError.AiError({
535 module: "Test",
536 method: "test",
537 reason: new AiError.UnknownError({})
538 })
539 assert.strictEqual(error._tag, "AiError")
540 })
541 })
542
543 describe("constructors", () => {
544 describe("make", () => {
545 it("should create AiError", () => {
546 const error = AiError.make({
547 module: "OpenAI",
548 method: "completion",
549 reason: new AiError.RateLimitError({})
550 })
551 assert.strictEqual(error._tag, "AiError")
552 assert.strictEqual(error.module, "OpenAI")
553 assert.strictEqual(error.method, "completion")
554 assert.strictEqual(error.reason._tag, "RateLimitError")
555 })
556 })
557
558 describe("reasonFromHttpStatus", () => {
559 it("should map 400 to InvalidRequestError", () => {
560 const reason = AiError.reasonFromHttpStatus({ status: 400 })
561 assert.strictEqual(reason._tag, "InvalidRequestError")
562 })
563
564 it("should map 401 to AuthenticationError with InvalidKey", () => {
565 const reason = AiError.reasonFromHttpStatus({ status: 401 })
566 assert.strictEqual(reason._tag, "AuthenticationError")
567 if (reason._tag === "AuthenticationError") {
568 assert.strictEqual(reason.kind, "InvalidKey")
569 }
570 })
571
572 it("should map 403 to AuthenticationError with InsufficientPermissions", () => {
573 const reason = AiError.reasonFromHttpStatus({ status: 403 })
574 assert.strictEqual(reason._tag, "AuthenticationError")
575 if (reason._tag === "AuthenticationError") {
576 assert.strictEqual(reason.kind, "InsufficientPermissions")
577 }
578 })
579
580 it("should thread the description into 401/403 AuthenticationError", () => {
581 const reason = AiError.reasonFromHttpStatus({
582 status: 403,
583 description: "User is not authorized to perform: bedrock:InvokeModel"
584 })
585 assert.strictEqual(reason._tag, "AuthenticationError")
586 if (reason._tag === "AuthenticationError") {
587 assert.strictEqual(reason.description, "User is not authorized to perform: bedrock:InvokeModel")
588 assert.include(reason.message, "User is not authorized to perform: bedrock:InvokeModel")
589 }
590 })
591
592 it("should map 429 to RateLimitError", () => {
593 const reason = AiError.reasonFromHttpStatus({ status: 429 })
594 assert.strictEqual(reason._tag, "RateLimitError")
595 })
596
597 it("should map 5xx to InternalProviderError", () => {
598 assert.strictEqual(
599 AiError.reasonFromHttpStatus({ status: 500 })._tag,
600 "InternalProviderError"
601 )
602 assert.strictEqual(
603 AiError.reasonFromHttpStatus({ status: 502 })._tag,
604 "InternalProviderError"
605 )
606 assert.strictEqual(
607 AiError.reasonFromHttpStatus({ status: 503 })._tag,
608 "InternalProviderError"
609 )
610 })
611
612 it("should map unknown status to UnknownError", () => {
613 const reason = AiError.reasonFromHttpStatus({ status: 418 })
614 assert.strictEqual(reason._tag, "UnknownError")
615 })
616 })
617 })
618
619 describe("type guards", () => {
620 describe("isAiError", () => {
621 it("should return true for AiError", () => {
622 const error = new AiError.AiError({
623 module: "Test",
624 method: "test",
625 reason: new AiError.UnknownError({})
626 })
627 assert.isTrue(AiError.isAiError(error))
628 })
629
630 it("should return false for non-AiError values", () => {
631 assert.isFalse(AiError.isAiError(new Error("regular error")))
632 assert.isFalse(AiError.isAiError(null))
633 assert.isFalse(AiError.isAiError(undefined))
634 assert.isFalse(AiError.isAiError({ _tag: "FakeError" }))
635 })
636
637 it("should return false for reason types", () => {
638 const networkError = new AiError.NetworkError({
639 reason: "TransportError",
640 request: {
641 method: "GET",
642 url: "https://example.com",
643 urlParams: [],
644 hash: undefined,
645 headers: {}
646 }
647 })
648 assert.isFalse(AiError.isAiError(networkError))
649 })
650 })
651 })
652
653 describe("supporting schemas", () => {
654 describe("ProviderMetadata", () => {
655 it.effect("should encode and decode roundtrip", () =>
656 Effect.gen(function*() {
657 const metadata = {
658 name: "OpenAI",
659 errorCode: "rate_limit_exceeded",
660 errorType: "rate_limit",
661 requestId: "req_123abc"
662 }
663 const encoded = yield* Schema.encodeEffect(AiError.ProviderMetadata)(metadata)
664 const decoded = yield* Schema.decodeEffect(AiError.ProviderMetadata)(encoded)
665 assert.deepStrictEqual(decoded, metadata)
666 }))
667 })
668
669 describe("UsageInfo", () => {
670 it.effect("should encode and decode roundtrip", () =>
671 Effect.gen(function*() {
672 const usage = {
673 promptTokens: 100,
674 completionTokens: 50,
675 totalTokens: 150
676 }
677 const encoded = yield* Schema.encodeEffect(AiError.UsageInfo)(usage)
678 const decoded = yield* Schema.decodeEffect(AiError.UsageInfo)(encoded)
679 assert.deepStrictEqual(decoded, usage)
680 }))
681 })
682
683 describe("HttpContext", () => {
684 it.effect("should encode and decode roundtrip", () =>
685 Effect.gen(function*() {
686 const context = {
687 request: {
688 method: "POST" as const,
689 url: "https://api.example.com/v1/chat",
690 urlParams: [["model", "gpt-4"]] as Array<[string, string]>,
691 hash: undefined,
692 headers: { "Content-Type": "application/json" }
693 },
694 response: {
695 status: 200,
696 headers: { "Content-Type": "application/json" }
697 },
698 body: "{\"result\": \"success\"}"
699 }
700 const encoded = yield* Schema.encodeEffect(AiError.HttpContext)(context)
701 const decoded = yield* Schema.decodeEffect(AiError.HttpContext)(encoded)
702 assert.deepStrictEqual(decoded, context)
703 }))
704 })
705 })
706
707 describe("schema roundtrip", () => {
708 it.effect("RateLimitError roundtrip", () =>
709 Effect.gen(function*() {
710 const error = new AiError.RateLimitError({
711 retryAfter: Duration.seconds(60)
712 })
713 const encoded = yield* Schema.encodeEffect(AiError.RateLimitError)(error)
714 const decoded = yield* Schema.decodeEffect(AiError.RateLimitError)(encoded)
715 assert.strictEqual(decoded._tag, "RateLimitError")
716 assert.isDefined(decoded.retryAfter)
717 }))
718
719 it.effect("AiErrorReason union roundtrip", () =>
720 Effect.gen(function*() {
721 const rateLimitError: AiError.AiErrorReason = new AiError.RateLimitError({})
722 const encoded = yield* Schema.encodeEffect(AiError.AiErrorReason)(rateLimitError)
723 const decoded = yield* Schema.decodeEffect(AiError.AiErrorReason)(encoded)
724 assert.strictEqual(decoded._tag, "RateLimitError")
725
726 const authError: AiError.AiErrorReason = new AiError.AuthenticationError({
727 kind: "ExpiredKey"
728 })
729 const authEncoded = yield* Schema.encodeEffect(AiError.AiErrorReason)(authError)
730 const authDecoded = yield* Schema.decodeEffect(AiError.AiErrorReason)(authEncoded)
731 assert.strictEqual(authDecoded._tag, "AuthenticationError")
732 }))
733
734 it.effect("AiError roundtrip", () =>
735 Effect.gen(function*() {
736 const error = new AiError.AiError({
737 module: "OpenAI",
738 method: "completion",
739 reason: new AiError.RateLimitError({})
740 })
741 const encoded = yield* Schema.encodeEffect(AiError.AiError)(error)
742 const decoded = yield* Schema.decodeEffect(AiError.AiError)(encoded)
743 assert.strictEqual(decoded._tag, "AiError")
744 assert.strictEqual(decoded.module, "OpenAI")
745 assert.strictEqual(decoded.method, "completion")
746 assert.strictEqual(decoded.reason._tag, "RateLimitError")
747 }))
748
749 it.effect("ToolNotFoundError roundtrip", () =>
750 Effect.gen(function*() {
751 const error = new AiError.ToolNotFoundError({
752 toolName: "UnknownTool",
753 availableTools: ["GetWeather", "GetTime"]
754 })
755 const encoded = yield* Schema.encodeEffect(AiError.ToolNotFoundError)(error)
756 const decoded = yield* Schema.decodeEffect(AiError.ToolNotFoundError)(encoded)
757 assert.strictEqual(decoded._tag, "ToolNotFoundError")
758 assert.strictEqual(decoded.toolName, "UnknownTool")
759 assert.deepStrictEqual(decoded.availableTools, ["GetWeather", "GetTime"])
760 }))
761
762 it.effect("ToolParameterValidationError roundtrip", () =>
763 Effect.gen(function*() {
764 const error = new AiError.ToolParameterValidationError({
765 toolName: "GetWeather",
766 description: "Expected string"
767 })
768 const encoded = yield* Schema.encodeEffect(AiError.ToolParameterValidationError)(error)
769 const decoded = yield* Schema.decodeEffect(AiError.ToolParameterValidationError)(encoded)
770 assert.strictEqual(decoded._tag, "ToolParameterValidationError")
771 assert.strictEqual(decoded.toolName, "GetWeather")
772 assert.strictEqual(decoded.description, "Expected string")
773 }))
774
775 it.effect("InvalidToolResultError roundtrip", () =>
776 Effect.gen(function*() {
777 const error = new AiError.InvalidToolResultError({
778 toolName: "GetWeather",
779 description: "missing required field"
780 })
781 const encoded = yield* Schema.encodeEffect(AiError.InvalidToolResultError)(error)
782 const decoded = yield* Schema.decodeEffect(AiError.InvalidToolResultError)(encoded)
783 assert.strictEqual(decoded._tag, "InvalidToolResultError")
784 assert.strictEqual(decoded.toolName, "GetWeather")
785 assert.strictEqual(decoded.description, "missing required field")
786 }))
787
788 it.effect("ToolResultEncodingError roundtrip", () =>
789 Effect.gen(function*() {
790 const error = new AiError.ToolResultEncodingError({
791 toolName: "GetWeather",
792 toolResult: { temp: 72 },
793 description: "Circular reference"
794 })
795 const encoded = yield* Schema.encodeEffect(AiError.ToolResultEncodingError)(error)
796 const decoded = yield* Schema.decodeEffect(AiError.ToolResultEncodingError)(encoded)
797 assert.strictEqual(decoded._tag, "ToolResultEncodingError")
798 assert.strictEqual(decoded.toolName, "GetWeather")
799 assert.strictEqual(decoded.description, "Circular reference")
800 }))
801
802 it.effect("ToolConfigurationError roundtrip", () =>
803 Effect.gen(function*() {
804 const error = new AiError.ToolConfigurationError({
805 toolName: "OpenAiCodeInterpreter",
806 description: "Invalid container ID format"
807 })
808 const encoded = yield* Schema.encodeEffect(AiError.ToolConfigurationError)(error)
809 const decoded = yield* Schema.decodeEffect(AiError.ToolConfigurationError)(encoded)
810 assert.strictEqual(decoded._tag, "ToolConfigurationError")
811 assert.strictEqual(decoded.toolName, "OpenAiCodeInterpreter")
812 assert.strictEqual(decoded.description, "Invalid container ID format")
813 }))
814
815 it.effect("StructuredOutputError roundtrip", () =>
816 Effect.gen(function*() {
817 const error = new AiError.StructuredOutputError({
818 description: "Invalid JSON structure",
819 responseText: "{\"invalid\":}"
820 })
821 const encoded = yield* Schema.encodeEffect(AiError.StructuredOutputError)(error)
822 const decoded = yield* Schema.decodeEffect(AiError.StructuredOutputError)(encoded)
823 assert.strictEqual(decoded._tag, "StructuredOutputError")
824 assert.strictEqual(decoded.description, "Invalid JSON structure")
825 assert.strictEqual(decoded.responseText, "{\"invalid\":}")
826 }))
827
828 it.effect("UnsupportedSchemaError roundtrip", () =>
829 Effect.gen(function*() {
830 const error = new AiError.UnsupportedSchemaError({
831 description: "Unions are not supported"
832 })
833 const encoded = yield* Schema.encodeEffect(AiError.UnsupportedSchemaError)(error)
834 const decoded = yield* Schema.decodeEffect(AiError.UnsupportedSchemaError)(encoded)
835 assert.strictEqual(decoded._tag, "UnsupportedSchemaError")
836 assert.strictEqual(decoded.description, "Unions are not supported")
837 }))
838
839 it.effect("AiErrorReason union with tool errors roundtrip", () =>
840 Effect.gen(function*() {
841 const toolNotFound: AiError.AiErrorReason = new AiError.ToolNotFoundError({
842 toolName: "Unknown",
843 availableTools: ["A", "B"]
844 })
845 const encoded = yield* Schema.encodeEffect(AiError.AiErrorReason)(toolNotFound)
846 const decoded = yield* Schema.decodeEffect(AiError.AiErrorReason)(encoded)
847 assert.strictEqual(decoded._tag, "ToolNotFoundError")
848
849 const paramError: AiError.AiErrorReason = new AiError.ToolParameterValidationError({
850 toolName: "Test",
851 description: "Error"
852 })
853 const paramEncoded = yield* Schema.encodeEffect(AiError.AiErrorReason)(paramError)
854 const paramDecoded = yield* Schema.decodeEffect(AiError.AiErrorReason)(paramEncoded)
855 assert.strictEqual(paramDecoded._tag, "ToolParameterValidationError")
856 }))
857 })
858})