session-error.test.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. import { describe, expect, test } from "bun:test"
  2. import {
  3. AuthenticationReason,
  4. ContentPolicyReason,
  5. InvalidProviderOutputReason,
  6. InvalidRequestReason,
  7. LLMError,
  8. NoRouteReason,
  9. ModelID,
  10. ProviderID,
  11. ProviderInternalReason,
  12. QuotaExceededReason,
  13. RateLimitReason,
  14. TransportReason,
  15. UnknownProviderReason,
  16. ToolFailure,
  17. } from "@opencode-ai/ai"
  18. import { PermissionV2 } from "@opencode-ai/core/permission"
  19. import { Tool } from "@opencode-ai/plugin/v2/effect/tool"
  20. import { toSessionError } from "@opencode-ai/core/session/to-session-error"
  21. import { SessionRunnerRetry } from "@opencode-ai/core/session/runner/retry"
  22. const llm = (reason: LLMError["reason"]) => new LLMError({ module: "test", method: "stream", reason })
  23. describe("toSessionError", () => {
  24. test("maps every LLM reason to the open wire type", () => {
  25. expect(toSessionError(llm(new RateLimitReason({ message: "rate", retryAfterMs: 123 })))).toEqual({
  26. type: "provider.rate-limit",
  27. message: "rate",
  28. })
  29. expect(toSessionError(llm(new AuthenticationReason({ message: "auth", kind: "invalid" }))).type).toBe(
  30. "provider.auth",
  31. )
  32. expect(toSessionError(llm(new QuotaExceededReason({ message: "quota" }))).type).toBe("provider.quota")
  33. expect(toSessionError(llm(new ContentPolicyReason({ message: "blocked" }))).type).toBe("provider.content-filter")
  34. expect(toSessionError(llm(new TransportReason({ message: "transport" }))).type).toBe("provider.transport")
  35. expect(toSessionError(llm(new ProviderInternalReason({ message: "internal", status: 500 }))).type).toBe(
  36. "provider.internal",
  37. )
  38. expect(toSessionError(llm(new InvalidProviderOutputReason({ message: "output" }))).type).toBe(
  39. "provider.invalid-output",
  40. )
  41. expect(toSessionError(llm(new InvalidRequestReason({ message: "request" }))).type).toBe("provider.invalid-request")
  42. expect(
  43. toSessionError(
  44. llm(
  45. new NoRouteReason({
  46. route: "route",
  47. provider: ProviderID.make("provider"),
  48. model: ModelID.make("model"),
  49. }),
  50. ),
  51. ).type,
  52. ).toBe("provider.no-route")
  53. expect(toSessionError(llm(new UnknownProviderReason({ message: "unknown" }))).type).toBe("provider.unknown")
  54. })
  55. test("preserves the permission rejection type without exposing internal fields", () => {
  56. const blocked = new PermissionV2.BlockedError({ rules: [], permission: "external_directory", resources: [] })
  57. expect(toSessionError(blocked)).toEqual({
  58. type: "permission.rejected",
  59. message: "Permission denied: external_directory",
  60. })
  61. expect(toSessionError(new ToolFailure({ message: blocked.message, error: blocked }))).toEqual({
  62. type: "permission.rejected",
  63. message: "Permission denied: external_directory",
  64. })
  65. expect(toSessionError(new Tool.Failure({ message: "failed" }))).toEqual({
  66. type: "tool.execution",
  67. message: "failed",
  68. })
  69. })
  70. test("retries only rate limits, provider-internal failures, and transport failures", () => {
  71. const eligible = [
  72. llm(new RateLimitReason({ message: "rate" })),
  73. llm(new ProviderInternalReason({ message: "internal", status: 500 })),
  74. llm(new TransportReason({ message: "transport" })),
  75. ]
  76. const ineligible = [
  77. llm(new AuthenticationReason({ message: "auth", kind: "invalid" })),
  78. llm(new QuotaExceededReason({ message: "quota" })),
  79. llm(new ContentPolicyReason({ message: "blocked" })),
  80. llm(new InvalidProviderOutputReason({ message: "output" })),
  81. llm(new InvalidRequestReason({ message: "request" })),
  82. llm(new NoRouteReason({ route: "route", provider: ProviderID.make("provider"), model: ModelID.make("model") })),
  83. llm(new UnknownProviderReason({ message: "unknown" })),
  84. ]
  85. expect(eligible.map(SessionRunnerRetry.isRetryable)).toEqual([true, true, true])
  86. expect(ineligible.map(SessionRunnerRetry.isRetryable)).toEqual([false, false, false, false, false, false, false])
  87. })
  88. })