opencode/packages/tui/test/fixture/app.ts
2026-10-19 16:47:33 +02:00

88 lines
2.9 KiB
TypeScript

import { createTestRenderer, type TestRendererSetup } from "@opentui/core/testing"
import { Effect, FileSystem } from "effect"
import path from "node:path"
import { Global } from "@opencode/util/global"
import type { TuiInput } from "../../src/app"
import type { Config } from "../../src/config"
import { tmpdir } from "./fixture"
import { createEventStream, createFetch, type FetchHandler } from "./tui-client"
export async function createAppFixture(
input: {
width?: number
height?: number
state?: string
config?: Config.Info
args?: TuiInput["args"]
fetch?: FetchHandler
// A renderer whose owner already set up its terminal, such as the simulation renderer.
setup?: TestRendererSetup
} = {},
) {
const { run } = await import("../../src/app")
const root = await tmpdir()
const setup =
input.setup ??
(await createTestRenderer({
width: input.width ?? 100,
height: input.height ?? 30,
kittyKeyboard: true,
}))
if (!input.setup) await setup.renderer.setupTerminal()
setup.renderer.start()
const ready = Promise.withResolvers<void>()
const events = createEventStream()
const calls = createFetch(input.fetch, events)
// Updates apply to a copy, as a config file would, so preference writes persist for the rest of the test.
let config: Config.Info = structuredClone(input.config ?? { animations: false })
const server = Bun.serve({ port: 0, fetch: (request) => calls.fetch(request) })
const task = Effect.runPromise(
run({
app: { name: "test", version: "test", channel: "test" },
server: { endpoint: { url: server.url.toString() } },
config: {
get: async () => config,
update: async (mutate) => {
const next = structuredClone(config)
mutate(next)
config = next
return next
},
},
packages: { prepare: async () => ({ directory: "" }) },
terminalHandoff: async () => ({ renderer: setup.renderer, mode: "dark", complete: ready.resolve }),
args: input.args ?? {},
log: () => {},
}).pipe(
Effect.provide(
Global.layerWith({
home: path.join(root.path, "home"),
data: path.join(root.path, "data"),
cache: path.join(root.path, "cache"),
config: path.join(root.path, "config"),
state: input.state ?? path.join(root.path, "state"),
bin: path.join(root.path, "cache", "bin"),
log: path.join(root.path, "data", "log"),
repos: path.join(root.path, "data", "repos"),
tmp: path.join(root.path, "tmp"),
}),
),
Effect.provide(FileSystem.layerNoop({})),
),
)
return {
...setup,
events,
config: () => config,
ready: ready.promise,
async [Symbol.asyncDispose]() {
try {
if (!setup.renderer.isDestroyed) setup.renderer.destroy()
await task
} finally {
await server.stop()
await root[Symbol.asyncDispose]()
}
},
}
}