session-error.test.ts 3.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788
  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/llm"
  18. import { PermissionV2 } from "@opencode-ai/core/permission"
  19. import { toSessionError } from "@opencode-ai/core/session/to-session-error"
  20. import { SessionRunnerRetry } from "@opencode-ai/core/session/runner/retry"
  21. const llm = (reason: LLMError["reason"]) => new LLMError({ module: "test", method: "stream", reason })
  22. describe("toSessionError", () => {
  23. test("maps every LLM reason to the open wire type", () => {
  24. expect(toSessionError(llm(new RateLimitReason({ message: "rate", retryAfterMs: 123 })))).toEqual({
  25. type: "provider.rate-limit",
  26. message: "rate",
  27. })
  28. expect(toSessionError(llm(new AuthenticationReason({ message: "auth", kind: "invalid" }))).type).toBe(
  29. "provider.auth",
  30. )
  31. expect(toSessionError(llm(new QuotaExceededReason({ message: "quota" }))).type).toBe("provider.quota")
  32. expect(toSessionError(llm(new ContentPolicyReason({ message: "blocked" }))).type).toBe("provider.content-filter")
  33. expect(toSessionError(llm(new TransportReason({ message: "transport" }))).type).toBe("provider.transport")
  34. expect(toSessionError(llm(new ProviderInternalReason({ message: "internal", status: 500 }))).type).toBe(
  35. "provider.internal",
  36. )
  37. expect(toSessionError(llm(new InvalidProviderOutputReason({ message: "output" }))).type).toBe(
  38. "provider.invalid-output",
  39. )
  40. expect(toSessionError(llm(new InvalidRequestReason({ message: "request" }))).type).toBe("provider.invalid-request")
  41. expect(
  42. toSessionError(
  43. llm(
  44. new NoRouteReason({
  45. route: "route",
  46. provider: ProviderID.make("provider"),
  47. model: ModelID.make("model"),
  48. }),
  49. ),
  50. ).type,
  51. ).toBe("provider.no-route")
  52. expect(toSessionError(llm(new UnknownProviderReason({ message: "unknown" }))).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. })
  65. test("retries only rate limits, provider-internal failures, and transport failures", () => {
  66. const eligible = [
  67. llm(new RateLimitReason({ message: "rate" })),
  68. llm(new ProviderInternalReason({ message: "internal", status: 500 })),
  69. llm(new TransportReason({ message: "transport" })),
  70. ]
  71. const ineligible = [
  72. llm(new AuthenticationReason({ message: "auth", kind: "invalid" })),
  73. llm(new QuotaExceededReason({ message: "quota" })),
  74. llm(new ContentPolicyReason({ message: "blocked" })),
  75. llm(new InvalidProviderOutputReason({ message: "output" })),
  76. llm(new InvalidRequestReason({ message: "request" })),
  77. llm(new NoRouteReason({ route: "route", provider: ProviderID.make("provider"), model: ModelID.make("model") })),
  78. llm(new UnknownProviderReason({ message: "unknown" })),
  79. ]
  80. expect(eligible.map(SessionRunnerRetry.isRetryable)).toEqual([true, true, true])
  81. expect(ineligible.map(SessionRunnerRetry.isRetryable)).toEqual([false, false, false, false, false, false, false])
  82. })
  83. })