| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371 |
- 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<void>) | 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"
- )
- }
|