session-error.test.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
  1. import { describe, expect, test } from "bun:test"
  2. import {
  3. APIError,
  4. Authentication,
  5. BadRequest,
  6. ConnectionError,
  7. ContentPolicy,
  8. ContextOverflow,
  9. MalformedResponse,
  10. ModelID,
  11. NoRoute,
  12. NotFound,
  13. PermissionDenied,
  14. ProviderID,
  15. QuotaExceeded,
  16. RateLimit,
  17. RouteID,
  18. ServerError,
  19. TimeoutError,
  20. ToolFailure,
  21. } from "@opencode-ai/llm"
  22. import { PermissionV2 } from "@opencode-ai/core/permission"
  23. import { Tool } from "@opencode-ai/plugin/v2/effect/tool"
  24. import { toSessionError } from "@opencode-ai/core/session/to-session-error"
  25. import { SessionRunnerRetry } from "@opencode-ai/core/session/runner/retry"
  26. describe("toSessionError", () => {
  27. test("maps every LLM error tag to the open wire type", () => {
  28. expect(toSessionError(new RateLimit({ message: "rate", retryAfterMs: 123 }))).toEqual({
  29. type: "provider.rate-limit",
  30. message: "rate",
  31. })
  32. expect(toSessionError(new Authentication({ message: "auth" })).type).toBe("provider.auth")
  33. expect(toSessionError(new PermissionDenied({ message: "forbidden" })).type).toBe("provider.auth")
  34. expect(toSessionError(new NotFound({ message: "missing" })).type).toBe("provider.not-found")
  35. expect(toSessionError(new QuotaExceeded({ message: "quota" })).type).toBe("provider.quota")
  36. expect(toSessionError(new ContentPolicy({ message: "blocked" })).type).toBe("provider.content-filter")
  37. expect(toSessionError(new ContextOverflow({ message: "too long" })).type).toBe("provider.context-overflow")
  38. expect(toSessionError(new ConnectionError({ message: "reset" })).type).toBe("provider.transport")
  39. expect(toSessionError(new TimeoutError({ message: "timed out" })).type).toBe("provider.timeout")
  40. expect(toSessionError(new ServerError({ message: "internal", status: 500 })).type).toBe("provider.internal")
  41. expect(toSessionError(new MalformedResponse({ message: "output" })).type).toBe("provider.invalid-output")
  42. expect(toSessionError(new BadRequest({ message: "request" })).type).toBe("provider.invalid-request")
  43. expect(
  44. toSessionError(
  45. new NoRoute({
  46. route: RouteID.make("route"),
  47. provider: ProviderID.make("provider"),
  48. model: ModelID.make("model"),
  49. }),
  50. ).type,
  51. ).toBe("provider.no-route")
  52. expect(toSessionError(new APIError({ message: "unknown", status: 418 })).type).toBe("provider.unknown")
  53. })
  54. test("preserves the permission rejection type without exposing internal fields", () => {
  55. const blocked = new PermissionV2.BlockedError({ rules: [], permission: "external_directory", resources: [] })
  56. expect(toSessionError(blocked)).toEqual({
  57. type: "permission.rejected",
  58. message: "Permission denied: external_directory",
  59. })
  60. expect(toSessionError(new ToolFailure({ message: blocked.message, error: blocked }))).toEqual({
  61. type: "permission.rejected",
  62. message: "Permission denied: external_directory",
  63. })
  64. expect(toSessionError(new Tool.Failure({ message: "failed" }))).toEqual({
  65. type: "tool.execution",
  66. message: "failed",
  67. })
  68. })
  69. test("retries only rate limits, server errors, connection failures, and timeouts", () => {
  70. const eligible = [
  71. new RateLimit({ message: "rate" }),
  72. new ServerError({ message: "internal", status: 500 }),
  73. new ConnectionError({ message: "reset" }),
  74. new TimeoutError({ message: "timed out" }),
  75. ]
  76. const ineligible = [
  77. new Authentication({ message: "auth" }),
  78. new PermissionDenied({ message: "forbidden" }),
  79. new NotFound({ message: "missing" }),
  80. new QuotaExceeded({ message: "quota" }),
  81. new ContentPolicy({ message: "blocked" }),
  82. new ContextOverflow({ message: "too long" }),
  83. new MalformedResponse({ message: "output" }),
  84. new BadRequest({ message: "request" }),
  85. new NoRoute({
  86. route: RouteID.make("route"),
  87. provider: ProviderID.make("provider"),
  88. model: ModelID.make("model"),
  89. }),
  90. new APIError({ message: "unknown" }),
  91. ]
  92. expect(eligible.map(SessionRunnerRetry.isRetryable)).toEqual([true, true, true, true])
  93. expect(ineligible.map(SessionRunnerRetry.isRetryable)).toEqual(ineligible.map(() => false))
  94. })
  95. })