|
| 1 | +import { afterEach, describe, expect, mock, test } from "bun:test" |
| 2 | +import { Effect, Layer, Option } from "effect" |
| 3 | + |
| 4 | +// Account.orgsByAccount() can fail with AccountServiceError when the |
| 5 | +// upstream Anthropic Console API is unreachable. The HTTP API used to |
| 6 | +// pipe the call through Effect.orDie, which converts the typed error |
| 7 | +// into a defect — surfacing as a 500 with the raw stack trace embedded |
| 8 | +// in the response body. |
| 9 | +// |
| 10 | +// The handlers now map the failure onto HttpApiError.InternalServerError |
| 11 | +// and the endpoints declare it as a typed error. Operators get a |
| 12 | +// structured 500 response with no stack-trace leak, and future error |
| 13 | +// middleware can recognize the failure type instead of seeing a defect. |
| 14 | +// |
| 15 | +// To force the failure path, mock @/account/account so its defaultLayer |
| 16 | +// provides an Account.Service whose orgsByAccount returns Effect.fail. |
| 17 | + |
| 18 | +const ORIG = await import("../../src/account/account") |
| 19 | + |
| 20 | +const failingAccountLayer = Layer.succeed( |
| 21 | + ORIG.Service, |
| 22 | + ORIG.Service.of({ |
| 23 | + active: () => Effect.succeed(Option.none()), |
| 24 | + activeOrg: () => Effect.succeed(Option.none()), |
| 25 | + list: () => Effect.succeed([]), |
| 26 | + orgsByAccount: () => |
| 27 | + Effect.fail(new ORIG.AccountServiceError({ message: "simulated upstream failure" })), |
| 28 | + remove: () => Effect.void, |
| 29 | + use: () => Effect.void, |
| 30 | + orgs: () => Effect.succeed([]), |
| 31 | + config: () => Effect.succeed(Option.none()), |
| 32 | + token: () => Effect.succeed(Option.none()), |
| 33 | + login: () => Effect.fail(new ORIG.AccountServiceError({ message: "unused" })), |
| 34 | + poll: () => Effect.fail(new ORIG.AccountServiceError({ message: "unused" })), |
| 35 | + }), |
| 36 | +) |
| 37 | + |
| 38 | +const mocked = { |
| 39 | + ...ORIG, |
| 40 | + defaultLayer: failingAccountLayer, |
| 41 | + layer: failingAccountLayer, |
| 42 | + Account: { |
| 43 | + ...ORIG.Account, |
| 44 | + defaultLayer: failingAccountLayer, |
| 45 | + layer: failingAccountLayer, |
| 46 | + }, |
| 47 | +} |
| 48 | + |
| 49 | +void mock.module("@/account/account", () => mocked) |
| 50 | +void mock.module("../../src/account/account", () => mocked) |
| 51 | + |
| 52 | +const { Flag } = await import("@opencode-ai/core/flag/flag") |
| 53 | +const Log = await import("@opencode-ai/core/util/log") |
| 54 | +const { Server } = await import("../../src/server/server") |
| 55 | +const { ExperimentalPaths } = await import("../../src/server/routes/instance/httpapi/groups/experimental") |
| 56 | +const { resetDatabase } = await import("../fixture/db") |
| 57 | +const { disposeAllInstances, tmpdir } = await import("../fixture/fixture") |
| 58 | + |
| 59 | +void Log.init({ print: false }) |
| 60 | + |
| 61 | +const original = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI |
| 62 | + |
| 63 | +afterEach(async () => { |
| 64 | + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = original |
| 65 | + await disposeAllInstances() |
| 66 | + await resetDatabase() |
| 67 | +}) |
| 68 | + |
| 69 | +function httpApiApp() { |
| 70 | + Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = true |
| 71 | + return Server.Default().app |
| 72 | +} |
| 73 | + |
| 74 | +async function probe(path: string) { |
| 75 | + await using tmp = await tmpdir({ config: { formatter: false, lsp: false } }) |
| 76 | + return httpApiApp().request(path, { |
| 77 | + headers: { "x-opencode-directory": tmp.path }, |
| 78 | + }) |
| 79 | +} |
| 80 | + |
| 81 | +describe("HTTP API account failure mapping", () => { |
| 82 | + test("/experimental/console returns a structured 500, not a stack-trace defect", async () => { |
| 83 | + const response = await probe(ExperimentalPaths.console) |
| 84 | + expect(response.status).toBe(500) |
| 85 | + const body = await response.text() |
| 86 | + expect(body).not.toContain("\n at ") |
| 87 | + const json = JSON.parse(body) |
| 88 | + expect(json._tag).toBe("InternalServerError") |
| 89 | + }) |
| 90 | + |
| 91 | + test("/experimental/console/orgs returns a structured 500, not a stack-trace defect", async () => { |
| 92 | + const response = await probe(ExperimentalPaths.consoleOrgs) |
| 93 | + expect(response.status).toBe(500) |
| 94 | + const body = await response.text() |
| 95 | + expect(body).not.toContain("\n at ") |
| 96 | + const json = JSON.parse(body) |
| 97 | + expect(json._tag).toBe("InternalServerError") |
| 98 | + }) |
| 99 | +}) |
0 commit comments