/** * Regression tests for the SDK error shape — the v2 SDK's `throwOnError: true` * path used to throw raw values (empty strings or POJOs from JSON-decoded * error bodies). The TUI catches those and `e.message`/`e.stack` are * undefined, so users see `[object Object]` or a blank crash. * * Both cases must throw a real `Error` instance with a non-empty `.message` * extracted from the response body, plus `.status` and `.body` attached. */ import { afterEach, describe, expect, test } from "bun:test" import { createOpencodeClient } from "@opencode-ai/sdk/v2" import { Server } from "../../src/server/server" import * as Log from "@opencode-ai/core/util/log" import { disposeAllInstances, tmpdir } from "../fixture/fixture" import { resetDatabase } from "../fixture/db" void Log.init({ print: false }) afterEach(async () => { await disposeAllInstances() await resetDatabase() }) function client(directory: string) { return createOpencodeClient({ baseUrl: "http://test", directory, fetch: ((req: Request) => Server.Default().app.fetch(req)) as unknown as typeof fetch, }) } describe("v2 SDK error shape", () => { test("404 with NamedError body throws a real Error carrying the server message", async () => { await using tmp = await tmpdir({ git: true, config: { formatter: false, lsp: false } }) const sdk = client(tmp.path) let caught: unknown try { await sdk.session.get({ sessionID: "ses_no_such" }, { throwOnError: true }) } catch (e) { caught = e } expect(caught).toBeInstanceOf(Error) const err = caught as Error const cause = err.cause as { body?: any; status?: number } expect(err.message).toContain("Session not found") expect(cause.status).toBe(404) expect(cause.body).toMatchObject({ name: "NotFoundError", data: { message: expect.stringContaining("Session not found") }, }) }) test("400 with empty body throws a real Error naming the status", async () => { await using tmp = await tmpdir({ config: { formatter: false, lsp: false } }) const sdk = client(tmp.path) let caught: unknown try { // POST /sync/history with `aggregate: -1` triggers schema validation // that returns an empty 400 body (verified via plan-mode probe). await sdk.sync.history.list({ aggregate: -1 } as any, { throwOnError: true }) } catch (e) { caught = e } expect(caught).toBeInstanceOf(Error) const err = caught as Error const cause = err.cause as { status?: number } expect(err.message.length).toBeGreaterThan(0) expect(cause.status).toBe(400) }) })