opencode/packages/opencode/test/server/httpapi-event.test.ts
Kit Langton ddbd119dcb test: drop AppRuntime usage from tests
Removes all direct AppRuntime usage from packages/opencode/test —
`grep -r AppRuntime packages/opencode/test` now returns nothing.

Two patterns are applied:

1. Event tests rewritten in the httpapi-cors.test.ts style. The two
   /event SSE tests now serve HttpApiApp.routes on
   NodeHttpServer.layerTest and hit them via HttpClient. Pub/sub
   identity with the in-process routes is preserved via a new opt-in
   `testEffectShared` (in test/lib/effect.ts) that builds the test
   layer through the shared process-wide memoMap so Bus.defaultLayer
   resolves to the same Bus.Service the routes subscribed to.

   The SSE reader helpers move to test/lib/sse.ts and use HttpClient +
   Effect.Stream + Queue<SseEvent>.

   The D7 diagnostic case is removed: the AppRuntime-vs-test-runtime
   distinction it diagnosed no longer exists.

2. Surgical swap in the remaining four files
   (provider/{amazon-bedrock,provider}, session/llm,
   control-plane/workspace). Each `AppRuntime.runPromise(...)` becomes a
   module-level `ManagedRuntime.make(Service.defaultLayer, { memoMap })`.
   The shared memoMap preserves service identity, so behavior is
   unchanged.

Tests: event (9/9), amazon-bedrock (19/19), provider (84/84),
workspace (35/35); 147 pass across the 5 affected files. The 3
pre-existing failures in session/llm.test.ts are independent of this
change (verified by stashing the diff).
2026-05-20 20:50:32 -04:00

95 lines
3.8 KiB
TypeScript

import { NodeHttpServer, NodeServices } from "@effect/platform-node"
import { afterEach, describe, expect } from "bun:test"
import { Config, Effect, Layer, Queue } from "effect"
import { HttpClient, HttpClientRequest, HttpRouter, HttpServer } from "effect/unstable/http"
import * as Socket from "effect/unstable/socket/Socket"
import * as Log from "@opencode-ai/core/util/log"
import { Bus } from "../../src/bus"
import { Event as ServerEvent } from "../../src/server/event"
import { EventPaths } from "../../src/server/routes/instance/httpapi/groups/event"
import { HttpApiApp } from "../../src/server/routes/instance/httpapi/server"
import { resetDatabase } from "../fixture/db"
import { disposeAllInstances, TestInstance } from "../fixture/fixture"
import { testEffectShared } from "../lib/effect"
import { openInstanceEventStream, readNextEvent } from "../lib/sse"
void Log.init({ print: false })
afterEach(async () => {
await disposeAllInstances()
await resetDatabase()
})
const servedRoutes: Layer.Layer<never, Config.ConfigError, HttpServer.HttpServer> = HttpRouter.serve(
HttpApiApp.routes,
{ disableListenLog: true, disableLogger: true },
)
const it = testEffectShared(
Layer.mergeAll(
Bus.defaultLayer,
servedRoutes.pipe(
Layer.provide(Socket.layerWebSocketConstructorGlobal),
Layer.provideMerge(NodeHttpServer.layerTest),
Layer.provideMerge(NodeServices.layer),
),
),
)
describe("event HttpApi", () => {
it.instance(
"serves event stream with correct headers and initial server.connected",
() =>
Effect.gen(function* () {
const { directory } = yield* TestInstance
const response = yield* HttpClientRequest.get(EventPaths.event).pipe(
HttpClientRequest.setHeader("x-opencode-directory", directory),
HttpClient.execute,
)
expect(response.status).toBe(200)
expect(response.headers["content-type"]).toContain("text/event-stream")
expect(response.headers["cache-control"]).toBe("no-cache, no-transform")
expect(response.headers["x-accel-buffering"]).toBe("no")
expect(response.headers["x-content-type-options"]).toBe("nosniff")
// Read one event to also verify the stream is wired up; the response is
// scope-bound so the connection closes when the test ends.
const events = yield* openInstanceEventStream(directory)
expect(yield* readNextEvent(events)).toMatchObject({ type: "server.connected", properties: {} })
}),
{ git: true, config: { formatter: false, lsp: false } },
)
it.instance(
"keeps the event stream open after the initial event",
() =>
Effect.gen(function* () {
const { directory } = yield* TestInstance
const events = yield* openInstanceEventStream(directory)
expect(yield* readNextEvent(events)).toMatchObject({ type: "server.connected", properties: {} })
// If no second event arrives within 250ms, the stream is still open.
const status = yield* Queue.take(events).pipe(
Effect.map(() => "event" as const),
Effect.timeoutOrElse({ duration: "250 millis", orElse: () => Effect.succeed("open" as const) }),
)
expect(status).toBe("open")
}),
{ git: true, config: { formatter: false, lsp: false } },
)
it.instance(
"delivers instance bus events after the initial event",
() =>
Effect.gen(function* () {
const { directory } = yield* TestInstance
const events = yield* openInstanceEventStream(directory)
expect(yield* readNextEvent(events)).toMatchObject({ type: "server.connected", properties: {} })
yield* Bus.Service.use((svc) => svc.publish(ServerEvent.Connected, {}))
expect(yield* readNextEvent(events)).toMatchObject({ type: "server.connected", properties: {} })
}),
{ git: true, config: { formatter: false, lsp: false } },
)
})