From 775321173d0e8571f3d4dfe2092f14729777acb3 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Mon, 25 May 2026 16:18:31 -0400 Subject: [PATCH] refactor(httpapi): describe bodyless global upgrade payload --- .../routes/instance/httpapi/groups/global.ts | 4 +- .../test/server/httpapi-global.test.ts | 63 +++++++++++++++++++ 2 files changed, 65 insertions(+), 2 deletions(-) create mode 100644 packages/opencode/test/server/httpapi-global.test.ts diff --git a/packages/opencode/src/server/routes/instance/httpapi/groups/global.ts b/packages/opencode/src/server/routes/instance/httpapi/groups/global.ts index 75441b4ca4..f50fd3351e 100644 --- a/packages/opencode/src/server/routes/instance/httpapi/groups/global.ts +++ b/packages/opencode/src/server/routes/instance/httpapi/groups/global.ts @@ -3,7 +3,7 @@ import { BusEvent } from "@/bus/bus-event" import { SyncEvent } from "@/sync" import "@/server/event" import { Schema } from "effect" -import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, OpenApi } from "effect/unstable/httpapi" +import { HttpApi, HttpApiEndpoint, HttpApiError, HttpApiGroup, HttpApiSchema, OpenApi } from "effect/unstable/httpapi" import { described } from "./metadata" const GlobalHealth = Schema.Struct({ @@ -92,7 +92,7 @@ export const GlobalApi = HttpApi.make("global").add( }), ), HttpApiEndpoint.post("upgrade", GlobalPaths.upgrade, { - payload: GlobalUpgradeInput, + payload: [HttpApiSchema.NoContent, GlobalUpgradeInput], success: described(GlobalUpgradeResult, "Upgrade result"), error: HttpApiError.BadRequest, }).annotateMerge( diff --git a/packages/opencode/test/server/httpapi-global.test.ts b/packages/opencode/test/server/httpapi-global.test.ts new file mode 100644 index 0000000000..7cb8ec2dc4 --- /dev/null +++ b/packages/opencode/test/server/httpapi-global.test.ts @@ -0,0 +1,63 @@ +import { NodeHttpServer } from "@effect/platform-node" +import { describe, expect } from "bun:test" +import { Context, Effect, Layer, Option } from "effect" +import { HttpBody, HttpClient, HttpClientRequest, HttpRouter } from "effect/unstable/http" +import { HttpApiBuilder } from "effect/unstable/httpapi" +import { Auth } from "../../src/auth" +import { Config } from "../../src/config/config" +import { Installation } from "../../src/installation" +import { ServerAuth } from "../../src/server/auth" +import { RootHttpApi } from "../../src/server/routes/instance/httpapi/api" +import { GlobalPaths } from "../../src/server/routes/instance/httpapi/groups/global" +import { controlHandlers } from "../../src/server/routes/instance/httpapi/handlers/control" +import { globalHandlers } from "../../src/server/routes/instance/httpapi/handlers/global" +import { authorizationLayer } from "../../src/server/routes/instance/httpapi/middleware/authorization" +import { schemaErrorLayer } from "../../src/server/routes/instance/httpapi/middleware/schema-error" +import { testEffect } from "../lib/effect" + +const apiLayer = HttpRouter.serve( + HttpApiBuilder.layer(RootHttpApi).pipe( + Layer.provide([controlHandlers, globalHandlers]), + Layer.provide([authorizationLayer, schemaErrorLayer]), + ), + { disableListenLog: true, disableLogger: true }, +).pipe( + Layer.provideMerge(NodeHttpServer.layerTest), + Layer.provide(Layer.mock(Auth.Service)({})), + Layer.provide(Layer.mock(Config.Service)({})), + Layer.provide( + Layer.mock(Installation.Service)({ + method: () => Effect.succeed("npm"), + latest: () => Effect.succeed("9.9.9"), + upgrade: () => Effect.void, + }), + ), + Layer.provide(ServerAuth.Config.layer({ password: Option.none(), username: "opencode" })), + // Raw HttpApi routes expose an opaque handler context at the web boundary. + // oxlint-disable-next-line typescript-eslint/no-unsafe-type-assertion + Layer.provide(Layer.succeedContext(Context.empty() as Context.Context)), +) +const it = testEffect(apiLayer) + +describe("global HttpApi", () => { + it.live("upgrades to latest when the request body is omitted", () => + Effect.gen(function* () { + const response = yield* HttpClient.post(GlobalPaths.upgrade) + + expect(response.status).toBe(200) + expect(yield* response.json).toEqual({ success: true, version: "9.9.9" }) + }), + ) + + it.live("rejects malformed upgrade payloads", () => + Effect.gen(function* () { + const response = yield* HttpClientRequest.post(GlobalPaths.upgrade).pipe( + HttpClientRequest.setBody(HttpBody.text("{", "application/json")), + HttpClient.execute, + ) + + expect(response.status).toBe(400) + expect(yield* response.json).toEqual({ success: false, error: "Invalid request body" }) + }), + ) +})