Files
2026-08-12 21:04:50 -04:00

1821 lines
68 KiB
TypeScript

import { afterEach, describe, expect, setDefaultTimeout, test } from "bun:test"
import { mkdir, mkdtemp, readdir, realpath, rm } from "node:fs/promises"
import { tmpdir } from "node:os"
import { basename, dirname, join, resolve } from "node:path"
import {
controlPath,
initializeManifest,
listInstances,
manifestPath,
register,
unregister,
type InstanceManifest,
} from "../../src/instance/registry.js"
import { createResponseSettings, generateResponse } from "../../src/cli/response-generator.js"
import { CommandBatchError, executeCommands } from "../../src/cli/commands.js"
import { splitText } from "../../src/cli/mock-backend.js"
import { resolveSendEndpoint } from "../../src/cli/send.js"
import { sendError, sendResult, startTransportPeer } from "../simulation/transport-peer.js"
const roots: string[] = []
const instances: Array<{ root: string; name: string }> = []
setDefaultTimeout(30_000)
afterEach(async () => {
await Promise.all(
instances.splice(0).map(async ({ root, name }) => {
await spawn(["stop", "--name", name], root).exited
}),
)
await Promise.all(roots.splice(0).map((root) => rm(root, { recursive: true, force: true })))
})
describe("opencode-drive", () => {
test("rejects unsupported optional UI commands without sending them", async () => {
const peers = [
startTransportPeer(
({ request, socket }) => {
if (request.method !== "simulation.handshake") return
const params = request.params as {
readonly requiredCapabilities: ReadonlyArray<string>
}
sendResult(socket, request, {
protocolVersion: 1,
role: "ui",
server: { name: "opencode", version: "older" },
capabilities: params.requiredCapabilities,
})
},
{ handshake: false },
),
startTransportPeer(
({ request, socket }) => {
if (request.method === "simulation.handshake") sendError(socket, request, "method not found", -32601)
},
{ handshake: false },
),
]
try {
for (const peer of peers) {
for (const expected of [
{
command: { operation: "ui.snapshot" as const },
method: "ui.snapshot",
message: "ui.snapshot is not available on this OpenCode endpoint",
},
{
command: {
operation: "ui.click" as const,
value: JSON.stringify({
target: 3,
x: 1,
y: 0,
semantic: {
id: "session.permission.action.once",
instance: "permission-1",
element: 3,
},
}),
},
method: "ui.click",
message: "semantic ui.click is not available on this OpenCode endpoint",
},
]) {
const result = executeCommands(peer.url, [expected.command])
await expect(result).rejects.toMatchObject({
name: "CommandBatchError",
reason: {
name: "SimulationError",
method: expected.method,
message: expected.message,
},
} satisfies Partial<CommandBatchError>)
}
expect(peer.received.map(({ request }) => request.method)).toEqual([
"simulation.handshake",
"simulation.handshake",
])
}
} finally {
await Promise.all(peers.map((peer) => peer.stop()))
}
})
test("requires an explicit name to initialize or start headless", async () => {
const root = await temporary()
for (const command of ["init", "start"]) {
const child = spawn([command], root)
expect(await child.exited).toBe(1)
expect(await new Response(child.stderr).text()).toContain("--name")
}
})
test("initializes artifacts and starts from the prepared instance", async () => {
const root = await temporary()
const name = "initialized-test"
const initialized = spawn(["init", "--name", name], root)
const [initStatus, initOutput] = await Promise.all([initialized.exited, new Response(initialized.stdout).text()])
expect(initStatus).toBe(0)
const artifacts = initOutput.trim()
expect(await Bun.file(join(artifacts, "files", ".opencode", "opencode.jsonc")).exists()).toBe(true)
expect(
Bun.JSONC.parse(await Bun.file(join(artifacts, "files", ".opencode", "opencode.jsonc")).text()),
).toMatchObject({
model: "simulation/gpt-sim-model",
snapshots: false,
permissions: [{ action: "*", resource: "*", effect: "allow" }],
providers: {
simulation: {
package: "@opencode-ai/ai/providers/openai/chat",
settings: { apiKey: "sim-key" },
models: { "gpt-sim-model": { capabilities: { tools: true } } },
},
},
})
expect(await Bun.file(join(artifacts, "drive", "name")).text()).toBe(`${name}\n`)
expect(await Bun.file(join(root, "registry", `${name}.json`)).json()).toMatchObject({
name,
artifacts,
status: "initialized",
})
const repeated = spawn(["init", "--name", name], root)
expect(await repeated.exited).toBe(0)
expect((await new Response(repeated.stdout).text()).trim()).toBe(artifacts)
const listed = spawn(["list"], root)
expect(await listed.exited).toBe(0)
expect(await new Response(listed.stdout).text()).toBe(`${name}: ${join(root, "registry", `${name}.json`)}\n`)
await Bun.write(join(artifacts, "prepared.txt"), "prepared before start\n")
const started = spawn(["start", "--name", name, "--", process.execPath, fixture("fake-opencode.ts")], root)
const [startStatus, startError] = await Promise.all([started.exited, new Response(started.stderr).text()])
expect(startStatus).toBe(0)
expect(startError).toContain(`opencode-drive: using artifacts ${artifacts}`)
instances.push({ root, name })
expect(await Bun.file(join(artifacts, "prepared.txt")).text()).toBe("prepared before start\n")
expect(await Bun.file(join(root, "registry", `${name}.json`)).json()).toMatchObject({
name,
artifacts,
status: "ready",
})
})
test("starts, drives, prints dir, restarts, and stops a named detached instance", async () => {
const root = await temporary()
const name = "detached-test"
const started = spawn(
["start", "--name", name, "--record", "--", process.execPath, fixture("fake-opencode.ts")],
root,
)
const [startStatus, startError] = await Promise.all([started.exited, new Response(started.stderr).text()])
expect(startStatus).toBe(0)
expect(startError).toContain("opencode-drive: using artifacts ")
const startArtifacts = artifactPath(startError)
expect(startError).toContain(
`opencode-drive: opencode instance logs: ${join(startArtifacts, "logs", "opencode", "log", "opencode*.log")}`,
)
expect(startError).toContain(
`opencode-drive: current run script logs: ${join(startArtifacts, "logs", "opencode-drive.log")}`,
)
expect(startError).not.toContain(`opencode-drive: ${name}`)
instances.push({ root, name })
const manifest = await Bun.file(join(root, "registry", `${name}.json`)).json()
expect(manifest.visible).toBe(false)
expect(manifest.endpoints.ui).toMatch(/^ws:\/\/127\.0\.0\.1:\d+$/)
expect(manifest.endpoints.backend).toMatch(/^ws:\/\/127\.0\.0\.1:\d+$/)
const state = spawn(["send", "--name", name, "--command.ui.state"], root)
expect(await state.exited).toBe(0)
expect(JSON.parse(await new Response(state.stdout).text()).focused.editor).toBe(true)
const snapshot = spawn(["send", "--name", name, "--command.ui.snapshot"], root)
expect(await snapshot.exited).toBe(0)
expect(JSON.parse(await new Response(snapshot.stdout).text())).toMatchObject({
format: "opencode-ui-snapshot-v1",
nodes: [{ id: "prompt", role: "textbox", element: 1 }],
})
const matches = spawn(["send", "--name", name, "--command.ui.matches", '{"text":"Fake OpenCode"}'], root)
expect(await matches.exited).toBe(0)
expect(await new Response(matches.stdout).text()).toBe("true\n")
const literal = spawn(["send", "--name", name, "--command.ui.matches", '{"text":"Fake.*OpenCode"}'], root)
expect(await literal.exited).toBe(0)
expect(await new Response(literal.stdout).text()).toBe("false\n")
const screenshot = spawn(["send", "--name", name, "--command.ui.screenshot"], root)
expect(await screenshot.exited).toBe(0)
const screenshotPath = (await new Response(screenshot.stdout).text()).trim()
expect(dirname(screenshotPath)).toBe(runOutputDirectory(root, manifest.artifacts, 0))
expect(basename(screenshotPath).startsWith("screenshot-")).toBe(true)
expect(screenshotPath.endsWith(".png")).toBe(true)
const listed = spawn(["dir", "--name", name], root)
expect(await listed.exited).toBe(0)
expect(await new Response(listed.stdout).text()).toBe(`${manifest.artifacts}\n`)
const driveLog = join(manifest.artifacts, "logs", "opencode-drive.log")
const driveLogText = await Bun.file(driveLog).text()
expect(driveLogText).toContain("INFO ready detached-test")
expect(driveLogText).toContain(
`INFO opencode instance logs: ${join(manifest.artifacts, "logs", "opencode", "log", "opencode*.log")}`,
)
expect(driveLogText).toContain(`INFO current run script logs: ${driveLog}`)
expect(driveLogText).toContain("INFO ui command ui.state params=undefined")
expect(driveLogText).toContain('INFO ui command ui.matches params={"text":"Fake OpenCode"}')
const restarted = spawn(["restart", "--name", name], root)
expect(await restarted.exited).toBe(0)
const restartedRecording = (await new Response(restarted.stdout).text()).trim()
expect(dirname(restartedRecording)).toBe(runOutputDirectory(root, manifest.artifacts, 0))
expect(basename(restartedRecording)).toMatch(/^recording-.*\.mp4$/)
expect(await Bun.file(restartedRecording).exists()).toBe(true)
await waitForLines(join(manifest.artifacts, "launches.txt"), 2)
expect(await spawn(["send", "--name", name, "--command.ui.state"], root).exited).toBe(0)
const stopped = spawn(["stop", "--name", name], root)
const [stoppedStatus, stoppedOutput, stoppedError] = await Promise.all([
stopped.exited,
new Response(stopped.stdout).text(),
new Response(stopped.stderr).text(),
])
expect(stoppedStatus).toBe(0)
expect(stoppedOutput).toBe("")
const stoppedRecording = stoppedError.match(/Video successfully created: (.+\.mp4)/)?.[1]
expect(stoppedRecording).toBeDefined()
expect(dirname(stoppedRecording!)).toBe(runOutputDirectory(root, manifest.artifacts, 1))
expect(basename(stoppedRecording!)).toMatch(/^recording-.*\.mp4$/)
expect(await Bun.file(stoppedRecording!).exists()).toBe(true)
expect(stoppedError).toContain("Rendering video: 10%")
expect(stoppedError).toContain("Rendering video: 100%")
expect(stoppedError).toContain(`Video successfully created: ${stoppedRecording}`)
expect(await Bun.file(join(root, "registry", `${name}.json`)).exists()).toBe(false)
expect(await Bun.file(manifest.artifacts).exists()).toBe(false)
instances.splice(
instances.findIndex((item) => item.name === name),
1,
)
}, 60_000)
test("exports a completed timeline after the OpenCode child exits", async () => {
const root = await temporary()
const name = "exited-recording-test"
const started = spawn(
["start", "--name", name, "--record", "--", process.execPath, fixture("fake-opencode.ts"), "exit-after-attach"],
root,
)
const [status, stderr] = await Promise.all([started.exited, new Response(started.stderr).text()])
expect(status, stderr).toBe(0)
const artifacts = stderr.match(/opencode-drive: using artifacts (.+)/)?.[1]
expect(artifacts).toBeDefined()
const deadline = Date.now() + 10_000
while (await Bun.file(join(root, "registry", `${name}.json`)).exists()) {
if (Date.now() >= deadline) throw new Error("timed out waiting for exited instance cleanup")
await Bun.sleep(25)
}
const directory = runOutputDirectory(root, artifacts!, 0)
const files = await readdir(directory)
const video = files.find((file) => file.endsWith(".mp4"))
expect(video).toBeDefined()
expect(await Bun.file(join(directory, video!)).size).toBeGreaterThan(500)
}, 15_000)
test("does not record unless start receives --record", async () => {
const root = await temporary()
const name = "no-recording-test"
expect(
await spawn(["start", "--name", name, "--", process.execPath, fixture("fake-opencode.ts")], root).exited,
).toBe(0)
instances.push({ root, name })
const manifest = await Bun.file(join(root, "registry", `${name}.json`)).json()
const drive = await Bun.file(join(manifest.artifacts, "drive", `${name}.json`)).json()
expect(drive.recording).toBeUndefined()
const stopped = spawn(["stop", "--name", name], root)
expect(await stopped.exited).toBe(0)
expect(await new Response(stopped.stdout).text()).toBe("success\n")
expect(await Bun.file(join(root, "output")).exists()).toBe(false)
expect(await Bun.file(manifest.artifacts).exists()).toBe(false)
instances.pop()
})
test("rejects duplicate names", async () => {
const root = await temporary()
const name = "duplicate-test"
const args = ["start", "--name", name, "--", process.execPath, fixture("fake-opencode.ts")]
expect(await spawn(args, root).exited).toBe(0)
instances.push({ root, name })
const duplicate = spawn(args, root)
const [status, stderr] = await Promise.all([duplicate.exited, new Response(duplicate.stderr).text()])
expect(status).toBe(1)
expect(stderr).toContain(`drive instance "${name}" is already running`)
})
test(
"only the owning detached launcher starts an automatic instance",
() => expectSingleConcurrentStart(false),
75_000,
)
test("only the owning detached launcher starts a prepared instance", () => expectSingleConcurrentStart(true), 75_000)
test("registers only one concurrent owner for a name", async () => {
const root = await temporary()
await withRegistry(root, async () => {
const results = await Promise.allSettled([
register(testManifest("racing", process.pid)),
register(testManifest("racing", process.pid)),
])
expect(results.filter((result) => result.status === "fulfilled")).toHaveLength(1)
expect(results.filter((result) => result.status === "rejected")).toHaveLength(1)
})
})
test("transfers temporary initialization only to its declared daemon", async () => {
const root = await temporary()
const owner = Bun.spawn([process.execPath, "-e", "await Bun.sleep(60_000)"])
try {
await withRegistry(root, async () => {
const name = "temporary-owner"
const artifacts = join(root, "artifacts")
await mkdir(join(root, "registry"), { recursive: true })
await Bun.write(
manifestPath(name),
`${JSON.stringify({
version: 1,
name,
createdAt: new Date().toISOString(),
cwd: root,
artifacts,
status: "initialized",
temporary: true,
pid: owner.pid,
})}\n`,
)
const create = async () => {
throw new Error("existing initialization was replaced")
}
await expect(initializeManifest(name, root, create, { temporary: true })).rejects.toThrow(
`drive instance "${name}" is already starting`,
)
owner.kill()
await owner.exited
expect(
await initializeManifest(name, root, create, {
temporary: true,
adoptPid: owner.pid,
}),
).toMatchObject({ pid: process.pid, artifacts })
await expect(register(testManifest(name, owner.pid))).rejects.toThrow(
`drive instance "${name}" changed ownership`,
)
})
} finally {
if (owner.exitCode === null) owner.kill()
await owner.exited
}
})
test("preserves prepared artifacts when reclaiming a dead launcher", async () => {
const root = await temporary()
await withRegistry(root, async () => {
const name = "prepared-owner"
const artifacts = join(root, "prepared-artifacts")
await mkdir(join(root, "registry"), { recursive: true })
await Bun.write(
manifestPath(name),
`${JSON.stringify({
version: 1,
name,
createdAt: new Date().toISOString(),
cwd: root,
artifacts,
status: "initialized",
pid: 2_000_000_000,
})}\n`,
)
const create = async () => {
throw new Error("prepared artifacts were replaced")
}
const released = await initializeManifest(name, root, create)
expect(released).toMatchObject({ artifacts })
expect("pid" in released).toBe(false)
expect("pid" in (await Bun.file(manifestPath(name)).json())).toBe(false)
const reclaimed = await initializeManifest(name, root, create, { temporary: true })
expect(reclaimed).toMatchObject({ pid: process.pid, artifacts })
expect("temporary" in reclaimed).toBe(false)
})
})
test("registers only one visible instance", async () => {
const root = await temporary()
await withRegistry(root, async () => {
await register({
...testManifest("first-visible", process.pid),
visible: true,
})
await expect(
register({
...testManifest("second-visible", process.pid),
visible: true,
}),
).rejects.toThrow('visible drive instance "first-visible" is already running')
})
})
test("falls back to the default UI endpoint when no visible instance is registered", async () => {
const root = await temporary()
await withRegistry(root, async () => {
await register(testManifest("headless", process.pid))
expect(await resolveSendEndpoint()).toBe("ws://127.0.0.1:40900")
})
})
test("does not let a stale owner remove its replacement", async () => {
const root = await temporary()
await withRegistry(root, async () => {
const stalePid = 2_000_000_000
await register(testManifest("replacement", stalePid))
await register(testManifest("replacement", process.pid))
await unregister("replacement", stalePid)
expect((await Bun.file(manifestPath("replacement")).json()).pid).toBe(process.pid)
})
})
test("refuses to drive an instance that is still starting", async () => {
const root = await temporary()
await withRegistry(root, () => register(testManifest("starting", process.pid, "starting")))
const child = spawn(["send", "--name", "starting", "--command.ui.state"], root)
const [status, stderr] = await Promise.all([child.exited, new Response(child.stderr).text()])
expect(status).toBe(1)
expect(stderr).toContain('drive instance "starting" is still starting')
})
test("lists sorted active instances and prunes stale state", async () => {
const root = await temporary()
await withRegistry(root, async () => {
await register(testManifest("zeta", process.pid))
await register(testManifest("alpha", process.pid))
await register(testManifest("stale", 2_000_000_000))
await Bun.write(controlPath("orphan"), "")
expect((await listInstances()).map((manifest) => manifest.name)).toEqual(["alpha", "zeta"])
expect(await Bun.file(manifestPath("stale")).exists()).toBe(false)
expect(await Bun.file(controlPath("stale")).exists()).toBe(false)
expect(await Bun.file(controlPath("orphan")).exists()).toBe(false)
})
const listed = spawn(["list"], root)
expect(await listed.exited).toBe(0)
expect(await new Response(listed.stdout).text()).toBe(
`alpha: ${join(root, "registry", "alpha.json")}\nzeta: ${join(root, "registry", "zeta.json")}\n`,
)
})
test("prunes dead transient initialized manifests", async () => {
const root = await temporary()
await withRegistry(root, async () => {
const stalePid = 2_000_000_000
await Bun.write(
manifestPath("temporary-initialized"),
`${JSON.stringify({
version: 1,
name: "temporary-initialized",
createdAt: new Date().toISOString(),
cwd: root,
artifacts: join(root, "opencode-drive", "run-temporary"),
status: "initialized",
temporary: true,
pid: stalePid,
})}\n`,
)
await Bun.write(
manifestPath("visible-12345"),
`${JSON.stringify({
version: 1,
name: "visible-12345",
createdAt: new Date().toISOString(),
cwd: root,
artifacts: join(root, "opencode-drive", "run-visible"),
status: "initialized",
})}\n`,
)
expect(await listInstances()).toEqual([])
expect(await Bun.file(manifestPath("temporary-initialized")).exists()).toBe(false)
expect(await Bun.file(manifestPath("visible-12345")).exists()).toBe(false)
})
})
test("prunes artifact directories not referenced by active sessions", async () => {
const root = await temporary()
const artifactsRoot = join(root, "opencode-drive")
const active = join(artifactsRoot, "run-active")
const initialized = join(artifactsRoot, "run-initialized")
const visible = join(artifactsRoot, "run-visible")
const stale = join(artifactsRoot, "run-stale")
const unrelated = join(artifactsRoot, "other")
await Promise.all(
[active, initialized, visible, stale, unrelated].map((directory) =>
Bun.write(join(directory, "marker.txt"), "artifact\n"),
),
)
await withRegistry(root, async () => {
await register({
...testManifest("active", process.pid),
artifacts: active,
})
await Bun.write(
manifestPath("initialized"),
`${JSON.stringify({
version: 1,
name: "initialized",
createdAt: new Date().toISOString(),
cwd: root,
artifacts: initialized,
status: "initialized",
})}\n`,
)
await Bun.write(
manifestPath("visible-12345"),
`${JSON.stringify({
version: 1,
name: "visible-12345",
createdAt: new Date().toISOString(),
cwd: root,
artifacts: visible,
status: "initialized",
})}\n`,
)
})
const child = spawn(["prune"], root)
expect(await child.exited).toBe(0)
expect(await new Response(child.stdout).text()).toBe("2\n")
expect(await Bun.file(join(active, "marker.txt")).exists()).toBe(true)
expect(await Bun.file(join(initialized, "marker.txt")).exists()).toBe(true)
expect(await Bun.file(join(visible, "marker.txt")).exists()).toBe(false)
expect(await Bun.file(join(stale, "marker.txt")).exists()).toBe(false)
expect(await Bun.file(join(unrelated, "marker.txt")).exists()).toBe(true)
})
test("prunes one named inactive artifact directory", async () => {
const root = await temporary()
const artifactsRoot = join(root, "opencode-drive")
const active = join(artifactsRoot, "run-active")
const staleA = join(artifactsRoot, "run-stale-a")
const staleB = join(artifactsRoot, "run-stale-b")
await Promise.all(
[active, staleA, staleB].map((directory) => Bun.write(join(directory, "marker.txt"), "artifact\n")),
)
await Promise.all([
Bun.write(join(active, "drive", "name"), "active\n"),
Bun.write(join(staleA, "drive", "name"), "stale-a\n"),
Bun.write(join(staleB, "drive", "name"), "stale-b\n"),
])
await withRegistry(root, async () => {
await register({
...testManifest("active", process.pid),
artifacts: active,
})
})
const child = spawn(["prune", "--name", "stale-a"], root)
expect(await child.exited).toBe(0)
expect(await new Response(child.stdout).text()).toBe("1\n")
expect(await Bun.file(join(active, "marker.txt")).exists()).toBe(true)
expect(await Bun.file(join(staleA, "marker.txt")).exists()).toBe(false)
expect(await Bun.file(join(staleB, "marker.txt")).exists()).toBe(true)
})
test("force prunes matching active artifact directories", async () => {
const root = await temporary()
const artifactsRoot = join(root, "opencode-drive")
const active = join(artifactsRoot, "run-active")
const stale = join(artifactsRoot, "run-stale")
await Promise.all([active, stale].map((directory) => Bun.write(join(directory, "marker.txt"), "artifact\n")))
await Promise.all([
Bun.write(join(active, "drive", "name"), "active\n"),
Bun.write(join(stale, "drive", "name"), "stale\n"),
])
await withRegistry(root, async () => {
await register({
...testManifest("active", process.pid),
artifacts: active,
})
})
const child = spawn(["prune", "--name", "active", "--force"], root)
expect(await child.exited).toBe(0)
expect(await new Response(child.stdout).text()).toBe("1\n")
expect(await Bun.file(join(active, "marker.txt")).exists()).toBe(false)
expect(await Bun.file(join(stale, "marker.txt")).exists()).toBe(true)
})
test("force prunes all artifact directories", async () => {
const root = await temporary()
const artifactsRoot = join(root, "opencode-drive")
const active = join(artifactsRoot, "run-active")
const initialized = join(artifactsRoot, "run-initialized")
const stale = join(artifactsRoot, "run-stale")
const unrelated = join(artifactsRoot, "other")
await Promise.all(
[active, initialized, stale, unrelated].map((directory) =>
Bun.write(join(directory, "marker.txt"), "artifact\n"),
),
)
await withRegistry(root, async () => {
await register({
...testManifest("active", process.pid),
artifacts: active,
})
await Bun.write(
manifestPath("initialized"),
`${JSON.stringify({
version: 1,
name: "initialized",
createdAt: new Date().toISOString(),
cwd: root,
artifacts: initialized,
status: "initialized",
})}\n`,
)
})
const child = spawn(["prune", "--force"], root)
expect(await child.exited).toBe(0)
expect(await new Response(child.stdout).text()).toBe("3\n")
expect(await Bun.file(join(active, "marker.txt")).exists()).toBe(false)
expect(await Bun.file(join(initialized, "marker.txt")).exists()).toBe(false)
expect(await Bun.file(join(stale, "marker.txt")).exists()).toBe(false)
expect(await Bun.file(join(unrelated, "marker.txt")).exists()).toBe(true)
})
test("reports optional-name discovery errors", async () => {
const root = await temporary()
const missing = spawn(["dir"], root)
expect(await missing.exited).toBe(1)
expect(await new Response(missing.stderr).text()).toContain("no drive instances are running")
})
test("runs multiple named instances concurrently", async () => {
const root = await temporary()
const names = ["first", "second"] as const
const starts = names.map((name) =>
spawn(["start", "--name", name, "--", process.execPath, fixture("fake-opencode.ts")], root),
)
expect(await Promise.all(starts.map((child) => child.exited))).toEqual([0, 0])
instances.push(...names.map((name) => ({ root, name })))
const first = await Bun.file(join(root, "registry", "first.json")).json()
const second = await Bun.file(join(root, "registry", "second.json")).json()
expect(first.endpoints.ui).not.toBe(second.endpoints.ui)
expect(await spawn(["send", "--name", "first", "--command.ui.state"], root).exited).toBe(0)
expect(await spawn(["send", "--name", "second", "--command.ui.state"], root).exited).toBe(0)
const screenshots = await Promise.all(
[first, second].map(async (manifest, index) => {
const name = index === 0 ? "first" : "second"
const screenshot = spawn(["send", "--name", name, "--command.ui.screenshot", '{"name":"shared"}'], root)
expect(await screenshot.exited).toBe(0)
return (await new Response(screenshot.stdout).text()).trim()
}),
)
expect(screenshots).toEqual([
join(runOutputDirectory(root, first.artifacts, 0), "shared.png"),
join(runOutputDirectory(root, second.artifacts, 0), "shared.png"),
])
const unnamed = spawn(["dir"], root)
const [status, stderr] = await Promise.all([unnamed.exited, new Response(unnamed.stderr).text()])
expect(status).toBe(1)
expect(stderr).toContain("no visible drive instance is running; pass --name (first, second)")
const listed = spawn(["list"], root)
expect(await listed.exited).toBe(0)
expect(await new Response(listed.stdout).text()).toBe(
`first: ${join(root, "registry", "first.json")}\nsecond: ${join(root, "registry", "second.json")}\n`,
)
})
test("surfaces the owner log when detached startup fails", async () => {
const root = await temporary()
const name = "failed-start"
const child = spawn(["start", "--name", name, "--", process.execPath, "-e", "process.exit(7)"], root)
const [status, stdout, stderr] = await Promise.all([
child.exited,
new Response(child.stdout).text(),
new Response(child.stderr).text(),
])
expect(status).toBe(1)
expect(stderr).toContain(`see ${join(root, "registry", `${name}.log`)}`)
expect(await Bun.file(join(root, "registry", `${name}.log`)).text()).toContain("OpenCode exited with status 7")
})
test("makes the visible instance the nameless target", async () => {
const root = await temporary()
const background = "background"
const backgroundStart = spawn(
["start", "--name", background, "--", process.execPath, fixture("fake-opencode.ts")],
root,
)
const [backgroundStatus, backgroundError] = await Promise.all([
backgroundStart.exited,
new Response(backgroundStart.stderr).text(),
])
expect(backgroundStatus).toBe(0)
instances.push({ root, name: background })
const running = spawn(["start", "--visible", "--", process.execPath, fixture("fake-opencode.ts"), "30000"], root)
const name = `visible-${running.pid}`
const manifest = await waitForManifest(root, name)
instances.push({ root, name: manifest.name })
expect(manifest.visible).toBe(true)
const state = spawn(["send", "--command.ui.state"], root)
expect(await state.exited).toBe(0)
expect(JSON.parse(await new Response(state.stdout).text()).focused.editor).toBe(true)
expect(await spawn(["stop"], root).exited).toBe(0)
instances.pop()
expect(await running.exited).toBe(0)
expect(await spawn(["stop", "--name", background], root).exited).toBe(0)
instances.pop()
const listed = spawn(["list"], root)
expect(await listed.exited).toBe(0)
expect(await new Response(listed.stdout).text()).toBe("\n")
})
test("blocks and stops the instance after a script completes", async () => {
const root = await temporary()
const name = "script-test"
const child = spawn(
["start", "--name", name, "--script", fixture("script.ts"), "--", process.execPath, fixture("fake-opencode.ts")],
root,
)
const started = Date.now()
const [status, stdout, stderr] = await Promise.all([
child.exited,
new Response(child.stdout).text(),
new Response(child.stderr).text(),
])
expect(status).toBe(0)
expect(Date.now() - started).toBeLessThan(10_000)
const artifacts = artifactPath(stderr)
expect(await Bun.file(join(artifacts, "script-result.json")).json()).toMatchObject({
frame: { cols: 80, rows: 24 },
gitWriteError: expect.stringContaining("must not modify Git metadata"),
matches: true,
})
expect(
Bun.JSONC.parse(await Bun.file(join(artifacts, "files", ".opencode", "opencode.jsonc")).text()),
).toMatchObject({
autoupdate: false,
model: "simulation/gpt-sim-model",
providers: { simulation: { models: { "gpt-sim-model": {} } } },
test: { declared: true, setup: true },
})
expect(Bun.JSONC.parse(await Bun.file(join(artifacts, "files", ".opencode", "tui.jsonc")).text())).toEqual({
test: { declared: true, setup: true },
})
const backendEvents = (await Bun.file(join(artifacts, "backend-events.jsonl")).text())
.trim()
.split("\n")
.map((line) => JSON.parse(line))
expect(
backendEvents
.filter((event) => event.method === "llm.chunk")
.flatMap((event) => event.params.items)
.filter((item) => item.type === "textDelta")
.map((item) => item.text)
.join(""),
).toBe("script response")
expect(backendEvents.filter((event) => event.method === "llm.chunk").length).toBeGreaterThan(1)
expect(
backendEvents
.filter((event) => event.method === "llm.chunk")
.flatMap((event) => event.params.items)
.every((item) => item.type !== "textDelta" || item.text.length <= 8),
).toBe(true)
expect(backendEvents.filter((event) => event.method === "llm.finish")).toEqual([
{ method: "llm.finish", params: { id: "ex_mock", reason: "stop" } },
])
expect(await Bun.file(join(artifacts, "seeded-at-launch.txt")).text()).toBe("export const seeded = true\n")
expect(await Bun.file(join(artifacts, "files", "setup-seeded.txt")).text()).toBe("included in baseline\n")
expect(await Bun.$`git status --porcelain`.cwd(join(artifacts, "files")).text()).toBe("")
expect((await Bun.$`git log -1 --format=%s`.cwd(join(artifacts, "files")).text()).trim()).toBe("Initial commit")
expect(await realpath(await Bun.file(join(artifacts, "child-cwd.txt")).text())).toBe(
await realpath(join(artifacts, "files")),
)
expect(await Bun.file(join(artifacts, "service-argv.json")).json()).toEqual([
"serve",
"--service",
"--port",
expect.stringMatching(/^\d+$/),
])
const pid = Number(await Bun.file(join(artifacts, "child.pid")).text())
expect(running(pid)).toBe(false)
expect(await Bun.file(join(root, "registry", `${name}.json`)).exists()).toBe(false)
})
test("protects Git metadata in prepared project scripts", async () => {
const root = await temporary()
const name = "prepared-git-script-test"
const initialized = spawn(["init", "--name", name], root)
expect(await initialized.exited).toBe(0)
const artifacts = (await new Response(initialized.stdout).text()).trim()
const files = join(artifacts, "files")
await rm(join(files, ".git"), { recursive: true })
await Bun.$`git init --quiet --initial-branch=main`.cwd(files)
await Bun.$`git add --all`.cwd(files)
await Bun.$`git -c user.name=Fixture -c user.email=fixture@example.com commit --quiet --message=Prepared`.cwd(files)
const gitConfig = await Bun.file(join(files, ".git", "config")).text()
const started = spawn(["start", "--name", name, "--script", fixture("prepared-git-script.ts")], root)
expect(await started.exited).toBe(0)
expect(await Bun.file(join(artifacts, "prepared-git-result.json")).json()).toEqual({
runGitError: expect.stringContaining("must not modify Git metadata"),
setupGitError: expect.stringContaining("must not modify Git metadata"),
})
expect(await Bun.file(join(files, ".git", "config")).text()).toBe(gitConfig)
expect((await Bun.$`git log -1 --format=%s`.cwd(files).text()).trim()).toBe("Prepared")
}, 10_000)
test("cleans artifacts after a successful scripted run", async () => {
const root = await temporary()
const name = "script-cleanup-test"
const child = spawn(
["start", "--name", name, "--script", fixture("script.ts"), "--", process.execPath, fixture("fake-opencode.ts")],
root,
{ keepArtifacts: false },
)
const [status, stderr] = await Promise.all([child.exited, new Response(child.stderr).text()])
expect(status).toBe(0)
const artifacts = artifactPath(stderr)
expect(await Bun.file(artifacts).exists()).toBe(false)
expect(await Bun.file(join(root, "registry", `${name}.json`)).exists()).toBe(false)
})
test("aborts the script run when a UI wait times out even if the script catches it", async () => {
const root = await temporary()
const name = "caught-timeout-test"
const child = spawn(
[
"start",
"--name",
name,
"--script",
fixture("caught-timeout-script.ts"),
"--",
process.execPath,
fixture("fake-opencode.ts"),
],
root,
)
const [status, stderr] = await Promise.all([child.exited, new Response(child.stderr).text()])
expect(status).toBe(1)
const artifacts = artifactPath(stderr)
expect(stderr).toContain('timed out waiting for the UI to match "this text never appears"')
expect(await Bun.file(join(root, "registry", `${name}.json`)).exists()).toBe(false)
}, 10_000)
test("stops the whole run when a script crashes", async () => {
const root = await temporary()
const name = "crashing-script-test"
const child = spawn(
[
"start",
"--name",
name,
"--script",
fixture("crashing-script.ts"),
"--",
process.execPath,
fixture("fake-opencode.ts"),
],
root,
)
const [status, stderr] = await Promise.all([child.exited, new Response(child.stderr).text()])
expect(status).toBe(1)
const artifacts = artifactPath(stderr)
expect(stderr).toContain("script crashed")
expect(await Bun.file(join(root, "registry", `${name}.json`)).exists()).toBe(false)
const pid = Number(await Bun.file(join(artifacts, "child.pid")).text())
expect(running(pid)).toBe(false)
const pruned = spawn(["prune", "--name", name], root)
expect(await pruned.exited).toBe(0)
expect(await new Response(pruned.stdout).text()).toBe("1\n")
expect(await Bun.file(artifacts).exists()).toBe(false)
})
test("checks a typed script without modifying its source directory", async () => {
const root = await temporary()
const directory = join(root, "scripts")
await mkdir(directory, { recursive: true })
await Bun.write(
join(directory, "valid.ts"),
'import { Effect } from "effect"\nimport { defineScript } from "opencode-drive"\nexport default defineScript({ project: { git: true, files: { "src/index.ts": "export {}\\n" } }, run: () => Effect.sleep(1) })\n',
)
const checked = spawn(["check", join(directory, "valid.ts")], root)
expect(await checked.exited).toBe(0)
expect(await Bun.file(join(directory, "node_modules", "opencode-drive")).exists()).toBe(false)
expect(await Bun.file(join(directory, "node_modules", ".bin", "tsgo")).exists()).toBe(false)
expect(await Bun.file(join(directory, "node_modules")).exists()).toBe(false)
await Bun.write(join(directory, "invalid.ts"), 'import { Effect } from "effect"\nEffect.sleep("wrong")\n')
const invalid = spawn(["check", join(directory, "invalid.ts")], root)
const invalidError = new Response(invalid.stderr).text()
expect(await invalid.exited).toBe(1)
expect(await invalidError).toContain("is not assignable to parameter of type")
await Bun.write(
join(directory, "plain-object.ts"),
'import { Effect } from "effect"\nexport default { run: () => Effect.void }\n',
)
const plainObject = spawn(["check", join(directory, "plain-object.ts")], root)
expect(await plainObject.exited).toBe(1)
expect(await new Response(plainObject.stderr).text()).toContain("kind")
await Bun.write(
join(directory, "promise.ts"),
'import { defineScript } from "opencode-drive"\nexport default defineScript({ run: async ({ ui }) => { await ui.submit("Hello") } })\n',
)
const promise = spawn(["check", join(directory, "promise.ts")], root)
const promiseError = new Response(promise.stderr).text()
expect(await promise.exited).toBe(1)
expect(await promiseError).toContain("OpenCode Drive scripts are Effect-only")
expect(await promiseError).toContain("Effect.gen(function* ()")
}, 60_000)
test("runs a script imported from the Drive source checkout", async () => {
const root = await temporary()
const name = "source-script-test"
const child = spawn(
[
"start",
"--name",
name,
"--script",
fixture("source-script.ts"),
"--",
process.execPath,
fixture("fake-opencode.ts"),
],
root,
)
const [status, stderr] = await Promise.all([child.exited, new Response(child.stderr).text()])
expect(status).toBe(0)
expect(stderr).not.toContain("Failed to register capture font")
}, 10_000)
test("creates a type-checkable Effect script without overwriting it", async () => {
const root = await temporary()
const file = join(root, "scripts", "drive.ts")
const created = spawn(["script", "init", file], root)
const [status, stdout] = await Promise.all([created.exited, new Response(created.stdout).text()])
expect(status).toBe(0)
expect(stdout.trim()).toBe(file)
const source = await Bun.file(file).text()
expect(source).toContain('import { defineScript, Effect, Llm } from "opencode-drive"')
expect(source).toContain('llm.queue(Llm.text("The value is 1."))')
const checked = spawn(["check", file], root)
expect(await checked.exited).toBe(0)
const repeated = spawn(["script", "init", file], root)
expect(await repeated.exited).toBe(1)
expect(await new Response(repeated.stderr).text()).toContain("script already exists")
expect(await Bun.file(file).text()).toBe(source)
}, 60_000)
test("launches all TUIs explicitly for a manual UI script", async () => {
const root = await temporary()
const name = "manual-clients-test"
const child = spawn(
[
"start",
"--name",
name,
"--script",
fixture("manual-clients-script.ts"),
"--",
process.execPath,
fixture("fake-opencode.ts"),
],
root,
)
const stderr = new Response(child.stderr).text()
expect(await child.exited).toBe(0)
const error = await stderr
const artifacts = artifactPath(error)
expect(await Bun.file(join(artifacts, "manual-clients.json")).json()).toEqual({
apiHealthy: true,
aliceFrame: { cols: 80, rows: 24 },
aliceMatches: true,
bobMatches: true,
tuiBeforeServer: "launch the script server before launching TUIs",
duplicateServer: "the script server has already been launched",
aliceScreenshot: join(runOutputDirectory(root, artifacts, 0), "alice.png"),
bobScreenshot: join(runOutputDirectory(root, artifacts, 0), "bob.png"),
})
expect((await Bun.file(join(artifacts, "launches.txt")).text()).trim().split("\n")).toHaveLength(2)
}, 60_000)
test("controls a statically declared tool from the running script", async () => {
const root = await temporary()
const child = spawn(
[
"start",
"--name",
"tool-control-script-test",
"--script",
fixture("tool-control-script.ts"),
"--",
process.execPath,
fixture("fake-opencode.ts"),
],
root,
)
const stderr = new Response(child.stderr).text()
expect(await child.exited).toBe(0)
const artifacts = artifactPath(await stderr)
const events = (await Bun.file(join(artifacts, "tool-control-events.jsonl")).text())
.trim()
.split("\n")
.map((line) => JSON.parse(line))
expect(events).toEqual([
{ type: "progress", result: { output: "script progress\n" } },
{ type: "success", result: { output: "script success\n", exit: 0 } },
])
}, 60_000)
test("closes the automatic script's primary TUI", async () => {
const root = await temporary()
const child = spawn(
[
"start",
"--name",
"kill-primary-test",
"--script",
fixture("kill-primary-script.ts"),
"--",
process.execPath,
fixture("fake-opencode.ts"),
],
root,
)
const stderr = new Response(child.stderr).text()
expect(await child.exited).toBe(0)
}, 60_000)
test("kills and relaunches the scripted server and TUIs", async () => {
const root = await temporary()
const child = spawn(
[
"start",
"--name",
"kill-server-test",
"--script",
fixture("kill-server-script.ts"),
"--",
process.execPath,
fixture("fake-opencode.ts"),
],
root,
)
const stderr = new Response(child.stderr).text()
expect(await child.exited).toBe(0)
const error = await stderr
const artifacts = artifactPath(error)
const result = await Bun.file(join(artifacts, "kill-server-result.json")).json()
expect(Number.isInteger(result.firstServer)).toBe(true)
expect(Number.isInteger(result.secondServer)).toBe(true)
expect(result.secondServer).not.toBe(result.firstServer)
expect(dirname(result.aliceRecording)).toBe(runOutputDirectory(root, artifacts, 0))
expect(basename(result.aliceRecording)).toMatch(/^recording-.*\.mp4$/)
expect(await Bun.file(result.aliceRecording).exists()).toBe(true)
const recordings = [...error.matchAll(/opencode-drive: recording (.+\.mp4)/g)].map((match) => match[1]!)
expect(recordings).toHaveLength(2)
expect(await Promise.all(recordings.map((path) => Bun.file(path).exists()))).toEqual([true, true])
expect((await Bun.file(join(artifacts, "launches.txt")).text()).trim().split("\n")).toHaveLength(3)
}, 60_000)
test("retries a launch that fails after LLM attachment", async () => {
const root = await temporary()
const child = spawn(
[
"start",
"--name",
"failed-launch-retry-test",
"--script",
fixture("failed-launch-retry-script.ts"),
"--",
process.execPath,
fixture("fake-opencode.ts"),
"omit-service-registration",
],
root,
)
const stderr = new Response(child.stderr).text()
expect(await child.exited).toBe(0)
const artifacts = artifactPath(await stderr)
const result = await Bun.file(join(artifacts, "failed-launch-retry.json")).json()
expect(Number.isInteger(result.firstPid)).toBe(true)
expect(Number.isInteger(result.secondPid)).toBe(true)
expect(result.secondPid).not.toBe(result.firstPid)
}, 60_000)
test("rejects the removed callback script shape", async () => {
const root = await temporary()
const child = spawn(["start", "--name", "callback-script-test", "--script", fixture("callback-script.ts")], root)
expect(await child.exited).toBe(1)
expect(await new Response(child.stderr).text()).toContain("script must default-export defineScript(...)")
})
test("does not apply primary TUI options to additional TUIs", async () => {
const root = await temporary()
const child = spawn(
[
"start",
"--name",
"tui-options-test",
"--script",
fixture("tui-options-script.ts"),
"--",
process.execPath,
fixture("fake-opencode.ts"),
],
root,
)
const stderr = new Response(child.stderr).text()
expect(await child.exited).toBe(0)
const artifacts = artifactPath(await stderr)
expect(await Bun.file(join(artifacts, "tui-options.json")).json()).toEqual({
primaryRecording: true,
secondaryRecording: false,
})
})
test.each(["setup", "run"] as const)("rejects a Promise-returning script %s callback", async (callback) => {
const root = await temporary()
const script = join(root, `promise-${callback}-script.js`)
await Bun.write(
script,
callback === "setup"
? `import { Effect } from "effect"\nimport { defineScript } from "opencode-drive"\nexport default defineScript({ setup: async () => {}, run: () => Effect.void })\n`
: `import { defineScript } from "opencode-drive"\nexport default defineScript({ run: async () => {} })\n`,
)
const child = spawn(
[
"start",
"--name",
`promise-${callback}-script-test`,
"--script",
script,
"--",
process.execPath,
fixture("fake-opencode.ts"),
],
root,
)
expect(await child.exited).toBe(1)
expect(await new Response(child.stderr).text()).toContain(
callback === "setup" ? "setup must return an Effect" : "script run must return an Effect",
)
})
test("rejects a Promise-returning UI predicate", async () => {
const root = await temporary()
const script = join(root, "promise-predicate-script.js")
await Bun.write(
script,
`import { defineScript } from "opencode-drive"\nexport default defineScript({ run: ({ ui }) => ui.waitFor(() => Promise.resolve(false)) })\n`,
)
const child = spawn(
[
"start",
"--name",
"promise-predicate-script-test",
"--script",
script,
"--",
process.execPath,
fixture("fake-opencode.ts"),
],
root,
)
expect(await child.exited).toBe(1)
expect(await new Response(child.stderr).text()).toContain("ui.waitFor predicate must return a boolean or Effect")
})
test("stops a visible instance after a script completes", async () => {
const root = await temporary()
const name = "visible-script-test"
const child = spawn(
[
"start",
"--visible",
"--name",
name,
"--script",
fixture("script.ts"),
"--",
process.execPath,
fixture("fake-opencode.ts"),
],
root,
)
expect(await child.exited).toBe(0)
expect(await Bun.file(join(root, "registry", `${name}.json`)).exists()).toBe(false)
})
test("keeps service and progress output out of a visible client terminal", async () => {
const root = await temporary()
const child = spawn(
[
"start",
"--visible",
"--name",
"visible-output-test",
"--script",
fixture("script.ts"),
"--",
process.execPath,
fixture("fake-opencode.ts"),
"stdio-markers",
],
root,
)
const [status, stdout, stderr] = await Promise.all([
child.exited,
new Response(child.stdout).text(),
new Response(child.stderr).text(),
])
expect(status).toBe(0)
expect(stdout).toContain("fake-client-stdout")
expect(stdout).not.toContain("fake-service-stdout")
expect(stderr).toContain("fake-client-stderr")
expect(stderr).not.toContain("fake-service-stderr")
expect(stderr).not.toContain("script completed")
const artifacts = artifactPath(stderr)
expect(await Bun.file(join(artifacts, "logs", "service.stdout.log")).text()).toContain("fake-service-stdout")
expect(await Bun.file(join(artifacts, "logs", "service.stderr.log")).text()).toContain("fake-service-stderr")
expect(await Bun.file(join(artifacts, "logs", "opencode-drive.log")).text()).toContain("script completed")
})
test.each(["title-requests", "latest-title-requests"])(
"routes %s outside the normal LLM response sequence",
async (mode) => {
const root = await temporary()
const child = spawn(
[
"start",
"--name",
"title-script-test",
"--script",
fixture("title-script.ts"),
"--",
process.execPath,
fixture("fake-opencode.ts"),
mode,
],
root,
)
const stderr = new Response(child.stderr).text()
expect(await child.exited).toBe(0)
const artifacts = artifactPath(await stderr)
const events = (await Bun.file(join(artifacts, "backend-events.jsonl")).text())
.trim()
.split("\n")
.map((line) => JSON.parse(line))
const text = (id: string) =>
events
.filter((event) => event.method === "llm.chunk" && event.params.id === id)
.flatMap((event) => event.params.items)
.filter((item) => item.type === "textDelta")
.map((item) => item.text)
.join("")
expect(text("ex_title")).toBe("Custom title")
expect(text("ex_mock")).toBe("Normal response")
},
)
test("serves typed LLM chunks and preserves an explicit finish", async () => {
const root = await temporary()
const child = spawn(
[
"start",
"--name",
"serve-script-test",
"--script",
fixture("serve-script.ts"),
"--",
process.execPath,
fixture("fake-opencode.ts"),
],
root,
)
const stderr = new Response(child.stderr).text()
expect(await child.exited).toBe(0)
const artifacts = artifactPath(await stderr)
const events = (await Bun.file(join(artifacts, "backend-events.jsonl")).text())
.trim()
.split("\n")
.map((line) => JSON.parse(line))
expect(
events
.filter((event) => event.method === "llm.chunk")
.flatMap((event) => event.params.items)
.filter((item) => item.type === "textDelta")
.map((item) => item.text)
.join(""),
).toBe("served response")
expect(
events
.filter((event) => event.method === "llm.chunk")
.flatMap((event) => event.params.items)
.filter((item) => item.type === "reasoningDelta")
.map((item) => item.text)
.join(""),
).toBe("thinking")
expect(events.filter((event) => event.method === "llm.finish")).toEqual([
{ method: "llm.finish", params: { id: "ex_mock", reason: "length" } },
])
})
test("waits for a delayed request to accept a sent LLM response", async () => {
const root = await temporary()
const child = spawn(
[
"start",
"--name",
"delayed-queue-test",
"--script",
fixture("script.ts"),
"--",
process.execPath,
fixture("fake-opencode.ts"),
"10000",
"250",
],
root,
)
expect(await child.exited).toBe(0)
})
test("restarts an active scripted run", async () => {
const root = await temporary()
const name = "restart-script-test"
const owner = spawn(
[
"start",
"--name",
name,
"--script",
fixture("restart-script.ts"),
"--",
process.execPath,
fixture("fake-opencode.ts"),
],
root,
)
const manifest = await waitForManifest(root, name)
await waitForLines(join(manifest.artifacts, "script-runs.txt"), 1)
expect(await spawn(["restart", "--name", name], root).exited).toBe(0)
await waitForLines(join(manifest.artifacts, "script-runs.txt"), 2)
const screenshots = (await Bun.file(join(manifest.artifacts, "script-screenshots.txt")).text()).trim().split("\n")
expect(screenshots).toEqual([
join(runOutputDirectory(root, manifest.artifacts, 0), "restart-shared.png"),
join(runOutputDirectory(root, manifest.artifacts, 1), "restart-shared.png"),
])
expect(await spawn(["stop", "--name", name], root).exited).toBe(0)
expect(await owner.exited).toBe(0)
})
test("stops a hanging script after the OpenCode child exits", async () => {
const root = await temporary()
const name = "exited-script-test"
const child = spawn(
[
"start",
"--name",
name,
"--script",
fixture("hanging-script.ts"),
"--",
process.execPath,
fixture("fake-opencode.ts"),
"500",
],
root,
)
expect(await child.exited).toBe(0)
expect(await Bun.file(join(root, "registry", `${name}.json`)).exists()).toBe(false)
})
test("waits for script cancellation and lifecycle cleanup on interruption", async () => {
const root = await temporary()
const name = "interrupted-script-test"
const script = join(root, "interrupted-script.ts")
const marker = join(root, "script-interrupted")
await Bun.write(
script,
`import { Effect } from "effect"\nimport { defineScript } from "opencode-drive"\nexport default defineScript({ run: ({ artifacts }) => Effect.never.pipe(Effect.ensuring(Effect.promise(async () => { const pid = Number(await Bun.file(artifacts + "/child.pid").text()); let running = true; try { process.kill(pid, 0) } catch { running = false }; await Bun.write(${JSON.stringify(marker)}, running ? "child-running\\n" : "child-stopped\\n") }))) })\n`,
)
const child = spawn(
["start", "--daemon", "--name", name, "--script", script, "--", process.execPath, fixture("fake-opencode.ts")],
root,
)
const manifest = await waitForManifest(root, name)
const openCodePid = Number(await Bun.file(join(manifest.artifacts, "child.pid")).text())
process.kill(child.pid, "SIGTERM")
await child.exited
expect(await Bun.file(marker).text()).toBe("child-running\n")
expect(running(openCodePid)).toBe(false)
expect(await Bun.file(join(root, "registry", `${name}.json`)).exists()).toBe(false)
expect(await Bun.file(join(root, "registry", `${name}.sock`)).exists()).toBe(false)
expect(await Bun.file(join(root, "node_modules", "opencode-drive")).exists()).toBe(false)
expect(await Bun.file(join(root, "node_modules")).exists()).toBe(false)
})
test("finalizes recording before interrupted owner teardown completes", async () => {
const root = await temporary()
const name = "interrupted-recording-test"
const child = spawn(
["start", "--daemon", "--name", name, "--record", "--", process.execPath, fixture("fake-opencode.ts")],
root,
)
const manifest = await waitForManifest(root, name)
process.kill(child.pid, "SIGTERM")
await child.exited
expect((await readdir(runOutputDirectory(root, manifest.artifacts, 0))).some((file) => file.endsWith(".mp4"))).toBe(
true,
)
expect(await Bun.file(join(root, "registry", `${name}.json`)).exists()).toBe(false)
expect(await Bun.file(join(root, "registry", `${name}.sock`)).exists()).toBe(false)
}, 60_000)
test("rejects removed LLM commands", async () => {
const root = await temporary()
expect(await spawn(["send", "--command.llm.pending"], root).exited).toBe(1)
})
test("generates configured tool calls from offered schemas", () => {
const settings = createResponseSettings()
settings.update({ types: ["tool"], tools: ["read"] })
const tools = [
{
type: "function",
function: {
name: "shell",
parameters: {
type: "object",
properties: { command: { type: "string" } },
required: ["command"],
},
},
},
{
type: "function",
function: {
name: "read",
parameters: {
type: "object",
properties: {
path: { type: "string" },
offset: { type: "integer" },
limit: { type: "integer" },
},
required: ["path", "offset", "limit"],
},
},
},
]
const response = generateResponse(settings.current(), {
id: "ex_1",
url: "https://api.openai.com/v1/chat/completions",
body: { tools },
})
expect(response.finish).toBe("tool-calls")
expect(response.items).toEqual([
{
type: "toolCall",
index: 0,
id: expect.stringMatching(/^call_[a-f0-9]{16}$/),
name: "read",
input: {
path: ".opencode/opencode.jsonc",
offset: 1,
limit: 120,
},
},
])
settings.update({ types: ["tool"], tools: ["shell", "read"] })
const multiple = generateResponse(settings.current(), {
id: "ex_2",
url: "https://api.openai.com/v1/chat/completions",
body: { tools },
})
expect(multiple.items).toHaveLength(2)
expect(multiple.items).toMatchObject([
{ type: "toolCall", index: 0, name: "shell" },
{ type: "toolCall", index: 1, name: "read" },
])
})
test("generates patches and finishes tool continuations with text", () => {
const settings = createResponseSettings()
settings.update({ types: ["diff"], tools: ["apply_patch"] })
const body = {
tools: [
{
type: "function",
function: {
name: "apply_patch",
parameters: {
type: "object",
properties: { patchText: { type: "string" } },
required: ["patchText"],
},
},
},
],
}
const response = generateResponse(settings.current(), {
id: "ex_1",
url: "https://api.openai.com/v1/chat/completions",
body,
})
expect(response.finish).toBe("tool-calls")
expect(response.items[0]).toMatchObject({
type: "toolCall",
name: "apply_patch",
input: {
patchText: expect.stringMatching(/\n-export function greet\([^)]+\)[\s\S]+\n\+export function greet\(/),
},
})
const continuation = generateResponse(settings.current(), {
id: "ex_2",
url: "https://api.openai.com/v1/chat/completions",
body: { ...body, messages: [{ role: "tool", content: "done" }] },
})
expect(continuation.finish).toBe("stop")
expect(continuation.items[0]).toMatchObject({ type: "textDelta" })
const laterTurn = generateResponse(settings.current(), {
id: "ex_later",
url: "https://api.openai.com/v1/chat/completions",
body: {
...body,
messages: [
{ role: "tool", content: "done" },
{ role: "assistant", content: "Finished." },
{ role: "user", content: "Make another change." },
],
},
})
expect(laterTurn.finish).toBe("tool-calls")
settings.update({ types: ["diff"], tools: ["edit", "write"] })
const write = generateResponse(settings.current(), {
id: "ex_3",
url: "https://api.openai.com/v1/chat/completions",
body: {
tools: [
{
type: "function",
function: {
name: "edit",
parameters: {
type: "object",
properties: {
path: { type: "string" },
oldString: { type: "string" },
newString: { type: "string" },
},
required: ["path", "oldString", "newString"],
},
},
},
{
type: "function",
function: {
name: "write",
parameters: {
type: "object",
properties: {
path: { type: "string" },
content: { type: "string" },
},
required: ["path", "content"],
},
},
},
],
},
})
expect(write.items[0]).toMatchObject({
type: "toolCall",
name: "write",
input: {
path: "src/garden.js",
content: expect.stringContaining("export function greet"),
},
})
})
test("splits generated prose into small streaming chunks", () => {
const text = "Mushrooms gather quietly while flowers map the path home."
const chunks = splitText(text)
expect(chunks.join("")).toBe(text)
expect(chunks.length).toBeGreaterThan(1)
expect(chunks.every((chunk) => chunk.trim().split(/\s+/).length <= 3)).toBe(true)
})
})
function spawn(args: ReadonlyArray<string>, root: string, options: { readonly keepArtifacts?: boolean } = {}) {
const env = {
...process.env,
DRIVE_REGISTRY_DIR: join(root, "registry"),
OPENCODE_DRIVE_MEDIA_DIR: join(root, "output"),
OPENCODE_DRIVE_KEEP_ARTIFACTS: "1",
TMPDIR: root,
}
if (options.keepArtifacts === false) delete env.OPENCODE_DRIVE_KEEP_ARTIFACTS
return Bun.spawn([process.execPath, resolve("src/cli/index.ts"), ...args], {
cwd: resolve("."),
env,
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
})
}
function fixture(name: string) {
return resolve("test", "fixtures", name)
}
function runOutputDirectory(root: string, artifacts: string, generation: number) {
return join(root, "output", basename(artifacts), `generation-${generation}`)
}
async function temporary() {
const root = await mkdtemp(join(tmpdir(), "opencode-drive-test-"))
roots.push(root)
return root
}
async function waitForLines(file: string, count: number) {
const deadline = Date.now() + 10_000
while (Date.now() < deadline) {
const lines = await Bun.file(file)
.text()
.then((text) => text.trim().split("\n").length)
.catch(() => 0)
if (lines >= count) return
await Bun.sleep(25)
}
throw new Error(`timed out waiting for ${count} lines in ${file}`)
}
async function waitForManifest(root: string, name: string, timeout = 10_000) {
const file = join(root, "registry", `${name}.json`)
const deadline = Date.now() + timeout
while (Date.now() < deadline) {
const manifest = await Bun.file(file)
.json()
.catch(() => undefined)
if (manifest?.status === "ready") return manifest as InstanceManifest
await Bun.sleep(25)
}
throw new Error(`timed out waiting for ${file}`)
}
async function waitForLauncher(child: ReturnType<typeof spawn>, stderr: Promise<string>) {
let timer: ReturnType<typeof setTimeout> | undefined
const timeout = new Promise<"timeout">((resolve) => {
timer = setTimeout(() => resolve("timeout"), 10_000)
})
try {
const status = await Promise.race([child.exited, timeout])
if (status !== "timeout") return status
if (child.exitCode === null) child.kill()
await child.exited
throw new Error(`launcher ${child.pid} did not exit after ownership resolved: ${await stderr}`)
} finally {
if (timer !== undefined) clearTimeout(timer)
}
}
async function expectSingleConcurrentStart(prepared: boolean) {
const root = await temporary()
const name = `concurrent-start-${prepared ? "prepared" : "automatic"}`
if (prepared) expect(await spawn(["init", "--name", name], root).exited).toBe(0)
const args = ["start", "--name", name, "--", process.execPath, fixture("fake-opencode.ts")]
const children = [spawn(args, root), spawn(args, root)]
const stderr = children.map((child) => new Response(child.stderr).text())
let managed = false
try {
const manifest = await waitForManifest(root, name, 65_000)
instances.push({ root, name })
managed = true
const statuses = await Promise.all(children.map((child, index) => waitForLauncher(child, stderr[index]!)))
const errors = await Promise.all(stderr)
expect(statuses.sort()).toEqual([0, 1])
expect(errors.filter((error) => error.includes("is already starting"))).toHaveLength(1)
expect(errors.some((error) => error.includes("detached instance exited"))).toBe(false)
expect(manifest.pid).toBeGreaterThan(0)
} finally {
await Promise.all(
children.map(async (child) => {
if (child.exitCode === null) child.kill()
await child.exited
}),
)
await Promise.allSettled(stderr)
if (!managed) await terminateUnmanagedInstance(root, name)
}
}
async function terminateUnmanagedInstance(root: string, name: string) {
const manifest: unknown = await Bun.file(join(root, "registry", `${name}.json`))
.json()
.catch(() => undefined)
if (typeof manifest !== "object" || manifest === null || !("pid" in manifest) || typeof manifest.pid !== "number")
return
for (const signal of ["SIGTERM", "SIGKILL"] as const) {
if (!running(manifest.pid)) return
try {
process.kill(manifest.pid, signal)
} catch (error) {
if (!running(manifest.pid)) return
throw error
}
const deadline = Date.now() + 2_000
while (running(manifest.pid) && Date.now() < deadline) await Bun.sleep(25)
}
if (running(manifest.pid)) throw new Error(`failed to terminate drive owner ${manifest.pid}`)
}
async function waitForMissing(file: string) {
const deadline = Date.now() + 10_000
while (Date.now() < deadline) {
if (!(await Bun.file(file).exists())) return
await Bun.sleep(25)
}
throw new Error(`timed out waiting for ${file} removal`)
}
function artifactPath(stderr: string) {
const line = stderr.split("\n").find((value) => value.startsWith("opencode-drive: using artifacts "))
if (!line) throw new Error("artifact path was not reported")
return line.slice("opencode-drive: using artifacts ".length)
}
function running(pid: number) {
try {
process.kill(pid, 0)
return true
} catch {
return false
}
}
function testManifest(name: string, pid: number, status: "starting" | "ready" = "ready") {
return {
version: 1 as const,
name,
pid,
startedAt: new Date().toISOString(),
cwd: process.cwd(),
artifacts: join(tmpdir(), name),
visible: false,
status,
endpoints: {
ui: "ws://127.0.0.1:1",
backend: "ws://127.0.0.1:2",
},
control: join(tmpdir(), `${name}.sock`),
}
}
async function withRegistry<T>(root: string, task: () => Promise<T>) {
const previous = process.env.DRIVE_REGISTRY_DIR
process.env.DRIVE_REGISTRY_DIR = join(root, "registry")
try {
return await task()
} finally {
if (previous === undefined) delete process.env.DRIVE_REGISTRY_DIR
else process.env.DRIVE_REGISTRY_DIR = previous
}
}