import { afterEach, describe, expect, mock, test } from "bun:test" import { Effect, Layer, Option } from "effect" // Account.orgsByAccount() can fail with AccountServiceError when the // upstream Anthropic Console API is unreachable. The HTTP API used to // pipe the call through Effect.orDie, which converts the typed error // into a defect — surfacing as a 500 with the raw stack trace embedded // in the response body. // // The handlers now map the failure onto HttpApiError.InternalServerError // and the endpoints declare it as a typed error. Operators get a // structured 500 response with no stack-trace leak, and future error // middleware can recognize the failure type instead of seeing a defect. // // To force the failure path, mock @/account/account so its defaultLayer // provides an Account.Service whose orgsByAccount returns Effect.fail. const ORIG = await import("../../src/account/account") const failingAccountLayer = Layer.succeed( ORIG.Service, ORIG.Service.of({ active: () => Effect.succeed(Option.none()), activeOrg: () => Effect.succeed(Option.none()), list: () => Effect.succeed([]), orgsByAccount: () => Effect.fail(new ORIG.AccountServiceError({ message: "simulated upstream failure" })), remove: () => Effect.void, use: () => Effect.void, orgs: () => Effect.succeed([]), config: () => Effect.succeed(Option.none()), token: () => Effect.succeed(Option.none()), login: () => Effect.fail(new ORIG.AccountServiceError({ message: "unused" })), poll: () => Effect.fail(new ORIG.AccountServiceError({ message: "unused" })), }), ) const mocked = { ...ORIG, defaultLayer: failingAccountLayer, layer: failingAccountLayer, Account: { ...ORIG.Account, defaultLayer: failingAccountLayer, layer: failingAccountLayer, }, } void mock.module("@/account/account", () => mocked) void mock.module("../../src/account/account", () => mocked) const { Flag } = await import("@opencode-ai/core/flag/flag") const Log = await import("@opencode-ai/core/util/log") const { Server } = await import("../../src/server/server") const { ExperimentalPaths } = await import("../../src/server/routes/instance/httpapi/groups/experimental") const { resetDatabase } = await import("../fixture/db") const { disposeAllInstances, tmpdir } = await import("../fixture/fixture") void Log.init({ print: false }) const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI afterEach(async () => { Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original await disposeAllInstances() await resetDatabase() }) function httpApiApp() { Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true return Server.Default().app } async function probe(path: string) { await using tmp = await tmpdir({ config: { formatter: false, lsp: false } }) return httpApiApp().request(path, { headers: { "x-opencode-directory": tmp.path }, }) } describe("HTTP API account failure mapping", () => { test("/experimental/console returns a structured 500, not a stack-trace defect", async () => { const response = await probe(ExperimentalPaths.console) expect(response.status).toBe(500) const body = await response.text() expect(body).not.toContain("\n at ") const json = JSON.parse(body) expect(json._tag).toBe("InternalServerError") }) test("/experimental/console/orgs returns a structured 500, not a stack-trace defect", async () => { const response = await probe(ExperimentalPaths.consoleOrgs) expect(response.status).toBe(500) const body = await response.text() expect(body).not.toContain("\n at ") const json = JSON.parse(body) expect(json._tag).toBe("InternalServerError") }) })