| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899 |
- import { describe, expect, test } from "bun:test"
- import {
- APIError,
- Authentication,
- BadRequest,
- ConnectionError,
- ContentPolicy,
- ContextOverflow,
- MalformedResponse,
- ModelID,
- NoRoute,
- NotFound,
- PermissionDenied,
- ProviderID,
- QuotaExceeded,
- RateLimit,
- RouteID,
- ServerError,
- TimeoutError,
- ToolFailure,
- } from "@opencode-ai/llm"
- import { PermissionV2 } from "@opencode-ai/core/permission"
- import { Tool } from "@opencode-ai/plugin/v2/effect/tool"
- import { toSessionError } from "@opencode-ai/core/session/to-session-error"
- import { SessionRunnerRetry } from "@opencode-ai/core/session/runner/retry"
- describe("toSessionError", () => {
- test("maps every LLM error tag to the open wire type", () => {
- expect(toSessionError(new RateLimit({ message: "rate", retryAfterMs: 123 }))).toEqual({
- type: "provider.rate-limit",
- message: "rate",
- })
- expect(toSessionError(new Authentication({ message: "auth" })).type).toBe("provider.auth")
- expect(toSessionError(new PermissionDenied({ message: "forbidden" })).type).toBe("provider.auth")
- expect(toSessionError(new NotFound({ message: "missing" })).type).toBe("provider.not-found")
- expect(toSessionError(new QuotaExceeded({ message: "quota" })).type).toBe("provider.quota")
- expect(toSessionError(new ContentPolicy({ message: "blocked" })).type).toBe("provider.content-filter")
- expect(toSessionError(new ContextOverflow({ message: "too long" })).type).toBe("provider.context-overflow")
- expect(toSessionError(new ConnectionError({ message: "reset" })).type).toBe("provider.transport")
- expect(toSessionError(new TimeoutError({ message: "timed out" })).type).toBe("provider.timeout")
- expect(toSessionError(new ServerError({ message: "internal", status: 500 })).type).toBe("provider.internal")
- expect(toSessionError(new MalformedResponse({ message: "output" })).type).toBe("provider.invalid-output")
- expect(toSessionError(new BadRequest({ message: "request" })).type).toBe("provider.invalid-request")
- expect(
- toSessionError(
- new NoRoute({
- route: RouteID.make("route"),
- provider: ProviderID.make("provider"),
- model: ModelID.make("model"),
- }),
- ).type,
- ).toBe("provider.no-route")
- expect(toSessionError(new APIError({ message: "unknown", status: 418 })).type).toBe("provider.unknown")
- })
- test("preserves the permission rejection type without exposing internal fields", () => {
- const blocked = new PermissionV2.BlockedError({ rules: [], permission: "external_directory", resources: [] })
- expect(toSessionError(blocked)).toEqual({
- type: "permission.rejected",
- message: "Permission denied: external_directory",
- })
- expect(toSessionError(new ToolFailure({ message: blocked.message, error: blocked }))).toEqual({
- type: "permission.rejected",
- message: "Permission denied: external_directory",
- })
- expect(toSessionError(new Tool.Failure({ message: "failed" }))).toEqual({
- type: "tool.execution",
- message: "failed",
- })
- })
- test("retries only rate limits, server errors, connection failures, and timeouts", () => {
- const eligible = [
- new RateLimit({ message: "rate" }),
- new ServerError({ message: "internal", status: 500 }),
- new ConnectionError({ message: "reset" }),
- new TimeoutError({ message: "timed out" }),
- ]
- const ineligible = [
- new Authentication({ message: "auth" }),
- new PermissionDenied({ message: "forbidden" }),
- new NotFound({ message: "missing" }),
- new QuotaExceeded({ message: "quota" }),
- new ContentPolicy({ message: "blocked" }),
- new ContextOverflow({ message: "too long" }),
- new MalformedResponse({ message: "output" }),
- new BadRequest({ message: "request" }),
- new NoRoute({
- route: RouteID.make("route"),
- provider: ProviderID.make("provider"),
- model: ModelID.make("model"),
- }),
- new APIError({ message: "unknown" }),
- ]
- expect(eligible.map(SessionRunnerRetry.isRetryable)).toEqual([true, true, true, true])
- expect(ineligible.map(SessionRunnerRetry.isRetryable)).toEqual(ineligible.map(() => false))
- })
- })
|