import { Effect } from "effect" import { defineScript, Llm } from "opencode-drive" import { mkdir } from "node:fs/promises" import path from "node:path" export default defineScript({ launch: "manual", config: { autoupdate: false }, run: ({ artifacts, llm, server }) => Effect.gen(function* () { yield* Effect.promise(() => configureServicePort(artifacts)) yield* server.launch() const registration = yield* Effect.promise(() => serviceRegistration(artifacts)) const root = path.resolve(import.meta.dir, "../../../..") const preload = Bun.resolveSync("@opentui/solid/preload", path.join(root, "packages/cli")) const session = `mini-stage2-${process.pid}` const snapshots = path.join(artifacts, "mini-stage2") const explicitDirectory = path.join(artifacts, "explicit-model") yield* Effect.promise(() => Promise.all([snapshots, explicitDirectory].map((dir) => mkdir(dir, { recursive: true })))) /** @param {string} directory @param {string | undefined} model */ const mini = (directory, model) => [ "env", `PWD=${directory}`, `OPENCODE_PASSWORD=${registration.password}`, `OPENCODE_CONFIG_DIR=${path.join(artifacts, "files/.opencode")}`, `OPENCODE_TEST_HOME=${artifacts}`, `XDG_CACHE_HOME=${path.join(artifacts, "home/.cache")}`, `XDG_CONFIG_HOME=${path.join(artifacts, "home/.config")}`, `XDG_DATA_HOME=${path.join(artifacts, "logs")}`, `XDG_STATE_HOME=${path.join(artifacts, "home/.local/state")}`, "OPENCODE_DISABLE_AUTOUPDATE=1", "OPENCODE_DIRECT_TRACE=1", process.execPath, "--conditions=browser", `--preload=${preload}`, path.join(root, "packages/cli/src/index.ts"), "mini", "--server", registration.url, ...(model ? ["--model", model] : []), ] yield* llm.queue( Llm.toolCall({ index: 0, id: "mini-shell", name: "shell", input: { command: "printf 'drive-mini-tool-output\\n'" }, }), Llm.finish("tool-calls"), ) yield* llm.queue(Llm.text("drive mini response complete", { delay: 5, chunkSize: 4 })) const journey = Effect.gen(function* () { yield* Effect.uninterruptible( Effect.promise(() => tmux([ "new-session", "-d", "-s", session, "-x", "140", "-y", "30", "--", ...mini(path.join(artifacts, "files"), undefined), ]), ), ) yield* Effect.promise(() => tmux(["set-option", "-t", session, "remain-on-exit", "on"])) const first = yield* Effect.promise(() => waitForPane(session, "OpenCode")) yield* Effect.promise(() => Bun.write(path.join(snapshots, "01-first-paint.txt"), first)) if (first.includes("drive mini response complete")) throw new Error("response rendered before prompt submission") yield* Effect.promise(() => waitForPane(session, "Default model", 15_000)) yield* Effect.promise(() => tmux(["send-keys", "-t", session, "C-p"])) yield* Effect.promise(() => waitForVisiblePane(session, "Commands")) yield* Effect.promise(() => tmux(["send-keys", "-t", session, "-l", "model"])) yield* Effect.promise(() => waitForVisiblePane(session, "Switch model")) yield* Effect.promise(() => tmux(["send-keys", "-H", "-t", session, "0d"])) yield* Effect.promise(() => waitForVisiblePane(session, "Select model")) yield* Effect.promise(() => waitForVisiblePane(session, "Simulated Model", 15_000)) yield* Effect.promise(() => tmux(["send-keys", "-t", session, "Escape"])) yield* Effect.promise(() => waitForVisiblePane(session, "Ask anything...")) yield* Effect.promise(() => tmux(["send-keys", "-t", session, "-l", "exercise the mini frontend"])) yield* Effect.sleep(100) yield* Effect.promise(() => tmux(["send-keys", "-H", "-t", session, "0d"])) const completed = yield* Effect.promise(() => waitForPane(session, "drive mini response complete", 20_000)) if (!completed.includes("drive-mini-tool-output")) throw new Error("shell tool output was not rendered") yield* Effect.promise(() => Bun.write(path.join(snapshots, "02-tool-and-response.txt"), completed)) yield* Effect.sleep(500) const resizeOutput = path.join(snapshots, "03-resize-output.ansi") yield* Effect.promise(() => tmux(["pipe-pane", "-t", session, `cat > ${JSON.stringify(resizeOutput)}`])) yield* Effect.promise(() => tmux(["resize-window", "-t", session, "-x", "72", "-y", "22"])) yield* Effect.promise(() => waitForFile( resizeOutput, (value) => value.includes("drive mini response complete") && value.includes("drive-mini-tool-output"), ), ) yield* Effect.promise(() => tmux(["pipe-pane", "-t", session])) const resized = yield* Effect.promise(() => captureVisiblePane(session)) if (!resized.includes("drive-mini-tool-output")) throw new Error("resize replay lost shell tool output") yield* Effect.promise(() => Bun.write(path.join(snapshots, "03-resize-replay.txt"), resized)) yield* llm.queue( Llm.toolCall({ index: 0, id: "mini-question", name: "question", input: { questions: [ { header: "Drive form", question: "Choose the Mini Form answer", options: [{ label: "Accepted", description: "Continue the run" }], multiple: false, }, ], }, }), Llm.finish("tool-calls"), ) yield* llm.queue(Llm.text("drive mini form complete")) yield* Effect.promise(() => tmux(["send-keys", "-t", session, "-l", "exercise the form"])) yield* Effect.promise(() => tmux(["send-keys", "-H", "-t", session, "0d"])) yield* Effect.promise(() => waitForPane(session, "Choose the Mini Form answer", 20_000)) yield* Effect.promise(() => tmux(["send-keys", "-H", "-t", session, "0d"])) yield* Effect.promise(() => waitForPane(session, "drive mini form complete", 20_000)) yield* llm.queue( Llm.toolCall({ index: 0, id: "mini-slow-shell", name: "shell", input: { command: "sleep 10" }, }), Llm.finish("tool-calls"), ) yield* Effect.promise(() => tmux(["send-keys", "-t", session, "-l", "interrupt this turn"])) yield* Effect.sleep(100) yield* Effect.promise(() => tmux(["send-keys", "-H", "-t", session, "0d"])) yield* Effect.promise(() => waitForPane(session, "$ sleep 10")) yield* Effect.promise(() => tmux(["send-keys", "-t", session, "Escape"])) const armed = yield* Effect.promise(() => waitForPane(session, "esc again")) yield* Effect.promise(() => Bun.write(path.join(snapshots, "04-interrupt-armed.txt"), armed)) yield* Effect.promise(() => tmux(["send-keys", "-t", session, "Escape"])) const interrupted = yield* Effect.promise(() => waitForPane(session, "Step interrupted", 10_000)) yield* Effect.promise(() => Bun.write(path.join(snapshots, "05-interrupted.txt"), interrupted)) yield* Effect.promise(async () => { if (!(await paneAlive(session))) throw new Error("Mini exited while interrupting an active turn") }) yield* Effect.promise(() => tmux(["send-keys", "-t", session, "C-c"])) yield* Effect.promise(() => waitForPane(session, "EXIT Press ctrl+")) yield* Effect.promise(() => tmux(["send-keys", "-t", session, "C-c"])) yield* Effect.promise(() => waitForDeadPane(session)) const status = yield* Effect.promise(() => paneDeadStatus(session)) if (status !== 0) throw new Error(`Mini exited with status ${status}`) const exited = yield* Effect.promise(() => capturePane(session)) if (!exited.includes("Continue") || !exited.includes("opencode mini -s")) throw new Error("Mini exit splash was not rendered before teardown") yield* Effect.promise(() => Bun.write(path.join(snapshots, "06-exit-teardown.txt"), exited)) yield* Effect.promise(() => tmux(["clear-history", "-t", session])) yield* Effect.promise(() => tmux([ "respawn-pane", "-k", "-t", session, "--", ...mini(explicitDirectory, "simulation/gpt-sim-model"), ]), ) const explicitModel = yield* Effect.promise(() => waitForPane(session, "Simulated Model", 15_000)) yield* Effect.promise(() => Bun.write(path.join(snapshots, "07-explicit-model.txt"), explicitModel)) yield* Effect.promise(() => tmux(["send-keys", "-t", session, "C-c"])) yield* Effect.promise(() => waitForPane(session, "EXIT Press ctrl+")) yield* Effect.promise(() => tmux(["send-keys", "-t", session, "C-c"])) yield* Effect.promise(() => waitForDeadPane(session)) if ((yield* Effect.promise(() => paneDeadStatus(session))) !== 0) throw new Error("Explicit-model Mini did not exit cleanly") yield* Effect.promise(async () => { for (const failure of [ { args: ["--model", "simulation/definitely-missing"], capture: "08-unavailable-model.txt", expected: "Model unavailable: simulation/definitely-missing", }, { args: ["--agent", "definitely-missing"], capture: "09-unavailable-agent.txt", expected: 'Agent not found: "definitely-missing"', }, ]) { const child = Bun.spawn( [ process.execPath, path.join(root, "packages/cli/src/index.ts"), "run", "--server", registration.url, ...failure.args, "optimistic selection check", ], { cwd: path.join(root, "packages/cli"), env: { ...process.env, PWD: path.join(artifacts, "files"), OPENCODE_PASSWORD: registration.password, OPENCODE_CONFIG_DIR: path.join(artifacts, "files/.opencode"), OPENCODE_DISABLE_AUTOUPDATE: "1", }, stdin: "ignore", stdout: "pipe", stderr: "pipe", }, ) const [exitCode, stdout, stderr] = await Promise.all([ child.exited, new Response(child.stdout).text(), new Response(child.stderr).text(), ]) await Bun.write(path.join(snapshots, failure.capture), stdout + stderr) if (exitCode !== 1) throw new Error(`${failure.expected} run exited with status ${exitCode}`) if (!stderr.includes(failure.expected)) throw new Error(`Selection failure was not diagnosed by execution: ${stderr}`) } }) }) yield* journey.pipe(Effect.ensuring(Effect.promise(() => tmux(["kill-session", "-t", session], true)))) }), }) /** @param {string[]} args */ async function tmux(args, allowFailure = false) { const child = Bun.spawn(["tmux", ...args], { stdout: "pipe", stderr: "pipe" }) let timedOut = false const timeout = setTimeout(() => { timedOut = true child.kill("SIGKILL") }, 5_000) const [status, stdout, stderr] = await Promise.all([ child.exited, new Response(child.stdout).text(), new Response(child.stderr).text(), ]) clearTimeout(timeout) if (timedOut) throw new Error(`tmux ${args[0]} timed out`) if (status !== 0 && !allowFailure) throw new Error(`tmux ${args[0]} failed: ${stderr || stdout}`) return stdout } /** @param {string} session */ function capturePane(session) { return tmux(["capture-pane", "-p", "-t", session, "-S", "-"]) } /** @param {string} session */ function captureVisiblePane(session) { return tmux(["capture-pane", "-p", "-t", session]) } /** @param {string} session @param {string} text @param {number} [timeout] */ async function waitForVisiblePane(session, text, timeout = 5_000) { const deadline = Date.now() + timeout let last = "" while (Date.now() < deadline) { last = await captureVisiblePane(session) if (last.includes(text)) return last if (!(await paneAlive(session))) throw new Error(`Mini exited before rendering ${JSON.stringify(text)}:\n${last}`) await Bun.sleep(50) } throw new Error(`Timed out waiting for visible ${JSON.stringify(text)}:\n${last}`) } /** @param {string} session */ async function paneAlive(session) { return (await tmux(["display-message", "-p", "-t", session, "#{pane_dead}"], true)).trim() === "0" } /** @param {string} session */ async function paneDeadStatus(session) { return Number((await tmux(["display-message", "-p", "-t", session, "#{pane_dead_status}"])).trim()) } /** * @param {string} session * @param {string} text * @param {number} [timeout] * @param {(() => Promise) | undefined} [trigger] */ async function waitForPane(session, text, timeout = 5_000, trigger) { const deadline = Date.now() + timeout let last = "" while (Date.now() < deadline) { await trigger?.() last = await capturePane(session) if (last.includes(text)) return last if (!(await paneAlive(session))) throw new Error(`Mini exited before rendering ${JSON.stringify(text)}:\n${last}`) await Bun.sleep(50) } throw new Error(`Timed out waiting for ${JSON.stringify(text)}:\n${last}`) } /** @param {string} session */ async function waitForDeadPane(session) { for (let attempt = 0; attempt < 100; attempt++) { if (!(await paneAlive(session))) return await Bun.sleep(50) } throw new Error("Mini did not tear down after the exit sequence") } /** * @param {string} file * @param {(value: string) => boolean} accept */ async function waitForFile(file, accept) { let value = "" for (let attempt = 0; attempt < 100; attempt++) { value = await Bun.file(file) .text() .catch(() => "") if (accept(value)) return value await Bun.sleep(50) } throw new Error("resize did not replay committed transcript output") } /** @param {string} artifacts */ async function configureServicePort(artifacts) { const probe = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: () => new Response() }) const port = probe.port await probe.stop(true) if (!port) throw new Error("Failed to allocate a Drive service port") const file = path.join(artifacts, "files/.opencode/service-local.json") await mkdir(path.dirname(file), { recursive: true }) await Bun.write(file, JSON.stringify({ port })) } /** @param {string} artifacts */ async function serviceRegistration(artifacts) { const directory = path.join(artifacts, "home/.local/state/opencode") for (let attempt = 0; attempt < 200; attempt++) { for (const name of ["service-local.json", "service.json"]) { const value = await Bun.file(path.join(directory, name)) .json() .catch(() => undefined) if (isRegistration(value)) return value } await Bun.sleep(50) } throw new Error("Drive service registration was not written") } /** @param {unknown} value */ function isRegistration(value) { return ( typeof value === "object" && value !== null && "url" in value && typeof value.url === "string" && "password" in value && typeof value.password === "string" ) }