| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141 |
- import { spawn } from "node:child_process"
- import fs from "node:fs/promises"
- import net from "node:net"
- import os from "node:os"
- import path from "node:path"
- import { fileURLToPath } from "node:url"
- type Handle = {
- url: string
- stop: () => Promise<void>
- }
- function freePort() {
- return new Promise<number>((resolve, reject) => {
- const server = net.createServer()
- server.once("error", reject)
- server.listen(0, () => {
- const address = server.address()
- if (!address || typeof address === "string") {
- server.close(() => reject(new Error("Failed to acquire a free port")))
- return
- }
- server.close((err) => {
- if (err) reject(err)
- else resolve(address.port)
- })
- })
- })
- }
- async function waitForHealth(url: string, probe = "/global/health") {
- const end = Date.now() + 120_000
- let last = ""
- while (Date.now() < end) {
- try {
- const res = await fetch(`${url}${probe}`)
- if (res.ok) return
- last = `status ${res.status}`
- } catch (err) {
- last = err instanceof Error ? err.message : String(err)
- }
- await new Promise((resolve) => setTimeout(resolve, 250))
- }
- throw new Error(`Timed out waiting for backend health at ${url}${probe}${last ? ` (${last})` : ""}`)
- }
- function done(proc: ReturnType<typeof spawn>) {
- return proc.exitCode !== null || proc.signalCode !== null
- }
- async function waitExit(proc: ReturnType<typeof spawn>, timeout = 10_000) {
- if (done(proc)) return
- await Promise.race([
- new Promise<void>((resolve) => proc.once("exit", () => resolve())),
- new Promise<void>((resolve) => setTimeout(resolve, timeout)),
- ])
- }
- const LOG_CAP = 100
- function cap(input: string[]) {
- if (input.length > LOG_CAP) input.splice(0, input.length - LOG_CAP)
- }
- function tail(input: string[]) {
- return input.slice(-40).join("")
- }
- export async function startBackend(label: string, input?: { llmUrl?: string }): Promise<Handle> {
- const port = await freePort()
- const sandbox = await fs.mkdtemp(path.join(os.tmpdir(), `opencode-e2e-${label}-`))
- const appDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
- const repoDir = path.resolve(appDir, "../..")
- const opencodeDir = path.join(repoDir, "packages", "opencode")
- const env = {
- ...process.env,
- OPENCODE_DISABLE_LSP_DOWNLOAD: "true",
- OPENCODE_DISABLE_DEFAULT_PLUGINS: "true",
- OPENCODE_EXPERIMENTAL_DISABLE_FILEWATCHER: "true",
- OPENCODE_TEST_HOME: path.join(sandbox, "home"),
- XDG_DATA_HOME: path.join(sandbox, "share"),
- XDG_CACHE_HOME: path.join(sandbox, "cache"),
- XDG_CONFIG_HOME: path.join(sandbox, "config"),
- XDG_STATE_HOME: path.join(sandbox, "state"),
- OPENCODE_CLIENT: "app",
- OPENCODE_STRICT_CONFIG_DEPS: "true",
- OPENCODE_E2E_LLM_URL: input?.llmUrl,
- } satisfies Record<string, string | undefined>
- const out: string[] = []
- const err: string[] = []
- const proc = spawn(
- "bun",
- ["run", "--conditions=browser", "./src/index.ts", "serve", "--port", String(port), "--hostname", "127.0.0.1"],
- {
- cwd: opencodeDir,
- env,
- stdio: ["ignore", "pipe", "pipe"],
- },
- )
- proc.stdout?.on("data", (chunk) => {
- out.push(String(chunk))
- cap(out)
- })
- proc.stderr?.on("data", (chunk) => {
- err.push(String(chunk))
- cap(err)
- })
- const url = `http://127.0.0.1:${port}`
- try {
- await waitForHealth(url)
- } catch (error) {
- proc.kill("SIGTERM")
- await fs.rm(sandbox, { recursive: true, force: true }).catch(() => undefined)
- throw new Error(
- [
- `Failed to start isolated e2e backend for ${label}`,
- error instanceof Error ? error.message : String(error),
- tail(out),
- tail(err),
- ]
- .filter(Boolean)
- .join("\n"),
- )
- }
- return {
- url,
- async stop() {
- if (!done(proc)) {
- proc.kill("SIGTERM")
- await waitExit(proc)
- }
- if (!done(proc)) {
- proc.kill("SIGKILL")
- await waitExit(proc)
- }
- await fs.rm(sandbox, { recursive: true, force: true }).catch(() => undefined)
- },
- }
- }
|