// Opt-in real-host check: OPENCODE_2022=/absolute/path/to/2.0.22/opencode bun scripts/test-multi-work-host.ts // Only fake Herdr/model servers and fresh temporary repositories/databases are used. import { mkdir, mkdtemp, writeFile, chmod, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { createServer } from "node:net"; import { OpenCode } from "@opencode/client"; import { command } from "../src/multi-work/herdr.js"; import { z } from "zod"; const binary = process.env.OPENCODE_2022; if (!binary) throw new Error("Set OPENCODE_2022 to an isolated OpenCode 2.0.22 binary"); const temp = await mkdtemp(join(tmpdir(), "native-multi-work-")); try { const root = join(temp, "root"), destination = join(temp, "child"), bin = join(temp, "bin"), config = join(temp, "config"); const herdrCreation = join(temp, "herdr-creation.txt"); for (const path of [root, bin, config, join(temp, "plugin")]) await mkdir(path); const signal = AbortSignal.timeout(120_000); if ((await command([binary, "--version"], temp, signal)) !== "opencode v2.0.22") throw new Error("Exact 2.0.22 binary required"); await command(["git", "init", "-b", "main"], root, signal); await command( [ "git", "-c", "user.name=Fixture", "-c", "user.email=fixture@example.test", "commit", "--allow-empty", "-m", "fixture", ], root, signal, ); const inventory = (path: string, id: string) => JSON.stringify({ result: { source: { repo_key: join(root, ".git"), repo_root: root, source_checkout_path: root, source_workspace_id: "w1", }, worktrees: [ { path: root, is_linked_worktree: false, is_bare: false, open_workspace_id: "w1", }, ...(path === root ? [] : [ { path, is_linked_worktree: true, is_bare: false, open_workspace_id: id, }, ]), ], }, }); await writeFile( join(bin, "herdr"), `#!/bin/sh\ncase "$1 $2" in\n"worktree create") [ "$9" = "--label" ] && [ "\${10}" = "Fix native host" ] || exit 8; printf '%s\\n%s\\n' "$6" "\${10}" > '${herdrCreation}'; git -C '${root}' worktree add -b "$6" '${destination}' "$8" >&2 || exit 1; printf '{"result":{"workspace":{"workspace_id":"w2"},"worktree":{"path":"${destination}","branch":"%s","is_linked_worktree":true}}}' "$6"; exit ;;\nesac\ncase "$1 $2 $4" in\n"workspace list ") echo '{"result":{"workspaces":[{"workspace_id":"w1"}]}}' ;;\n"worktree list w1") echo '${inventory(root, "w1")}' ;;\n"worktree list w2") echo '${inventory(destination, "w2")}' ;;\n*) exit 9 ;;\nesac\n`, ); await chmod(join(bin, "herdr"), 0o755); for (const [name, fixture] of [ ["agents", "native-agents-plugin"], ["multi-work", "native-host-plugin"], ] as const) { const built = await Bun.build({ entrypoints: [resolve(`tests/fixtures/${fixture}.ts`)], outdir: join(temp, "plugin", name), naming: "index.js", target: "bun", format: "esm", }); if (!built.success) throw new Error(String(built.logs)); } await writeFile( join(config, "opencode.json"), JSON.stringify({ plugins: [ { package: join(temp, "plugin", "agents") }, { package: join(temp, "plugin", "multi-work") }, ], agents: { build: { permissions: [{ action: "herdr_worktree", resource: "*", effect: "deny" }], }, }, experimental: { subagent_depth: 2 }, snapshots: false, update: "disable", model: "fixture/gpt-6-astra", }), ); let holdModel = false; const modelEntered = Promise.withResolvers(); const releaseModel = Promise.withResolvers(); const model = Bun.serve({ hostname: "127.0.0.1", port: 0, async fetch(request) { await request.text(); if (holdModel) { modelEntered.resolve(); await releaseModel.promise; } const chunk = { id: "fixture", object: "chat.completion.chunk", created: 0, model: "fixture", choices: [ { index: 0, delta: { role: "assistant", content: "FIXTURE_COMPLETE" }, finish_reason: null, }, ], }; return new Response( `data: ${JSON.stringify(chunk)}\n\ndata: ${JSON.stringify({ ...chunk, choices: [{ index: 0, delta: {}, finish_reason: "stop" }] })}\n\ndata: [DONE]\n\n`, { headers: { "content-type": "text/event-stream" } }, ); }, }); const socket = createServer(); await new Promise((done) => socket.listen(0, "127.0.0.1", done)); const { port } = z.object({ port: z.number() }).parse(socket.address()); await new Promise((done) => socket.close(() => done())); // Do not inherit user credentials, discovery URLs, Herdr identity, or config overrides. const env = { PATH: `${bin}:${process.env.PATH}`, HOME: join(temp, "home"), XDG_CONFIG_HOME: config, XDG_DATA_HOME: join(temp, "data"), XDG_CACHE_HOME: join(temp, "cache"), XDG_STATE_HOME: join(temp, "state"), OPENCODE_CONFIG_DIR: config, OPENCODE_DB: join(temp, "fixture.sqlite"), FIXTURE_MODEL_URL: `http://127.0.0.1:${model.port}/v1`, FIXTURE_TRACE: join(temp, "admissions.jsonl"), }; const start = () => Bun.spawn([binary, "serve", "--hostname", "127.0.0.1", "--port", String(port)], { cwd: root, env, stdout: Bun.file(join(temp, "server.log")), stderr: Bun.file(join(temp, "server-errors.log")), }); let server = start(); const headers = { authorization: "" }; const client = OpenCode.make({ baseUrl: `http://127.0.0.1:${port}`, headers }); async function ready() { for (let attempt = 0; attempt < 200; attempt++) { try { const password = (await readFile(join(temp, "server.log"), "utf8")).match( /server password (\S+)/, )?.[1]; if (password) headers.authorization = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`; await client.server.info(); return; } catch { await Bun.sleep(100); } } throw new Error( `Host did not start: ${await readFile(join(temp, "server-errors.log"), "utf8")}`, ); } try { await ready(); const location = { directory: root }; for (let attempt = 0; attempt < 100; attempt++) { const agents = await client.agent.list({ location }); if (agents.data.some((agent) => agent.id === "multi-work")) break; if (attempt === 99) throw new Error( `multi-work unavailable: ${JSON.stringify(await client.plugin.list({ location }))}`, ); await Bun.sleep(100); } const parent = await client.session.create({ location, agent: "multi-work" }); const denied = await client.rpc.call({ rpcID: "multi-work-test", method: "delegate", location, input: { parent: parent.id, background: false }, }); if (!z.string().parse(denied.output).includes("herdr_worktree requires explicit allow")) throw new Error( `Late configured build deny was not enforced: ${JSON.stringify(denied)}`, ); await client.session.update({ sessionID: parent.id, permissions: [{ action: "herdr_worktree", resource: "*", effect: "allow" }], }); const result = await client.rpc.call({ rpcID: "multi-work-test", method: "delegate", location, input: { parent: parent.id, background: false }, }); const output = z .object({ metadata: z.object({ sessionID: z.string(), status: z.literal("completed") }), content: z.string(), }) .parse(JSON.parse(z.string().parse(result.output))); if (!output.content.includes("FIXTURE_COMPLETE")) throw new Error("Missing native child result"); if ((await readFile(herdrCreation, "utf8")) !== "fix/native-host\nFix native host\n") throw new Error("Custom Herdr branch/label mismatch"); const child = await client.session.get({ sessionID: output.metadata.sessionID }); if (child.parentID !== parent.id || child.location.directory !== destination) throw new Error("Child ancestry/placement mismatch"); const admission = z.array(z.object({ id: z.string(), directory: z.string() })).parse( (await readFile(env.FIXTURE_TRACE, "utf8")) .trim() .split("\n") .map((line) => JSON.parse(line)), ); if ( !admission.some((entry) => entry.id === child.id && entry.directory === destination) || admission.some((entry) => entry.id === child.id && entry.directory !== destination) ) throw new Error("Prompt admitted before move"); server.kill(); await server.exited; server = start(); await ready(); await client.agent.list({ location }); const resumed = await client.rpc.call({ rpcID: "multi-work-test", method: "delegate", location, input: { parent: parent.id, child: child.id, background: true }, }); const background = z .object({ metadata: z.object({ sessionID: z.literal(child.id), status: z.literal("running"), }), }) .parse(JSON.parse(z.string().parse(resumed.output))); console.log("Native background child", background.metadata.sessionID); await client.session.wait({ sessionID: child.id }); if ((await client.session.get({ sessionID: child.id })).outcome !== "succeeded") throw new Error("Background child did not complete"); for (let attempt = 0; attempt < 100; attempt++) { const messages = await client.session.context({ sessionID: parent.id }); if ( messages.some( (message) => message.type === "synthetic" && message.text.includes(child.id) && message.text.includes("FIXTURE_COMPLETE"), ) ) break; if (attempt === 99) throw new Error("Missing native background completion notification"); await Bun.sleep(100); } const finalAdmissions = (await readFile(env.FIXTURE_TRACE, "utf8")) .trim() .split("\n") .map((line) => z.object({ id: z.string(), directory: z.string() }).parse(JSON.parse(line)), ); if ( finalAdmissions.filter( (entry) => entry.id === child.id && entry.directory === destination, ).length !== 2 ) throw new Error("Expected two child prompts at destination"); if ((await client.session.get({ sessionID: parent.id })).location.directory !== root) throw new Error("Parent moved"); await client.session.wait({ sessionID: parent.id }); holdModel = true; const cancel = new AbortController(); const pending = client.rpc.call( { rpcID: "multi-work-test", method: "delegate", location, input: { parent: parent.id, child: child.id, background: false }, }, { signal: cancel.signal }, ); const deadline = AbortSignal.timeout(10_000); await Promise.race([ modelEntered.promise, pending.then(() => { throw new Error("Foreground unexpectedly finished before cancellation"); }), new Promise((_resolve, reject) => deadline.addEventListener( "abort", () => reject(new Error("Model was not reached")), { once: true, }, ), ), ]); cancel.abort(); let rejected = false; await pending.catch(() => { rejected = true; }); if (!rejected) throw new Error("Cancelled RPC did not reject"); for (let attempt = 0; attempt < 100; attempt++) { if ((await client.session.get({ sessionID: child.id })).outcome === "interrupted") break; if (attempt === 99) throw new Error("Native cancellation did not interrupt the child"); await Bun.sleep(50); } releaseModel.resolve(); holdModel = false; for (const plugins of [ [{ package: join(temp, "plugin", "multi-work") }], [ { package: join(temp, "plugin", "multi-work") }, { package: join(temp, "plugin", "agents") }, ], ]) { server.kill(); await server.exited; await writeFile( join(config, "opencode.json"), JSON.stringify({ plugins, update: "disable" }), ); server = start(); await ready(); for (let attempt = 0; attempt < 100; attempt++) { const { data } = await client.plugin.list({ location }); const multi = data.find((plugin) => plugin.id === "stanp-opencode.multi-work"); if (multi?.state.status === "failed") break; if (attempt === 99 || multi?.state.status === "active") throw new Error( `Missing or late Agents did not fail Multi-work activation: ${JSON.stringify(data)}`, ); await Bun.sleep(100); } } server.kill(); await server.exited; const quotaBuild = await Bun.build({ entrypoints: [resolve("src/quota/quota-plugin.ts")], outdir: join(temp, "plugin", "quota"), naming: "index.js", target: "bun", format: "esm", }); if (!quotaBuild.success) throw new Error(String(quotaBuild.logs)); await writeFile( join(config, "opencode.json"), JSON.stringify({ plugins: [{ package: join(temp, "plugin", "quota") }], update: "disable", }), ); server = start(); await ready(); for (let attempt = 0; attempt < 100; attempt++) { const independent = await client.plugin.list({ location }); if ( independent.data.some( (plugin) => plugin.id === "stanp-opencode.agents" || plugin.id === "stanp-opencode.multi-work", ) ) throw new Error("Quota-only configuration loaded Agents or Multi-work"); if ( independent.data.some( (plugin) => plugin.id === "stanp-opencode.quota" && plugin.state.status === "active", ) ) break; if (attempt === 99) throw new Error("Quota did not activate independently"); await Bun.sleep(100); } const quotaStatus = await client.rpc.call({ rpcID: "stanp-opencode.quota", method: "status", location, input: {}, }); if (quotaStatus.output !== null) throw new Error("Unauthenticated Quota RPC should return null"); console.log( "PASS: configured build deny + session override, native foreground result, child move-before-prompt, persisted binding resume after host restart, background completion + parent notification, native foreground interruption, missing/late Agents fail closed, independent Quota and stable RPC", ); } finally { releaseModel.resolve(); server.kill(); await server.exited; model.stop(true); } } finally { await rm(temp, { recursive: true, force: true }); }