bump deps
This commit is contained in:
+387
-355
@@ -1,384 +1,416 @@
|
||||
// Opt-in real-host check: OPENCODE_2015=/absolute/path/to/2.0.15/opencode bun scripts/test-multi-work-host.ts
|
||||
// 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 } from "node:fs/promises";
|
||||
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/herdr.js";
|
||||
import { z } from "zod";
|
||||
|
||||
const binary = process.env.OPENCODE_2015;
|
||||
if (!binary) throw new Error("Set OPENCODE_2015 to an isolated OpenCode 2.0.15 binary");
|
||||
const temp = await mkdtemp("/tmp/opencode/native-multi-work-");
|
||||
console.log(`Fixture retained at ${temp}`);
|
||||
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.15")
|
||||
throw new Error("Exact 2.0.15 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") },
|
||||
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",
|
||||
],
|
||||
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<void>();
|
||||
const releaseModel = Promise.withResolvers<void>();
|
||||
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,
|
||||
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") },
|
||||
],
|
||||
};
|
||||
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<void>((done) => socket.listen(0, "127.0.0.1", done));
|
||||
const { port } = z.object({ port: z.number() }).parse(socket.address());
|
||||
await new Promise<void>((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")),
|
||||
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<void>();
|
||||
const releaseModel = Promise.withResolvers<void>();
|
||||
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" } },
|
||||
);
|
||||
},
|
||||
});
|
||||
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 {
|
||||
const socket = createServer();
|
||||
await new Promise<void>((done) => socket.listen(0, "127.0.0.1", done));
|
||||
const { port } = z.object({ port: z.number() }).parse(socket.address());
|
||||
await new Promise<void>((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);
|
||||
}
|
||||
}
|
||||
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(
|
||||
{
|
||||
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, 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<never>((_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") },
|
||||
],
|
||||
]) {
|
||||
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<never>((_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-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, update: "disable" }),
|
||||
JSON.stringify({
|
||||
plugins: [{ package: join(temp, "plugin", "quota") }],
|
||||
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)}`,
|
||||
);
|
||||
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);
|
||||
}
|
||||
server.kill();
|
||||
await server.exited;
|
||||
const quotaBuild = await Bun.build({
|
||||
entrypoints: [resolve("src/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);
|
||||
await rm(temp, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user