server-errors.test.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175
  1. import { describe, expect, test } from "bun:test"
  2. import type { SessionNotFoundError } from "@opencode-ai/sdk/v2/client"
  3. import type { ConfigInvalidError, ProviderModelNotFoundError } from "./server-errors"
  4. import { formatServerError, isSessionNotFoundError, parseReadableConfigInvalidError } from "./server-errors"
  5. function fill(text: string, vars?: Record<string, string | number>) {
  6. if (!vars) return text
  7. return text.replace(/{{\s*(\w+)\s*}}/g, (_, key: string) => {
  8. const value = vars[key]
  9. if (value === undefined) return ""
  10. return String(value)
  11. })
  12. }
  13. function useLanguageMock() {
  14. const dict: Record<string, string> = {
  15. "error.chain.unknown": "Erro desconhecido",
  16. "error.chain.configInvalid": "Arquivo de config em {{path}} invalido",
  17. "error.chain.configInvalidWithMessage": "Arquivo de config em {{path}} invalido: {{message}}",
  18. "error.chain.modelNotFound": "Modelo nao encontrado: {{provider}}/{{model}}",
  19. "error.chain.didYouMean": "Voce quis dizer: {{suggestions}}",
  20. "error.chain.checkConfig": "Revise provider/model no config",
  21. }
  22. return {
  23. t(key: string, vars?: Record<string, string | number>) {
  24. const text = dict[key]
  25. if (!text) return key
  26. return fill(text, vars)
  27. },
  28. }
  29. }
  30. const language = useLanguageMock()
  31. describe("parseReadableConfigInvalidError", () => {
  32. test("formats issues with file path", () => {
  33. const error = {
  34. name: "ConfigInvalidError",
  35. data: {
  36. path: "opencode.config.ts",
  37. issues: [
  38. { path: ["settings", "host"], message: "Required" },
  39. { path: ["mode"], message: "Invalid" },
  40. ],
  41. },
  42. } satisfies ConfigInvalidError
  43. const result = parseReadableConfigInvalidError(error, language.t)
  44. expect(result).toBe(
  45. ["Arquivo de config em opencode.config.ts invalido: settings.host: Required", "mode: Invalid"].join("\n"),
  46. )
  47. })
  48. test("uses trimmed message when issues are missing", () => {
  49. const error = {
  50. name: "ConfigInvalidError",
  51. data: {
  52. path: "config",
  53. message: " Bad value ",
  54. },
  55. } satisfies ConfigInvalidError
  56. const result = parseReadableConfigInvalidError(error, language.t)
  57. expect(result).toBe("Arquivo de config em config invalido: Bad value")
  58. })
  59. })
  60. describe("formatServerError", () => {
  61. test("formats config invalid errors", () => {
  62. const error = {
  63. name: "ConfigInvalidError",
  64. data: {
  65. message: "Missing host",
  66. },
  67. } satisfies ConfigInvalidError
  68. const result = formatServerError(error, language.t)
  69. expect(result).toBe("Arquivo de config em config invalido: Missing host")
  70. })
  71. test("returns error messages", () => {
  72. expect(formatServerError(new Error("Request failed with status 503"), language.t)).toBe(
  73. "Request failed with status 503",
  74. )
  75. })
  76. test("returns provided string errors", () => {
  77. expect(formatServerError("Failed to connect to server", language.t)).toBe("Failed to connect to server")
  78. })
  79. test("uses translated unknown fallback", () => {
  80. expect(formatServerError(0, language.t)).toBe("Erro desconhecido")
  81. })
  82. test("falls back for unknown error objects and names", () => {
  83. expect(formatServerError({ name: "ServerTimeoutError", data: { seconds: 30 } }, language.t)).toBe(
  84. "Erro desconhecido",
  85. )
  86. })
  87. test("formats provider model errors using provider/model", () => {
  88. const error = {
  89. name: "ProviderModelNotFoundError",
  90. data: {
  91. providerID: "openai",
  92. modelID: "gpt-4.1",
  93. },
  94. } satisfies ProviderModelNotFoundError
  95. expect(formatServerError(error, language.t)).toBe(
  96. ["Modelo nao encontrado: openai/gpt-4.1", "Revise provider/model no config"].join("\n"),
  97. )
  98. })
  99. test("formats provider model suggestions", () => {
  100. const error = {
  101. name: "ProviderModelNotFoundError",
  102. data: {
  103. providerID: "x",
  104. modelID: "y",
  105. suggestions: ["x/y2", "x/y3"],
  106. },
  107. } satisfies ProviderModelNotFoundError
  108. expect(formatServerError(error, language.t)).toBe(
  109. ["Modelo nao encontrado: x/y", "Voce quis dizer: x/y2, x/y3", "Revise provider/model no config"].join("\n"),
  110. )
  111. })
  112. test("unwraps SDK-wrapped errors from cause.body", () => {
  113. const body = {
  114. name: "ConfigInvalidError",
  115. data: {
  116. message: "Missing host",
  117. },
  118. } satisfies ConfigInvalidError
  119. const wrapped = new Error("ConfigInvalidError", { cause: { body, status: 400 } })
  120. expect(formatServerError(wrapped, language.t)).toBe("Arquivo de config em config invalido: Missing host")
  121. })
  122. })
  123. describe("isSessionNotFoundError", () => {
  124. test("matches an SDK-wrapped error for the requested session", () => {
  125. const body = {
  126. _tag: "SessionNotFoundError",
  127. sessionID: "ses_missing",
  128. message: "Session not found",
  129. } satisfies SessionNotFoundError
  130. expect(isSessionNotFoundError(new Error(body.message, { cause: { body, status: 404 } }), body.sessionID)).toBe(true)
  131. })
  132. test("rejects errors for other sessions and other 404 responses", () => {
  133. const body = {
  134. _tag: "SessionNotFoundError",
  135. sessionID: "ses_parent",
  136. message: "Session not found",
  137. } satisfies SessionNotFoundError
  138. expect(isSessionNotFoundError(new Error(body.message, { cause: { body, status: 404 } }), "ses_tab")).toBe(false)
  139. expect(
  140. isSessionNotFoundError(
  141. new Error("Provider not found", {
  142. cause: { body: { _tag: "ProviderNotFoundError", providerID: "missing" }, status: 404 },
  143. }),
  144. "ses_tab",
  145. ),
  146. ).toBe(false)
  147. })
  148. })