chore: keep live registry acceptance harnesses local

This commit is contained in:
Ogulcan Celik
2026-09-19 21:46:23 +03:00
parent 18b4a8fe33
commit e0e4252cd3
2 changed files with 0 additions and 780 deletions
-588
View File
@@ -1,588 +0,0 @@
#!/usr/bin/env bun
/** Linux-only, real OpenCode / dynamic registry acceptance harness.
* Run only after compiling the candidate: bun run scripts/agent_registry_opencode_e2e.ts --binary target/debug/herdr
* Requires an existing Herdr session. Never launches/attaches/stops the default session.
* No reporters/plugins, report-agent calls, remote models, or global config writes.
*/
import assert from "node:assert/strict";
import path from "node:path";
import { appendFileSync } from "node:fs";
import { access, chmod, cp, mkdir, mkdtemp, readFile, readdir, readlink, rename, writeFile } from "node:fs/promises";
import { Database } from "bun:sqlite";
import { createFixtureProvider, selfTestProvider } from "./agent_registry_opencode_provider";
const repo = path.resolve(import.meta.dir, "..");
const quote = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
const unwrap = (value: any) => value.result ?? value;
const json = (text: string) => JSON.parse(text.trim());
const clearedHerdr = ["HERDR_SOCKET_PATH", "HERDR_CLIENT_SOCKET_PATH", "HERDR_SESSION", "HERDR_WORKSPACE_ID", "HERDR_TAB_ID", "HERDR_PANE_ID"];
type Check = { name: string; status: "pass" | "prerequisite" | "fail"; detail?: unknown };
type Session = { name: string; running: boolean; socket_path: string; session_dir: string };
type TrackedProcess = { pid: number; start: string; executable: string };
async function processStart(pid: number) {
const stat = await readFile(`/proc/${pid}/stat`, "utf8");
return stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19];
}
async function signalTracked(process: TrackedProcess, signal: NodeJS.Signals) {
assert.equal(await processStart(process.pid), process.start, "refusing signal: PID was reused");
assert.equal(await readlink(`/proc/${process.pid}/exe`), process.executable, "refusing signal: executable changed");
globalThis.process.kill(process.pid, signal);
}
/** Read only this run's isolated databases. Never inspect global OpenCode storage. */
async function localSessionID(root: string, work: string): Promise<string | undefined> {
for (const entry of await readdir(root, { withFileTypes: true })) {
const file = path.join(root, entry.name);
if (entry.isDirectory()) {
const id = await localSessionID(file, work);
if (id) return id;
} else if (entry.name.endsWith(".db") || entry.name.endsWith(".sqlite")) {
let db: Database | undefined;
try {
db = new Database(file, { readonly: true });
const columns = db.query("PRAGMA table_info(session)").all() as { name: string }[];
if (!["id", "directory"].every((name) => columns.some((column) => column.name === name))) continue;
const row = db.query("SELECT id FROM session WHERE directory = ? ORDER BY rowid DESC LIMIT 1").get(work) as { id?: string } | null;
if (row?.id?.startsWith("ses_")) return row.id;
} catch { /* Schema/version varies; lack of a reference is a prerequisite, not a fake pass. */ }
finally { db?.close(); }
}
}
}
async function runHarness(binaryArg: string) {
assert.equal(process.platform, "linux", "this harness currently supports local Linux only");
assert.equal(process.env.HERDR_ENV, "1", "requires an existing Herdr session (no CI PTY fallback yet)");
assert(process.env.HERDR_PANE_ID, "caller pane ID is required for explicit outer-pane split");
const binary = path.resolve(binaryArg);
await access(binary);
const opencode = Bun.which("opencode");
const parentBinary = process.env.HERDR_BIN_PATH || Bun.which("herdr");
assert(opencode && parentBinary, "installed opencode and parent Herdr CLI are required");
// Keep UNIX socket paths below sockaddr_un's 108-byte limit, including named-session suffixes.
const root = await mkdtemp("/var/tmp/hr-oc-");
const nonce = path.basename(root).split("-").at(-1)!.toLowerCase();
const sessionName = `registry-oc-${Date.now().toString(36)}-${nonce}`;
assert(sessionName !== "default");
const novel = `opencode-lab-${nonce}`;
const source = path.join(root, "registry-source");
const badSource = path.join(root, "bad-source");
const home = path.join(root, "home");
const config = path.join(home, ".config/opencode");
const work = path.join(root, "work");
const commandsLog = path.join(root, "commands.jsonl");
const delayedLaunchName = `opencode-delayed-${nonce}`;
const delayedLaunch = path.join(root, "bin", delayedLaunchName);
const checks: Check[] = [];
const provider = createFixtureProvider({ onEvent: (event) => appendFileSync(path.join(root, "provider.jsonl"), `${JSON.stringify(event)}\n`) });
let outerPane: string | undefined;
let pane: string | undefined;
let sessionInfo: Session | undefined;
let frozen: TrackedProcess | undefined;
let sessionOwned = false;
let sessionMayExist = false;
let failure: unknown;
let cleaning = false;
let interrupted = false;
const interrupt = () => { interrupted = true; };
process.on("SIGINT", interrupt);
process.on("SIGTERM", interrupt);
const check = (name: string, status: Check["status"], detail?: unknown) => {
checks.push({ name, status, detail });
console.log(`${status.toUpperCase()}: ${name}`);
};
console.log(`Artifacts: ${root}\nSession: ${sessionName}\nNovel agent: ${novel}`);
// Clean allowlist: no auth, plugins, proxy credentials, experimental flags or parent selection.
const env: Record<string, string> = {
PATH: [...new Set([path.join(root, "bin"), path.dirname(opencode), path.dirname(Bun.which("bun") || process.execPath), "/usr/local/bin", "/usr/bin", "/bin"])].join(":"),
HOME: home, SHELL: "/bin/bash", USER: process.env.USER || "fixture", LOGNAME: process.env.USER || "fixture",
TERM: process.env.TERM || "xterm-256color", COLORTERM: "truecolor", LANG: "C.UTF-8",
XDG_CONFIG_HOME: path.join(home, ".config"), XDG_DATA_HOME: path.join(root, "data"),
XDG_CACHE_HOME: path.join(root, "cache"), XDG_STATE_HOME: path.join(root, "state"), TMPDIR: path.join(root, "tmp"),
HERDR_ENV: "1", HERDR_SESSION: sessionName, HERDR_CONFIG_PATH: path.join(root, "herdr/config.toml"),
HERDR_AGENT_REGISTRY_SOURCE: source,
OPENCODE_TEST_HOME: home, OPENCODE_CONFIG_DIR: config, OPENCODE_CONFIG: path.join(config, "opencode.json"),
OPENCODE_TUI_CONFIG: path.join(config, "tui.json"), OPENCODE_TEST_MANAGED_CONFIG_DIR: path.join(root, "managed"),
OPENCODE_AUTH_CONTENT: "{}", OPENCODE_PURE: "1", OPENCODE_DISABLE_PROJECT_CONFIG: "1",
OPENCODE_DISABLE_DEFAULT_PLUGINS: "1", OPENCODE_DISABLE_EXTERNAL_SKILLS: "1", OPENCODE_DISABLE_CLAUDE_CODE: "1",
OPENCODE_DISABLE_MODELS_FETCH: "1", OPENCODE_DISABLE_AUTOUPDATE: "1", OPENCODE_DISABLE_AUTOCOMPACT: "1",
OPENCODE_DISABLE_LSP_DOWNLOAD: "1", OPENCODE_DISABLE_SHARE: "1", OPENCODE_DISABLE_FFF: "1",
// Best-effort deny background package/update traffic too; no model endpoint can leave loopback.
HTTP_PROXY: "http://127.0.0.1:9", HTTPS_PROXY: "http://127.0.0.1:9", ALL_PROXY: "http://127.0.0.1:9",
NO_PROXY: "127.0.0.1,localhost", no_proxy: "127.0.0.1,localhost",
};
const controlEnv = { ...env };
for (const key of clearedHerdr) delete controlEnv[key];
controlEnv.HERDR_SESSION = sessionName;
async function exec(executable: string, args: string[], environment = controlEnv, allowFailure = false) {
if (interrupted && !cleaning) throw new Error("interrupted; cleaning up owned resources");
const started = Date.now();
const child = Bun.spawn([executable, ...args], { cwd: repo, env: environment, stdin: "ignore", stdout: "pipe", stderr: "pipe" });
let timedOut = false;
const timer = setTimeout(() => { timedOut = true; child.kill("SIGKILL"); }, 25000);
try {
const [stdout, stderr, code] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text(), child.exited]);
appendFileSync(commandsLog, `${JSON.stringify({ at: new Date(started).toISOString(), ms: Date.now() - started, executable, args, code, timedOut, stdout, stderr })}\n`);
if (!allowFailure) assert(code === 0 && !timedOut, `${executable} ${args.join(" ")}: ${stderr || stdout}${timedOut ? " (timeout)" : ""}`);
return { stdout, stderr, code, timedOut };
} finally { clearTimeout(timer); }
}
// Several successful mutation commands (notably pane run/send-text) deliberately print nothing.
const response = (stdout: string) => stdout.trim() ? unwrap(json(stdout)) : {};
const cli = async (args: string[]) => response((await exec(binary, args)).stdout);
const parent = async (args: string[]) => response((await exec(parentBinary!, args, process.env as Record<string, string>)).stdout);
async function rawApi(request: unknown) {
assert(sessionInfo?.socket_path, "named session socket must come from session list");
const code = "import socket,sys; s=socket.socket(socket.AF_UNIX); s.connect(sys.argv[1]); s.sendall((sys.argv[2]+'\\n').encode()); print(s.makefile('r').readline(), end='')";
return json((await exec("python3", ["-c", code, sessionInfo.socket_path, JSON.stringify(request)])).stdout);
}
const sessions = async (): Promise<Session[]> => json((await exec(binary, ["session", "list", "--json"])).stdout).sessions;
async function poll<T>(name: string, fn: () => Promise<T | undefined | false>, timeout = 20000): Promise<T> {
const deadline = Date.now() + timeout;
let last: unknown;
while (Date.now() < deadline) {
if (interrupted && !cleaning) throw new Error("interrupted");
try { const value = await fn(); if (value !== undefined && value !== false) return value as T; }
catch (error) { last = error; }
await Bun.sleep(150);
}
throw new Error(`timeout: ${name}${last ? `; last error: ${last}` : ""}`);
}
async function state(status: string) {
assert(pane);
return poll(`agent ${status}`, async () => {
const info = (await cli(["agent", "get", pane!])).agent;
return info.agent === novel && info.agent_status === status ? info : undefined;
}, 30000);
}
async function productReady(history = false) {
const ready = await poll("product interactive_ready", async () => {
const info = (await cli(["agent", "get", pane!])).agent;
return info.interactive_ready === true && !info.launch_pending ? info : undefined;
}, 60000);
const screen = (await exec(binary, ["pane", "read", pane!, "--source", "recent", "--format", "text"])).stdout;
assert(/^\s*╹▀{8,}\s*$/m.test(screen), "interactive_ready must correspond to the painted composer frame");
assert(/commands/i.test(screen), "interactive_ready must correspond to a visible commands action");
if (history) assert(screen.includes("Fixture complete."), "resumed interactive_ready must include restored conversation history");
return { ready, screen };
}
async function rejectPrematurePrompt(label: string) {
const startup = await poll("startup visible before interactive_ready", async () => {
const info = (await cli(["agent", "get", pane!])).agent;
if (info.interactive_ready || !info.launch_pending) return;
const screen = (await exec(binary, ["pane", "read", pane!, "--source", "recent", "--format", "text"])).stdout;
return screen.trim() ? { info, screen } : undefined;
}, 30000);
const marker = `HERDR_E2E_PREMATURE_${nonce}_${label}`;
const providerCallsBefore = provider.events.filter((event) => event.type === "request").length;
const rejected = await exec(binary, ["agent", "prompt", pane!, marker], controlEnv, true);
assert(rejected.code !== 0 && !rejected.timedOut, "premature prompt must be rejected synchronously");
const payload = json(rejected.stderr || rejected.stdout);
assert.equal(payload.error?.code, "agent_not_ready");
await Bun.sleep(350);
const after = (await exec(binary, ["pane", "read", pane!, "--source", "recent", "--format", "text"])).stdout;
assert(!after.includes(marker), "rejected premature prompt must not write to the PTY");
assert.equal(provider.events.filter((event) => event.type === "request").length, providerCallsBefore, "rejected premature prompt must not call the provider");
await writeFile(path.join(root, `${label}-premature-rejection.json`), JSON.stringify({ startup: startup.info, screen: startup.screen, response: payload, providerCallsBefore }, null, 2));
check(`${label} rejects prompt before product readiness`, "pass");
}
async function capture(label: string, recognized = true) {
assert(pane);
await writeFile(path.join(root, `${label}.pane.json`), JSON.stringify(await cli(["pane", "get", pane]), null, 2));
await writeFile(path.join(root, `${label}.process.json`), JSON.stringify(await cli(["pane", "process-info", "--pane", pane]), null, 2));
if (recognized) {
await writeFile(path.join(root, `${label}.agent.json`), JSON.stringify(await cli(["agent", "get", pane]), null, 2));
const explain = await exec(binary, ["agent", "explain", pane, "--json"], controlEnv, true);
await writeFile(path.join(root, `${label}.explain.json`), explain.stdout || explain.stderr);
}
for (const format of ["text", "ansi"]) {
const result = await exec(binary, [recognized ? "agent" : "pane", "read", pane, "--source", recognized ? "detection" : "recent", "--format", format]);
await writeFile(path.join(root, `${label}.${format === "text" ? "txt" : "ansi"}`), result.stdout);
}
}
async function foreground(): Promise<TrackedProcess | undefined> {
const info = (await cli(["pane", "process-info", "--pane", pane!])).process_info;
for (const item of info.foreground_processes ?? []) {
try {
const executable = await readlink(`/proc/${item.pid}/exe`);
if (executable === await readlinkResolved(opencode!)) return { pid: item.pid, executable, start: await processStart(item.pid) };
} catch { /* A process may exit between API and /proc reads. */ }
}
}
async function readlinkResolved(file: string) {
// realpath supports both ELF files and symlink-based installations.
return (await import("node:fs/promises")).realpath(file);
}
async function namedLaunch(round: string) {
assert(outerPane);
sessionMayExist = true;
const launchEnv = { ...env };
for (const key of clearedHerdr) delete launchEnv[key];
const command = `env -i ${Object.entries(launchEnv).map(([key, value]) => `${key}=${quote(value)}`).join(" ")} ${quote(binary)} --session ${quote(sessionName)}; printf ${quote(`\\nHERDR_E2E_NAMED_EXIT_${nonce}_${round}\\n`)}`;
await parent(["pane", "run", outerPane, command]);
await poll("named session API", async () => {
const list = await sessions();
const item = list.find((item) => item.name === sessionName && item.running);
if (!item) return;
sessionInfo = item; // IDs and socket/directory paths originate in command output.
const panes = (await cli(["pane", "list"])).panes;
if (!panes?.length) return;
sessionOwned = true;
pane = panes[0].pane_id;
return true;
}, 45000);
await writeFile(path.join(root, `session-${round}.json`), JSON.stringify(sessionInfo, null, 2));
}
async function namedStop(round: string) {
assert(sessionOwned && sessionInfo?.name === sessionName && outerPane);
await exec(binary, ["session", "stop", sessionName, "--json"]);
await poll("named session stopped", async () => !(await sessions()).some((item) => item.name === sessionName && item.running));
await exec(parentBinary!, ["pane", "wait-output", outerPane, "--match", `HERDR_E2E_NAMED_EXIT_${nonce}_${round}`, "--timeout", "20000"], process.env as Record<string, string>);
}
async function exitOpenCode(label: string) {
assert(pane && await foreground(), "refusing exit input: target is not tracked OpenCode");
await cli(["pane", "send-text", pane, "/exit"]);
await cli(["pane", "send-keys", pane, "Enter"]);
await poll("OpenCode exits", async () => !(await foreground()));
await poll("agent clears after exit", async () => !(await cli(["pane", "get", pane!])).pane.agent);
await capture(label, false);
}
async function turn(mode: "COMPLETE" | "PERMISSION" | "CANCEL", suffix: string) {
const marker = `HERDR_E2E_${mode}_${nonce}_${suffix}`;
await cli(["agent", "prompt", pane!, marker]);
await poll(`provider gate ${mode}`, async () => provider.events.some((event) => event.type === "gated" && event.marker === marker));
await state("working");
await capture(`${suffix}-working`);
if (mode === "CANCEL") {
await cli(["pane", "send-keys", pane!, "Escape"]);
await cli(["pane", "send-keys", pane!, "Escape"]);
await poll("HTTP stream aborted", async () => provider.events.some((event) => event.type === "aborted" && event.marker === marker));
assert(!provider.events.some((event) => event.type === "finished" && event.marker === marker), "cancelled turn must not complete");
} else {
await provider.release(marker);
if (mode === "PERMISSION") {
await state("blocked");
await capture(`${suffix}-blocked`);
const screen = await readFile(path.join(root, `${suffix}-blocked.txt`), "utf8");
assert(screen.includes("Permission required"), "must be real permission UI, not just a blocked state");
await cli(["pane", "send-keys", pane!, "Escape"]); // Root permission Reject, never approve.
}
}
await state("idle");
await capture(`${suffix}-idle`);
check(`real UI ${mode.toLowerCase()} → idle`, "pass");
}
try {
for (const dir of [config, work, path.dirname(delayedLaunch), env.XDG_DATA_HOME, env.XDG_CACHE_HOME, env.XDG_STATE_HOME, env.TMPDIR, env.OPENCODE_TEST_MANAGED_CONFIG_DIR, path.dirname(env.HERDR_CONFIG_PATH), path.join(source, "agents")]) await mkdir(dir, { recursive: true });
await writeFile(delayedLaunch, `#!/bin/sh\nsleep 1.5\nexec ${quote(opencode)} "$@"\n`);
await chmod(delayedLaunch, 0o700);
await writeFile(env.HERDR_CONFIG_PATH, 'onboarding = false\n[terminal]\ndefault_shell = "/bin/bash"\nshell_mode = "non_login"\n[session]\nresume_agents_on_restore = true\n[experimental]\nallow_nested = true\n[update]\nversion_check = false\nmanifest_check = false\n');
await writeFile(path.join(config, "tui.json"), JSON.stringify({ plugin: [] }));
await writeFile(path.join(config, "opencode.json"), JSON.stringify({
model: "herdr-local/fixture", small_model: "herdr-local/fixture", enabled_providers: ["herdr-local"],
provider: { "herdr-local": { npm: "@ai-sdk/openai-compatible", name: "Herdr Local Fixture", env: [],
options: { baseURL: `${provider.url}/v1`, apiKey: "local-not-a-secret" },
models: { fixture: { name: "Herdr Fixture", tool_call: true, reasoning: false, limit: { context: 32768, output: 1024 }, cost: { input: 0, output: 0 } } } } },
permission: { "*": "deny", bash: "ask" }, agent: { title: { disable: true }, summary: { disable: true } },
share: "disabled", autoupdate: false, snapshot: false, lsp: false, formatter: false,
}, null, 2));
await writeFile(path.join(root, "launch-environment.json"), JSON.stringify(env, null, 2));
const agents = path.join(repo, "vendor/agent-registry/agents");
for (const entry of await readdir(agents)) if (entry !== "opencode") await cp(path.join(agents, entry), path.join(source, "agents", entry), { recursive: true });
// Installed/candidate help is the authority. These calls are noninteractive.
for (const [exe, args, environment] of [
[binary, ["--version"], controlEnv], [binary, ["registry", "--help"], controlEnv],
[binary, ["pane", "--help"], controlEnv], [binary, ["agent", "--help"], controlEnv],
[binary, ["session", "--help"], controlEnv], [opencode, ["--version"], controlEnv],
[opencode, ["--help"], controlEnv], [parentBinary, ["pane", "split", "--help"], process.env],
] as [string, string[], Record<string, string>][]) await exec(exe, args, environment);
await exec(binary, ["registry", "validate", source]);
assert(!(await sessions()).some((item) => item.name === sessionName), "refusing to reuse existing session");
const parentBefore = await parent(["pane", "get", process.env.HERDR_PANE_ID!]);
await writeFile(path.join(root, "parent-before.json"), JSON.stringify(parentBefore, null, 2));
const split = await parent(["pane", "split", process.env.HERDR_PANE_ID!, "--direction", "down", "--ratio", "0.3", "--cwd", root, "--no-focus"]);
outerPane = split.pane?.pane_id;
assert(outerPane && outerPane !== process.env.HERDR_PANE_ID, "missing/unsafe outer pane ID");
await namedLaunch("first");
const initial = (await cli(["registry", "status"])).registry;
assert.equal(initial.source, source);
assert(!initial.agents.some((agent: any) => agent.id === "opencode" || agent.id === novel));
await writeFile(path.join(root, "registry-initial.json"), JSON.stringify(initial, null, 2));
const rootPane = (await cli(["pane", "get", pane!])).pane;
const rootProcess = (await cli(["pane", "process-info", "--pane", pane!])).process_info;
assert.equal(rootPane.agent, undefined);
assert(rootPane.cwd && rootProcess.shell_pid, "new disposable root must have a known cwd/shell");
assert((rootProcess.foreground_processes ?? []).every((item: any) => item.pid === rootProcess.shell_pid), "disposable root is not at its shell");
await writeFile(path.join(root, "disposable-root-before-launch.json"), JSON.stringify({ pane: rootPane, process: rootProcess }, null, 2));
// Keep an intermediate noninteractive shell alive while SIGSTOP freezes its OpenCode child.
// Stopping a direct child of the interactive shell would otherwise return its job to the shell.
const ocCommand = `${quote(opencode)} --model herdr-local/fixture --agent build; printf ${quote(`\\nHERDR_E2E_OC_EXIT_${nonce}\\n`)}`;
await cli(["pane", "run", pane!, `cd ${quote(work)} && /bin/bash --noprofile --norc -c ${quote(ocCommand)}`]);
const trackedOpenCode = await poll("real OpenCode foreground", foreground, 30000);
await poll("OpenCode home screen", async () => {
const screen = (await exec(binary, ["pane", "read", pane!, "--source", "recent", "--format", "text"])).stdout;
return /Herdr Fixture|herdr-local|fixture/i.test(screen) ? true : undefined;
}, 60000);
assert(!(await cli(["pane", "get", pane!])).pane.agent, "OpenCode must initially be unrecognized");
await capture("initial-unknown", false);
check("running OpenCode initially unknown", "pass");
// Stop the exact API-observed executable, not a name match or guessed PID. No input during reload.
frozen = trackedOpenCode;
await signalTracked(frozen, "SIGSTOP");
let previous = "";
let stable = 0;
const frozenScreen = await poll("drain pre-SIGSTOP screen", async () => {
const screen = (await exec(binary, ["pane", "read", pane!, "--source", "recent", "--format", "text"])).stdout;
stable = screen === previous ? stable + 1 : 0;
previous = screen;
return stable >= 3 ? { screen } : undefined;
});
const novelDir = path.join(source, "agents", novel);
await mkdir(novelDir);
for (const name of ["agent.toml", "process.toml", "detection.toml", "resume.toml"]) {
let content = await readFile(path.join(agents, "opencode", name), "utf8");
if (name === "agent.toml") {
content = content.replace(/^id = "opencode"$/m, `id = "${novel}"`).replace(/^name = "opencode"$/m, `name = "${novel}"`).replace(/^aliases = .*$/m, 'aliases = []');
content = content.replace(/^unix = "opencode"$/m, `unix = "${delayedLaunchName}"`);
content = content.replace(/\n\[sound\][\s\S]*$/, "\n");
}
if (name === "detection.toml") content = content.replace(/^id = "opencode"$/m, `id = "${novel}"`).replace(/^aliases = .*$/m, 'aliases = []');
// process.toml retains actual installed opencode process names. No integration.toml/assets.
await writeFile(path.join(novelDir, name), content);
}
await exec(binary, ["registry", "validate", source]);
let activated = (await cli(["registry", "reload", source])).registry;
assert(activated.generation > initial.generation && activated.digest !== initial.digest);
const summary = activated.agents.find((agent: any) => agent.id === novel);
assert(summary?.process && summary?.detection && summary?.resume && !summary?.integration);
await poll("same frozen process recognized after reload", async () => (await cli(["pane", "get", pane!])).pane.agent === novel);
const afterScreen = (await exec(binary, ["pane", "read", pane!, "--source", "recent", "--format", "text"])).stdout;
assert.equal(afterScreen, frozenScreen.screen, "reload must not change the pane screen");
await capture("reloaded-frozen");
check("novel registry detects existing stopped OpenCode without input", "pass", { process: trackedOpenCode, screenUnchanged: true, byteCounterAvailable: false });
await signalTracked(frozen, "SIGCONT");
frozen = undefined;
await state("idle");
await capture("recognized-idle");
await writeFile(path.join(root, "recognized-idle.layout.json"), JSON.stringify(await cli(["pane", "layout"]), null, 2));
const idleScreen = await readFile(path.join(root, "recognized-idle.txt"), "utf8");
await cli(["pane", "send-keys", pane!, "Ctrl+p"]);
await poll("OpenCode command palette paints", async () => {
const screen = (await exec(binary, ["pane", "read", pane!, "--source", "recent", "--format", "text"])).stdout;
return screen !== idleScreen && /command/i.test(screen) ? true : undefined;
});
await capture("command-palette");
const paletteExplain = json((await exec(binary, ["agent", "explain", pane!, "--json"])).stdout);
assert.equal(paletteExplain.state, "unknown");
assert.equal(paletteExplain.skip_state_update, true);
assert.equal(paletteExplain.matched_rule?.id, "command_palette");
await cli(["pane", "send-keys", pane!, "Escape"]);
await poll("OpenCode command palette closes", async () => {
const screen = (await exec(binary, ["pane", "read", pane!, "--source", "recent", "--format", "text"])).stdout;
return !/select a command|search commands/i.test(screen) ? true : undefined;
});
await capture("recognized-idle-after-command-palette");
// Publication returned only after journal persistence. Copy the committed evidence now.
assert(sessionInfo);
const journalPath = path.join(sessionInfo.session_dir, "agent-registry/active.json");
const journal = json(await readFile(journalPath, "utf8"));
assert.equal(journal.generation, activated.generation);
assert.equal(journal.digest, activated.digest);
await cp(journalPath, path.join(root, "committed-registry.json"));
check("published registry already persisted", "pass");
await mkdir(path.join(badSource, "agents", novel), { recursive: true });
await writeFile(path.join(badSource, "agents", novel, "agent.toml"), "not valid TOML [[[\n");
const bad = await exec(binary, ["registry", "reload", badSource], controlEnv, true);
assert(bad.code !== 0 && !bad.timedOut, "bad reload must be rejected, not time out");
const retained = (await cli(["registry", "status"])).registry;
assert.equal(retained.generation, activated.generation);
assert.equal(retained.digest, activated.digest);
assert(retained.last_error, "bad reload should be visible in status");
assert.equal(json(await readFile(journalPath, "utf8")).digest, activated.digest);
check("bad reload retains active generation/digest and journal", "pass");
const originalProcess = await foreground();
assert(originalProcess);
const retiredPackage = path.join(root, "retired-package");
await rename(novelDir, retiredPackage);
const retired = (await cli(["registry", "reload", source])).registry;
assert(!retired.agents.some((agent: any) => agent.id === novel));
const denied = await exec(binary, ["agent", "start", "retired-start", "--kind", novel, "--pane", pane!], controlEnv, true);
assert(denied.code !== 0 && !denied.timedOut);
assert.equal(json(denied.stderr || denied.stdout).error?.code, "unsupported_agent_kind");
await turn("COMPLETE", "retained-process");
assert.deepEqual(await foreground(), originalProcess, "removal must retain the same live process");
await rename(retiredPackage, novelDir);
const readded = (await cli(["registry", "reload", source])).registry;
assert.equal(readded.digest, activated.digest);
assert(readded.generation > retired.generation);
activated = readded;
check("removed package blocks new starts but retains live prompting and identity", "pass");
await turn("COMPLETE", "complete");
await turn("PERMISSION", "permission");
await turn("CANCEL", "cancel");
await turn("COMPLETE", "after-cancel");
await exitOpenCode("exited");
check("real OpenCode exit clears identity", "pass");
const nativeID = await localSessionID(env.XDG_DATA_HOME, work);
let beforeNativeRestart: TrackedProcess | undefined;
const resumeOptions = ["--model", "herdr-local/fixture", "--agent", "build"];
if (nativeID) {
await writeFile(path.join(root, "native-session-reference.json"), JSON.stringify({ id: nativeID, source: "isolated OpenCode sqlite (read-only)" }, null, 2));
// Legitimate internal launch path only; never forge a reporter/source or grant hook authority.
const started = await rawApi({ id: "e2e:native-explicit-start", method: "agent.start", params: {
name: "registry-native-resume", kind: novel, pane_id: pane!, args: ["--session", nativeID, ...resumeOptions], timeout_ms: 20000,
} });
assert.equal(started.result?.type, "agent_started");
assert.equal(started.result?.agent?.launch_pending, true);
assert.notEqual(started.result?.agent?.interactive_ready, true);
await rejectPrematurePrompt("native-explicit-resume");
await productReady(true);
await state("idle");
const info = await poll("legitimate native reference capture", async () => {
const info = (await cli(["agent", "get", pane!])).agent;
return info.agent_session?.value === nativeID && info.agent_session?.agent === novel ? info : undefined;
});
assert.equal(info.agent_session.source, "herdr:launch", "native reference must come from internal launch capture, not a reporter");
beforeNativeRestart = await poll("explicit native OpenCode process", foreground);
await capture("native-explicit-resume");
check("explicit native resume launch/reference capture", "pass", { requestedSessionID: nativeID, observedSession: info.agent_session });
// Leave this idle native session running: stopping Herdr now must persist its pinned recipe.
// Exiting OpenCode first would test only registry restart, not native restoration.
} else check("native resume/restore", "prerequisite", "No supported native ID in isolated OpenCode database; no synthetic session report submitted.");
// Remove only our source via rename, retaining it as evidence; restart the same recorded session.
await namedStop("first");
if (nativeID) {
const savedPath = path.join(sessionInfo!.session_dir, "session.json");
const saved = json(await readFile(savedPath, "utf8"));
const references: any[] = [];
const collect = (value: any) => {
if (!value || typeof value !== "object") return;
if (value.agent_session?.agent === novel && value.agent_session?.value === nativeID) references.push(value.agent_session);
for (const child of Object.values(value)) collect(child);
};
collect(saved);
assert(references.some((ref) => ref.source === "herdr:launch" && ref.recipe), "stopped session must persist legitimate native reference plus pinned recipe");
for (const ref of references) assert.deepEqual(ref.resume_options, resumeOptions, "persist options only with the exact accepted native session");
await cp(savedPath, path.join(root, "native-before-restart.session.json"));
check("native reference and pinned recipe persisted", "pass", references);
}
await rename(source, `${source}-removed`);
await namedLaunch("lkg");
const restored = (await cli(["registry", "status"])).registry;
// App startup rebuilds detection overlays via replace_detection, advancing the generation.
// LKG guarantees package bytes, not an unchanged in-memory detection epoch across processes.
assert(restored.generation >= activated.generation, "restart must not regress the published generation");
assert.equal(restored.digest, activated.digest);
const restoredJournal = json(await readFile(path.join(sessionInfo!.session_dir, "agent-registry/active.json"), "utf8"));
assert.deepEqual(restoredJournal.files, journal.files, "LKG must retain exact package bytes without the source");
assert.equal(restoredJournal.generation, restored.generation);
assert(restored.agents.some((agent: any) => agent.id === novel));
await writeFile(path.join(root, "registry-lkg.json"), JSON.stringify(restored, null, 2));
check("restart with missing source reuses persisted LKG", "pass", { beforeGeneration: activated.generation, afterGeneration: restored.generation, packageBytesUnchanged: true, note: "Startup detection rebuild may advance generation" });
if (nativeID) {
const resumedProcess = await poll("automatic native OpenCode restore", foreground, 60000);
assert(beforeNativeRestart && (resumedProcess.pid !== beforeNativeRestart.pid || resumedProcess.start !== beforeNativeRestart.start), "must observe a new OpenCode process after restart");
const argv = (await readFile(`/proc/${resumedProcess.pid}/cmdline`, "utf8")).split("\0").filter(Boolean);
assert.deepEqual(argv.slice(1), ["--session", nativeID, ...resumeOptions], "automatic restore must preserve the pinned native session and allowed original CLI choices");
await capture("native-restored-process-before-ui");
await rejectPrematurePrompt("native-automatic-restore");
await productReady(true);
await state("idle");
const info = (await cli(["agent", "get", pane!])).agent;
assert.equal(info.agent_session?.value, nativeID);
assert.equal(info.agent_session?.agent, novel);
assert.equal(info.agent_session?.source, "herdr:launch");
await capture("native-automatic-restore");
check("automatic native restore from LKG with same session ID", "pass", { before: beforeNativeRestart, after: resumedProcess, argv, observedSession: info.agent_session });
await turn("COMPLETE", "after-native-restore");
await exitOpenCode("native-restored-exited");
}
assert(!provider.events.some((event) => ["stream_error", "invalid_request", "unexpected_endpoint", "duplicate_turn"].includes(event.type)), "provider contract errors; inspect provider.jsonl");
check("model requests handled only by loopback fixture", "pass", { requests: provider.events.filter((event) => event.type === "request").length, model: "herdr-local/fixture", externalModelClients: 0 });
} catch (error) {
failure = error;
check("acceptance", "fail", String(error));
if (outerPane) {
try {
const screen = await exec(parentBinary!, ["pane", "read", outerPane, "--source", "recent", "--format", "text"], process.env as Record<string, string>, true);
await writeFile(path.join(root, "failure-outer.txt"), screen.stdout || screen.stderr);
} catch { /* Preserve original error. */ }
}
if (pane && sessionOwned) {
try { await capture("failure", false); } catch { /* Preserve original error. */ }
}
} finally {
cleaning = true;
const cleanupErrors: string[] = [];
if (frozen) {
try { await signalTracked(frozen, "SIGCONT"); } catch (error) { cleanupErrors.push(`resume owned OpenCode: ${error}`); }
}
// Stop only the unique name checked absent before launch. Re-query for partial startup failures.
let stopped = !sessionMayExist;
if (sessionMayExist) {
try {
const item = (await sessions()).find((item) => item.name === sessionName);
if (item) {
sessionInfo = item;
if (item.running) await exec(binary, ["session", "stop", sessionName, "--json"]);
await poll("cleanup session stopped", async () => !(await sessions()).some((entry) => entry.name === sessionName && entry.running));
// Preserve logs before deleting just this session directory through the official CLI.
await cp(item.session_dir, path.join(root, "stopped-session"), { recursive: true }).catch((error) => cleanupErrors.push(`copy logs: ${error}`));
await exec(binary, ["session", "delete", sessionName, "--json"]);
}
assert(!(await sessions()).some((item) => item.name === sessionName));
stopped = true;
} catch (error) { cleanupErrors.push(`named session cleanup: ${error}`); }
}
if (outerPane && stopped) {
try {
// Wait until its actual foreground job has returned to the original shell before closing.
await poll("outer pane returned to shell", async () => {
const info = (await parent(["pane", "process-info", "--pane", outerPane!])).process_info;
return info.shell_pid && info.foreground_process_group_id === info.shell_pid && (info.foreground_processes ?? []).every((item: any) => item.pid === info.shell_pid) ? true : undefined;
});
await parent(["pane", "close", outerPane]);
} catch (error) { cleanupErrors.push(`outer pane cleanup (left intact for inspection): ${error}`); }
} else if (outerPane) cleanupErrors.push(`outer pane ${outerPane} intentionally retained because named stop was not confirmed`);
await provider.stop();
check("cleanup", cleanupErrors.length ? "fail" : "pass", cleanupErrors);
process.off("SIGINT", interrupt);
process.off("SIGTERM", interrupt);
const report = { at: new Date().toISOString(), binary, opencode, root, sessionName, novel, outerPane, pane, sessionInfo, checks,
limitations: ["Linux only; requires HERDR_ENV=1", "No raw PTY byte counter: SIGSTOP plus unchanged screen evidence", "Native launch/automatic restore run only when an isolated native session ID is available; otherwise explicitly flagged prerequisite", "Loopback model allowlist is not an OS network namespace"] };
await writeFile(path.join(root, "report.json"), JSON.stringify(report, null, 2));
await mkdir(path.join(repo, ".local/prd"), { recursive: true });
appendFileSync(path.join(repo, ".local/prd/registry-dynamic-e2e.md"), `\n## Run ${report.at}\n\nArtifacts: \`${root}\`\nCandidate: \`${binary}\`\nSession: \`${sessionName}\`\n\n${checks.map((item) => `- **${item.status}** ${item.name}${item.detail ? `${JSON.stringify(item.detail)}` : ""}`).join("\n")}\n`);
console.log(`Report: ${root}/report.json`);
if (failure || cleanupErrors.length) throw failure || new Error(cleanupErrors.join("\n"));
}
}
async function main() {
const args = process.argv.slice(2);
if (args.length === 1 && args[0] === "--self-test") return selfTestProvider();
if (args.length === 1 && args[0] === "--provider-only") {
const provider = createFixtureProvider({ onEvent: (event) => console.log(JSON.stringify(event)) });
console.log(`Provider: ${provider.url}/v1; GET /control/status; POST /control/release {"marker":"..."}`);
await new Promise<void>((resolve) => {
process.once("SIGINT", resolve);
process.once("SIGTERM", resolve);
});
await provider.stop();
return;
}
if (args.length === 2 && args[0] === "--binary") return runHarness(args[1]);
console.log("Usage: bun run scripts/agent_registry_opencode_e2e.ts --binary target/debug/herdr\n bun run scripts/agent_registry_opencode_e2e.ts --self-test | --provider-only\nRequires local Linux inside Herdr; compile candidate first. Leaves evidence under /var/tmp, cleans only owned runtime resources.");
if (args.length && !args.includes("--help")) process.exitCode = 2;
}
if (import.meta.main) main().catch((error) => { console.error(error); process.exitCode = 1; });
-192
View File
@@ -1,192 +0,0 @@
// Loopback-only deterministic model fixture. No upstream client, credentials, or forwarding.
import assert from "node:assert/strict";
export type Mode = "COMPLETE" | "PERMISSION" | "CANCEL";
export type ProviderEvent = { at: string; type: string; marker?: string; [key: string]: unknown };
type Gate = { marker: string; mode: Mode; release: () => void; aborted: boolean; released: boolean };
const encoder = new TextEncoder();
const markerPattern = /HERDR_E2E_(COMPLETE|PERMISSION|CANCEL)_[a-zA-Z0-9_-]+/g;
function text(content: unknown): string {
if (typeof content === "string") return content;
if (Array.isArray(content)) return content.map((part) => text(part?.text)).join("\n");
return "";
}
/** Route by the most recent marked user turn, not request order (auxiliary calls may race). */
function route(body: any): { marker: string; mode: Mode; toolResult: boolean } | undefined {
const messages: any[] = Array.isArray(body.messages) ? body.messages : [];
for (let i = messages.length - 1; i >= 0; i--) {
if (messages[i].role !== "user") continue;
const matches = [...text(messages[i].content).matchAll(markerPattern)];
const match = matches.at(-1);
if (match) return {
marker: match[0], mode: match[1] as Mode,
toolResult: messages.slice(i + 1).some((m) => m.role === "tool"),
};
}
}
export function createFixtureProvider(options: { port?: number; onEvent?: (event: ProviderEvent) => void } = {}) {
const events: ProviderEvent[] = [];
const gates = new Map<string, Gate>();
const liveStreams = new Set<() => void>();
const event = (type: string, details: Record<string, unknown> = {}) => {
const item = { at: new Date().toISOString(), type, ...details };
events.push(item);
options.onEvent?.(item);
};
const server = Bun.serve({
hostname: "127.0.0.1", port: options.port ?? 0, idleTimeout: 0,
async fetch(request) {
const url = new URL(request.url);
if (url.pathname === "/control/status" && request.method === "GET") {
return Response.json({ events, gates: [...gates.values()].map(({ marker, mode, aborted, released }) => ({ marker, mode, aborted, released })) });
}
if (url.pathname === "/control/release" && request.method === "POST") {
const { marker } = await request.json() as { marker: string };
const gate = gates.get(marker);
if (!gate || gate.aborted || gate.released) return Response.json({ error: "no pending gate" }, { status: 409 });
gate.released = true;
event("released", { marker });
gate.release();
return Response.json({ released: marker });
}
if (url.pathname !== "/v1/chat/completions" || request.method !== "POST") {
event("unexpected_endpoint", { path: url.pathname, method: request.method });
return Response.json({ error: { message: "fixture only supports POST /v1/chat/completions" } }, { status: 404 });
}
const body = await request.json() as any;
if (body.model !== "fixture" || body.stream !== true) {
event("invalid_request", { body });
return Response.json({ error: { message: "fixture requires model=fixture, stream=true" } }, { status: 400 });
}
const turn = route(body);
event("request", { marker: turn?.marker, toolResult: turn?.toolResult, body });
if (turn && !turn.toolResult && gates.has(turn.marker)) {
event("duplicate_turn", { marker: turn.marker });
return Response.json({ error: { message: "duplicate initial turn marker" } }, { status: 409 });
}
// Abort and cancel both unblock the writer. No heartbeat/text is emitted behind the gate.
let closed = false;
let gate: Gate | undefined;
let unblock = () => {};
const wait = new Promise<void>((resolve) => { unblock = resolve; });
const abort = () => {
if (closed) return;
if (gate) gate.aborted = true;
event("aborted", { marker: turn?.marker });
closed = true;
unblock();
liveStreams.delete(abort);
};
request.signal.addEventListener("abort", abort, { once: true });
if (turn && !turn.toolResult) {
gate = { marker: turn.marker, mode: turn.mode, release: unblock, aborted: false, released: false };
gates.set(turn.marker, gate);
}
liveStreams.add(abort);
const stream = new ReadableStream<Uint8Array>({
async start(controller) {
const send = (delta: object, finish_reason: string | null = null) => {
if (closed) return;
controller.enqueue(encoder.encode(`data: ${JSON.stringify({
id: "chatcmpl-herdr-fixture", object: "chat.completion.chunk", created: 1, model: "fixture",
choices: [{ index: 0, delta, finish_reason }],
...(finish_reason ? { usage: { prompt_tokens: 1, completion_tokens: 1, total_tokens: 2 } } : {}),
})}\n\n`));
};
try {
send({ role: "assistant" });
if (gate) {
send({ content: "Local fixture is processing.\n" });
event("gated", { marker: gate.marker, mode: gate.mode });
await wait;
}
if (closed) return;
if (turn?.mode === "PERMISSION" && !turn.toolResult) {
// Use only the advertised tool. Fail closed if the installed schema changes.
const tool = body.tools?.find((t: any) => t.type === "function" && t.function?.name === "bash");
assert(tool, "installed OpenCode did not advertise bash");
const args: Record<string, unknown> = { command: "printf 'HERDR_PERMISSION_PROBE\\n'" };
for (const key of tool.function.parameters?.required ?? []) {
if (key === "description") args.description = "Print a harmless local probe (reject this request)";
else assert(key in args, `unsupported required bash argument: ${key}`);
}
send({ tool_calls: [{ index: 0, id: `call_${turn.marker}`, type: "function", function: { name: "bash", arguments: JSON.stringify(args) } }] });
send({}, "tool_calls");
event("tool_call", { marker: turn.marker, args });
} else {
send({ content: turn?.toolResult ? "Permission rejected; fixture complete." : "Fixture complete." });
send({}, "stop");
}
controller.enqueue(encoder.encode("data: [DONE]\n\n"));
closed = true;
controller.close();
event("finished", { marker: turn?.marker, toolResult: turn?.toolResult });
} catch (error) {
event("stream_error", { marker: turn?.marker, error: String(error) });
closed = true;
controller.error(error);
} finally {
request.signal.removeEventListener("abort", abort);
liveStreams.delete(abort);
}
},
cancel: abort,
});
return new Response(stream, { headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" } });
},
});
return {
url: `http://127.0.0.1:${server.port}`, events,
async release(marker: string) {
const response = await fetch(`http://127.0.0.1:${server.port}/control/release`, {
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ marker }),
});
assert(response.ok, `release ${marker}: ${await response.text()}`);
},
async stop() {
for (const abort of liveStreams) abort();
await server.stop(true);
},
};
}
/** Provider-only contract check: no Herdr/OpenCode processes or external requests. */
export async function selfTestProvider() {
const provider = createFixtureProvider();
const request = (marker: string, more: object[] = [], signal?: AbortSignal) => fetch(`${provider.url}/v1/chat/completions`, {
method: "POST", headers: { "Content-Type": "application/json" }, signal,
body: JSON.stringify({ model: "fixture", stream: true, messages: [{ role: "user", content: marker }, ...more],
tools: [{ type: "function", function: { name: "bash", parameters: { required: ["command"] } } }] }),
});
try {
// Independent concurrent markers prove that gates are not keyed by call count.
const complete = "HERDR_E2E_COMPLETE_selftest";
const permission = "HERDR_E2E_PERMISSION_selftest";
const a = await request(complete);
const b = await request(permission);
const readA = a.text();
const readB = b.text();
await provider.release(permission);
assert.match(await readB, /tool_calls/);
assert(!provider.events.some((e) => e.type === "finished" && e.marker === complete));
await provider.release(complete);
assert.match(await readA, /\[DONE\]/);
const followup = await request(permission, [{ role: "assistant", content: null, tool_calls: [] }, { role: "tool", tool_call_id: `call_${permission}`, content: "Rejected" }]);
assert.match(await followup.text(), /Permission rejected/);
const controller = new AbortController();
const cancel = "HERDR_E2E_CANCEL_selftest";
const response = await request(cancel, [], controller.signal);
const reader = response.body!.getReader();
await reader.read();
controller.abort();
await reader.cancel().catch(() => {});
const deadline = Date.now() + 3000;
while (!provider.events.some((e) => e.type === "aborted" && e.marker === cancel) && Date.now() < deadline) await Bun.sleep(20);
assert(provider.events.some((e) => e.type === "aborted" && e.marker === cancel), "cancel closes stream");
assert(!provider.events.some((e) => ["stream_error", "invalid_request", "duplicate_turn"].includes(e.type)));
console.log("PASS: provider concurrent gates, tool result, completion and abort (loopback only)");
} finally { await provider.stop(); }
}