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).
95 lines
3.8 KiB
TypeScript
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 } },
|
|
)
|
|
})
|