Просмотр исходного кода

refactor: replace error handling with serverErrorMessage utility and checks for if error is ConfigInvalidError (#14685)

OpeOginni 5 месяцев назад
Родитель
Сommit
cc02476ea5

+ 3 - 6
packages/app/src/context/global-sync.tsx

@@ -36,6 +36,7 @@ import type { ProjectMeta } from "./global-sync/types"
 import { SESSION_RECENT_LIMIT } from "./global-sync/types"
 import { SESSION_RECENT_LIMIT } from "./global-sync/types"
 import { sanitizeProject } from "./global-sync/utils"
 import { sanitizeProject } from "./global-sync/utils"
 import { usePlatform } from "./platform"
 import { usePlatform } from "./platform"
+import { formatServerError } from "@/utils/server-errors"
 
 
 type GlobalStore = {
 type GlobalStore = {
   ready: boolean
   ready: boolean
@@ -51,11 +52,6 @@ type GlobalStore = {
   reload: undefined | "pending" | "complete"
   reload: undefined | "pending" | "complete"
 }
 }
 
 
-function errorMessage(error: unknown) {
-  if (error instanceof Error && error.message) return error.message
-  if (typeof error === "string" && error) return error
-  return "Unknown error"
-}
 
 
 function createGlobalSync() {
 function createGlobalSync() {
   const globalSDK = useGlobalSDK()
   const globalSDK = useGlobalSDK()
@@ -207,8 +203,9 @@ function createGlobalSync() {
         console.error("Failed to load sessions", err)
         console.error("Failed to load sessions", err)
         const project = getFilename(directory)
         const project = getFilename(directory)
         showToast({
         showToast({
+          variant: "error",
           title: language.t("toast.session.listFailed.title", { project }),
           title: language.t("toast.session.listFailed.title", { project }),
-          description: errorMessage(err),
+          description: formatServerError(err),
         })
         })
       })
       })
 
 

+ 6 - 2
packages/app/src/context/global-sync/bootstrap.ts

@@ -16,6 +16,7 @@ import { batch } from "solid-js"
 import { reconcile, type SetStoreFunction, type Store } from "solid-js/store"
 import { reconcile, type SetStoreFunction, type Store } from "solid-js/store"
 import type { State, VcsCache } from "./types"
 import type { State, VcsCache } from "./types"
 import { cmp, normalizeProviderList } from "./utils"
 import { cmp, normalizeProviderList } from "./utils"
+import { formatServerError } from "@/utils/server-errors"
 
 
 type GlobalStore = {
 type GlobalStore = {
   ready: boolean
   ready: boolean
@@ -133,8 +134,11 @@ export async function bootstrapDirectory(input: {
   } catch (err) {
   } catch (err) {
     console.error("Failed to bootstrap instance", err)
     console.error("Failed to bootstrap instance", err)
     const project = getFilename(input.directory)
     const project = getFilename(input.directory)
-    const message = err instanceof Error ? err.message : String(err)
-    showToast({ title: `Failed to reload ${project}`, description: message })
+    showToast({ 
+      variant: "error", 
+      title: `Failed to reload ${project}`, 
+      description: formatServerError(err) 
+    })
     input.setStore("status", "partial")
     input.setStore("status", "partial")
     return
     return
   }
   }

+ 69 - 0
packages/app/src/utils/server-errors.test.ts

@@ -0,0 +1,69 @@
+import { describe, expect, test } from "bun:test"
+import type { ConfigInvalidError } from "./server-errors"
+import { formatServerError, parseReabaleConfigInvalidError } from "./server-errors"
+
+describe("parseReabaleConfigInvalidError", () => {
+  test("formats issues with file path", () => {
+    const error = {
+      name: "ConfigInvalidError",
+      data: {
+        path: "opencode.config.ts",
+        issues: [
+          { path: ["settings", "host"], message: "Required" },
+          { path: ["mode"], message: "Invalid" },
+        ],
+      },
+    } satisfies ConfigInvalidError
+
+    const result = parseReabaleConfigInvalidError(error)
+
+    expect(result).toBe(
+      ["Invalid configuration", "opencode.config.ts", "settings.host: Required", "mode: Invalid"].join("\n"),
+    )
+  })
+
+  test("uses trimmed message when issues are missing", () => {
+    const error = {
+      name: "ConfigInvalidError",
+      data: {
+        path: "config",
+        message: "  Bad value  ",
+      },
+    } satisfies ConfigInvalidError
+
+    const result = parseReabaleConfigInvalidError(error)
+
+    expect(result).toBe(["Invalid configuration", "Bad value"].join("\n"))
+  })
+})
+
+describe("formatServerError", () => {
+  test("formats config invalid errors", () => {
+    const error = {
+      name: "ConfigInvalidError",
+      data: {
+        message: "Missing host",
+      },
+    } satisfies ConfigInvalidError
+
+    const result = formatServerError(error)
+
+    expect(result).toBe(["Invalid configuration", "Missing host"].join("\n"))
+  })
+
+  test("returns error messages", () => {
+    expect(formatServerError(new Error("Request failed with status 503"))).toBe("Request failed with status 503")
+  })
+
+  test("returns provided string errors", () => {
+    expect(formatServerError("Failed to connect to server")).toBe("Failed to connect to server")
+  })
+
+  test("falls back to unknown", () => {
+    expect(formatServerError(0)).toBe("Unknown error")
+  })
+
+  test("falls back for unknown error objects and names", () => {
+    expect(formatServerError({ name: "ServerTimeoutError", data: { seconds: 30 } })).toBe("Unknown error")
+  })
+})

+ 32 - 0
packages/app/src/utils/server-errors.ts

@@ -0,0 +1,32 @@
+export type ConfigInvalidError = {
+  name: "ConfigInvalidError"
+  data: {
+    path?: string
+    message?: string
+    issues?: Array<{ message: string; path: string[] }>
+  }
+}
+
+export function formatServerError(error: unknown) {
+  if (isConfigInvalidErrorLike(error)) return parseReabaleConfigInvalidError(error)
+  if (error instanceof Error && error.message) return error.message
+  if (typeof error === "string" && error) return error
+  return "Unknown error"
+}
+
+function isConfigInvalidErrorLike(error: unknown): error is ConfigInvalidError {
+  if (typeof error !== "object" || error === null) return false
+  const o = error as Record<string, unknown>
+  return o.name === "ConfigInvalidError" && typeof o.data === "object" && o.data !== null
+}
+
+export function parseReabaleConfigInvalidError(errorInput: ConfigInvalidError) {
+  const head = "Invalid configuration"
+  const file = errorInput.data.path && errorInput.data.path !== "config" ? errorInput.data.path : ""
+  const detail = errorInput.data.message?.trim() ?? ""
+  const issues = (errorInput.data.issues ?? []).map((issue) => {
+    return `${issue.path.join(".")}: ${issue.message}`
+  })
+  if (issues.length) return [head, file, "", ...issues].filter(Boolean).join("\n")
+  return [head, file, detail].filter(Boolean).join("\n")
+}