import test from "node:test";
import assert from "node:assert/strict";
import http from "node:http";
import { readFileSync } from "node:fs";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import {
BUILTIN_TOOL_IDS,
OPENCODE_CONFIGS,
OpencodeClient,
buildServerEnv,
ciFullProblems,
createSseParser,
externalDirectoryProblem,
httpRequest,
isTransientError,
modelVariantIds,
resolveOpencodeConfig,
selfCheck,
shouldRetry,
toolAllowedWithoutAsking,
toolEnabled,
wildcardMatch,
wrapUpStartMs,
} from "../src/opencode.mjs";
import { stripJsonc } from "./jsonc.mjs";
const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
const AGENT_RUN_MS = 1500;
const HEADERS_TIMEOUT_MS = 1000;
function fakeOpencode({ idleAfterMs = AGENT_RUN_MS, finalEvent = "session.execution.succeeded", error = null, errorBeforeAck = null, eventStream = true, wrapUpAfterMs = null, interruptEventDelayMs = 50 } = {}) {
const calls = [];
let prompts = 0;
const streams = new Set();
let idle = null;
const emit = (type, data) => {
for (const res of streams) res.write(`data: ${JSON.stringify({ id: "evt", type, data })}\n\n`);
};
const finishRun = () => {
idle = { id: `idle_${prompts}`, type: "idle", outcome: finalEvent === "session.execution.succeeded" ? "succeeded" : "failed", ...(error ? { error } : {}) };
if (eventStream) emit(finalEvent, { sessionID: "ses_1", ...(error ? { error } : {}) });
};
const server = http.createServer((req, res) => {
let body = "";
req.on("data", chunk => { body += chunk; });
req.on("end", () => {
calls.push({ method: req.method, url: req.url, body: body ? JSON.parse(body) : null, auth: req.headers.authorization });
const send = (status, payload) => {
res.writeHead(status, { "Content-Type": "application/json" });
res.end(payload === undefined ? "" : JSON.stringify(payload));
};
if (req.url === "/api/event") {
res.writeHead(200, { "Content-Type": "text/event-stream" });
res.write(`data: ${JSON.stringify({ id: "evt", type: "server.connected", data: {} })}\n\n: heartbeat\n\n`);
if (eventStream) streams.add(res);
res.on("close", () => streams.delete(res));
return;
}
// 2.x serves its web app for every unknown route, including all of 1.x's.
if (!req.url.startsWith("/api/")) {
res.writeHead(200, { "Content-Type": "text/html" });
res.end("");
return;
}
if (req.method === "POST" && req.url === "/api/session") return send(200, { data: { id: "ses_1" } });
if (req.method === "PUT" && req.url === "/api/experimental/session/ses_1/instructions/entries/review-instructions") {
res.writeHead(204);
res.end();
return;
}
if (req.method === "POST" && req.url === "/api/session/ses_1/prompt") {
if (errorBeforeAck) {
emit("session.execution.failed", { sessionID: "ses_1", error: errorBeforeAck });
setTimeout(() => send(200, { data: { id: "msg_u1" } }), 200);
return;
}
prompts += 1;
send(200, { data: { id: `msg_u${prompts}` } });
// The second prompt is the wrap-up: it answers, where the first ran on until interrupted.
if (wrapUpAfterMs !== null && prompts > 1) {
setTimeout(finishRun, wrapUpAfterMs);
return;
}
setTimeout(() => emit("permission.asked", { id: "per_1", sessionID: "ses_1", action: "shell", resources: ["*"] }), 50);
if (idleAfterMs !== null) setTimeout(finishRun, idleAfterMs);
return;
}
// A blocking call: no headers until the agent run ends (what 1.x's /session/:id/message did).
if (req.method === "POST" && req.url === "/api/slow") {
setTimeout(() => send(200, { data: {} }), AGENT_RUN_MS);
return;
}
if (req.method === "GET" && req.url === "/api/session/ses_1/message?type=assistant&order=desc&limit=1") {
return send(200, {
data: [{
id: "msg_a2",
type: "assistant",
model: { id: "m", providerID: "p" },
time: { created: 1, completed: 2 },
content: [{ type: "reasoning", text: "thinking" }, { type: "text", text: "" }],
finish: "stop",
...(error && finalEvent === "session.execution.succeeded" ? { error } : {}),
}],
});
}
if (req.method === "GET" && req.url === "/api/session/ses_1/message?type=idle&order=desc&limit=1") return send(200, { data: idle ? [idle] : [] });
if (req.method === "GET" && req.url === "/api/session/ses_1") return send(200, { data: { id: "ses_1", tokens: { input: 2000, output: 200, reasoning: 0, cache: { read: 0, write: 0 } } } });
if (req.method === "POST" && req.url === "/api/session/ses_1/permission/per_1/reply") {
res.writeHead(204);
res.end();
return;
}
if (req.method === "POST" && req.url === "/api/session/ses_1/interrupt") {
idle = { id: `idle_${prompts}`, type: "idle", outcome: "interrupted" };
setTimeout(() => emit("session.execution.interrupted", { sessionID: "ses_1" }), interruptEventDelayMs);
return send(200, { interrupted: true });
}
if (req.method === "DELETE" && req.url === "/api/session/ses_1") {
res.writeHead(204);
res.end();
return;
}
send(404, { _tag: "NotFound" });
});
});
return new Promise(resolve => {
server.listen(0, "127.0.0.1", () => {
const { port } = server.address();
resolve({
baseUrl: `http://127.0.0.1:${port}`,
calls,
close: () => new Promise(done => {
for (const res of streams) res.end();
server.closeAllConnections();
server.close(done);
}),
});
});
});
}
const PROMPT = { agent: "a", model: { providerID: "p", modelID: "m" }, system: "", text: "", title: "t" };
test("C12 regression: a blocking request dies on the headers timeout, the async transport does not", async () => {
const fake = await fakeOpencode();
try {
await assert.rejects(
httpRequest(fake.baseUrl, "POST", "/api/slow", { body: {}, headersTimeoutMs: HEADERS_TIMEOUT_MS }),
err => err.name === "HeadersTimeoutError",
);
const client = new OpencodeClient({ baseUrl: fake.baseUrl, password: "pw", headersTimeoutMs: HEADERS_TIMEOUT_MS });
const result = await client.prompt({
agent: "ai-review-security",
model: { providerID: "anthropic-review", modelID: "claude-sonnet-5-5" },
variant: "high",
system: "rules",
text: "diff",
title: "t",
timeoutMs: 10_000,
});
client.close();
assert.equal(result.text, "");
assert.equal(result.responseId, "msg_a2");
assert.equal(result.info.tokens.input, 2000, "tokens are the session total, not the last step");
assert.equal(result.rejectedPermissions, 1);
const reply = fake.calls.find(c => c.url === "/api/session/ses_1/permission/per_1/reply");
assert.equal(reply.body.decision, "reject");
const create = fake.calls.find(c => c.method === "POST" && c.url === "/api/session");
assert.equal(create.body.agent, "ai-review-security");
assert.deepEqual(create.body.model, { providerID: "anthropic-review", id: "claude-sonnet-5-5", variant: "high" });
const system = fake.calls.find(c => c.method === "PUT");
assert.equal(system.body.value, "rules");
const prompt = fake.calls.find(c => c.url === "/api/session/ses_1/prompt");
assert.deepEqual(prompt.body, { text: "diff" });
assert.equal(prompt.auth, `Basic ${Buffer.from("opencode:pw").toString("base64")}`);
assert.ok(fake.calls.some(c => c.method === "DELETE" && c.url === "/api/session/ses_1"));
} finally {
await fake.close();
}
});
test("ci-full approves a permission prompt once instead of rejecting it", async () => {
const fake = await fakeOpencode({ idleAfterMs: 200 });
try {
const client = new OpencodeClient({ baseUrl: fake.baseUrl, password: "pw", readOnly: false });
const result = await client.prompt({ ...PROMPT, timeoutMs: 5000 });
client.close();
assert.equal(result.rejectedPermissions, 0);
const reply = fake.calls.find(c => c.url === "/api/session/ses_1/permission/per_1/reply");
assert.equal(reply.body.decision, "once");
} finally {
await fake.close();
}
});
test("without the event stream, the idle poll still ends the wait", async () => {
const fake = await fakeOpencode({ idleAfterMs: 100, eventStream: false });
try {
const client = new OpencodeClient({ baseUrl: fake.baseUrl, password: "pw", statusPollMs: 100 });
const result = await client.prompt({ ...PROMPT, timeoutMs: 5000 });
client.close();
assert.equal(result.text, "");
} finally {
await fake.close();
}
});
test("a timed-out agent is interrupted server-side and not reported as transient", async () => {
const fake = await fakeOpencode({ idleAfterMs: null });
try {
const client = new OpencodeClient({ baseUrl: fake.baseUrl, password: "pw" });
await assert.rejects(
client.prompt({ ...PROMPT, timeoutMs: 300 }),
err => {
assert.equal(err.name, "TimeoutError");
assert.equal(isTransientError(err), false);
return true;
},
);
client.close();
assert.ok(fake.calls.some(c => c.method === "POST" && c.url === "/api/session/ses_1/interrupt"));
assert.ok(fake.calls.some(c => c.method === "DELETE" && c.url === "/api/session/ses_1"));
} finally {
await fake.close();
}
});
test("a reviewer still running near its timeout is interrupted and asked for its findings in the same session", async () => {
const fake = await fakeOpencode({ idleAfterMs: null, wrapUpAfterMs: 100 });
try {
const client = new OpencodeClient({ baseUrl: fake.baseUrl, password: "pw", statusPollMs: 100 });
const started = Date.now();
const result = await client.prompt({ ...PROMPT, timeoutMs: 3000, wrapUp: { ms: 1000, text: "wrap up now" } });
client.close();
assert.ok(Date.now() - started < 3000, "waited for the hard timeout instead of the wrap-up answer");
assert.equal(result.wrappedUp, true);
assert.equal(result.text, "");
assert.equal(result.info.tokens.input, 2000, "tokens are the whole session's, both prompts included");
const prompts = fake.calls.filter(c => c.url === "/api/session/ses_1/prompt");
assert.deepEqual(prompts.map(c => c.body.text), ["", "wrap up now"]);
assert.equal(fake.calls.filter(c => c.url === "/api/session/ses_1/interrupt").length, 1);
} finally {
await fake.close();
}
});
test("an interrupted event that arrives after the wrap-up prompt went out does not end the wrap-up", async () => {
// The poll confirms the interrupt first; the stream's interrupted event lands while the follow-up is running.
const fake = await fakeOpencode({ idleAfterMs: null, wrapUpAfterMs: 600, interruptEventDelayMs: 400 });
try {
const client = new OpencodeClient({ baseUrl: fake.baseUrl, password: "pw", statusPollMs: 100 });
const result = await client.prompt({ ...PROMPT, timeoutMs: 4000, wrapUp: { ms: 2000, text: "wrap up now" } });
client.close();
assert.equal(result.wrappedUp, true);
assert.equal(result.text, "");
assert.equal(fake.calls.filter(c => c.url === "/api/session/ses_1/prompt").length, 2);
} finally {
await fake.close();
}
});
test("a wrap-up that does not answer in its slot still ends as a hard timeout", async () => {
const fake = await fakeOpencode({ idleAfterMs: null });
try {
const client = new OpencodeClient({ baseUrl: fake.baseUrl, password: "pw", statusPollMs: 100 });
await assert.rejects(client.prompt({ ...PROMPT, timeoutMs: 1600, wrapUp: { ms: 800, text: "wrap up now" } }), err => err.name === "TimeoutError");
client.close();
assert.equal(fake.calls.filter(c => c.url === "/api/session/ses_1/prompt").length, 2);
} finally {
await fake.close();
}
});
test("the wrap-up slot is taken only from a session at least twice its length", () => {
const wrapUp = { ms: 3 * 60_000, text: "wrap" };
assert.equal(wrapUpStartMs(20 * 60_000, wrapUp), 17 * 60_000);
assert.equal(wrapUpStartMs(6 * 60_000, wrapUp), 3 * 60_000);
assert.equal(wrapUpStartMs(5 * 60_000, wrapUp), 5 * 60_000);
assert.equal(wrapUpStartMs(20 * 60_000, null), 20 * 60_000);
assert.equal(wrapUpStartMs(20 * 60_000, { ms: 60_000, text: "" }), 20 * 60_000);
});
test("a provider rate limit surfaced as session.execution.failed is transient", async () => {
const error = { type: "provider.rate-limit", message: "Too Many Requests", status: 429 };
const fake = await fakeOpencode({ idleAfterMs: 100, finalEvent: "session.execution.failed", error });
try {
const client = new OpencodeClient({ baseUrl: fake.baseUrl, password: "pw" });
await assert.rejects(
client.prompt({ ...PROMPT, timeoutMs: 5000 }),
err => isTransientError(err) && /Too Many Requests/.test(err.message),
);
client.close();
} finally {
await fake.close();
}
});
test("an auth failure is permanent even when its message looks transient", async () => {
const error = { type: "provider.auth", message: "rate limit: invalid key", status: 401 };
const fake = await fakeOpencode({ idleAfterMs: 100, finalEvent: "session.execution.failed", error });
try {
const client = new OpencodeClient({ baseUrl: fake.baseUrl, password: "pw" });
await assert.rejects(client.prompt({ ...PROMPT, timeoutMs: 5000 }), err => !isTransientError(err) && /provider\.auth/.test(err.message));
client.close();
} finally {
await fake.close();
}
});
test("a run failure emitted before the prompt call returns is not lost", async () => {
const error = { type: "provider.auth", message: "bad key", status: 401 };
const fake = await fakeOpencode({ idleAfterMs: null, errorBeforeAck: error });
try {
const client = new OpencodeClient({ baseUrl: fake.baseUrl, password: "pw" });
const started = Date.now();
await assert.rejects(
client.prompt({ ...PROMPT, timeoutMs: 5000 }),
err => err.name !== "TimeoutError" && /bad key/.test(err.message),
);
assert.ok(Date.now() - started < 2000, "waited for the timeout instead of the early error");
client.close();
} finally {
await fake.close();
}
});
test("a 1.x route answered with the 2.x web app's HTML fails loudly", async () => {
const fake = await fakeOpencode();
try {
const client = new OpencodeClient({ baseUrl: fake.baseUrl, password: "pw" });
await assert.rejects(client.json("GET", "/agent"), /non-JSON.*opencode 2\.x/);
} finally {
await fake.close();
}
});
test("a retry needs room for its own timeout plus the work reserved after it", () => {
const err = Object.assign(new Error("429 Too Many Requests"), { transient: true });
const minute = 60_000;
const base = { err, maxRetries: 2, timeoutMs: 10 * minute, reserveMs: 3 * minute };
assert.equal(shouldRetry({ ...base, attempt: 1, remainingMs: 19 * minute }), true);
assert.equal(shouldRetry({ ...base, attempt: 1, remainingMs: 12 * minute }), false, "the coordinator would be pushed past the watchdog");
assert.equal(shouldRetry({ ...base, attempt: 3, remainingMs: 60 * minute }), false);
assert.equal(shouldRetry({ ...base, attempt: 1, remainingMs: 60 * minute, err: new Error("schema mismatch") }), false);
const timeout = Object.assign(new Error("timed out"), { name: "TimeoutError", transient: true });
assert.equal(shouldRetry({ ...base, attempt: 1, remainingMs: 60 * minute, err: timeout }), false);
});
test("server env is an allowlist: no GitHub, Actions or trace variables reach opencode", () => {
const env = buildServerEnv({
parentEnv: {
PATH: "/usr/bin",
GH_TOKEN: "ghs_secret",
GITHUB_TOKEN: "ghs_secret",
ACTIONS_RUNTIME_TOKEN: "x",
O2_TRACE_PASSWORD: "x",
RUNNER_TEMP: "/tmp",
ANTHROPIC_REVIEW_API_KEY: "sk-123",
ANTHROPIC_API_KEY: "sk-789",
OTHER_API_KEY: "sk-456",
},
home: "/tmp/h",
configPath: "/engine/opencode.jsonc",
password: "pw",
providerKeyVars: ["ANTHROPIC_REVIEW_API_KEY", "bad name"],
});
assert.equal(env.ANTHROPIC_REVIEW_API_KEY, "sk-123");
for (const name of ["GH_TOKEN", "GITHUB_TOKEN", "ACTIONS_RUNTIME_TOKEN", "O2_TRACE_PASSWORD", "RUNNER_TEMP", "OTHER_API_KEY", "ANTHROPIC_API_KEY"]) {
assert.equal(env[name], undefined, name);
}
assert.equal(env.OPENCODE_DISABLE_PROJECT_CONFIG, "true");
assert.equal(env.OPENCODE_CONFIG_PROJECT_DISABLE, "true");
assert.equal(env.OPENCODE_DISABLE_FREE_MODELS, "true");
assert.equal(env.OPENCODE_CONFIG, "/engine/opencode.jsonc");
assert.equal(env.HOME, "/tmp/h");
assert.equal(env.XDG_CONFIG_HOME, "/tmp/h/.config");
});
const rule = (action, resource, effect) => ({ action, resource, effect });
test("toolEnabled follows opencode's last-match-wins evaluation", () => {
const ruleset = [
rule("*", "*", "allow"),
rule("read", "*.env", "ask"),
rule("*", "*", "deny"),
rule("read", "*", "allow"),
rule("glob", "*", "allow"),
rule("grep", "*", "allow"),
// opencode appends this to every agent; it narrows skill further and must not count as enabling it.
rule("skill", "browser", "deny"),
];
assert.equal(toolEnabled("read", ruleset), true);
assert.equal(toolEnabled("shell", ruleset), false);
assert.equal(toolEnabled("write", ruleset), false);
assert.equal(toolEnabled("webfetch", ruleset), false);
assert.equal(toolEnabled("skill", ruleset), false);
assert.equal(toolEnabled("shell", [...ruleset, rule("shell", "git *", "allow")]), true);
assert.equal(toolEnabled("write", [...ruleset, rule("edit", "docs/*", "ask")]), true);
});
function fakeClient({ agents, plugins = BUILTIN_PLUGINS, mcp = [] }) {
const routes = { "/api/agent": { data: agents }, "/api/plugin": { data: plugins }, "/api/mcp": { data: mcp } };
return { json: async (method, path) => routes[path] };
}
const BUILTIN_PLUGINS = ["opencode.agent", "opencode.skill", ...[...BUILTIN_TOOL_IDS].map(id => `opencode.tool.${id}`)]
.map(id => ({ id, source: { type: "builtin" }, state: { status: "active" } }));
const BASE = [rule("*", "*", "allow"), rule("read", "*.env", "ask")];
const READ_ONLY = [
...BASE,
rule("*", "*", "deny"),
rule("read", "*", "allow"),
rule("glob", "*", "allow"),
rule("grep", "*", "allow"),
rule("external_directory", "*", "deny"),
rule("browser", "*", "deny"),
rule("skill", "browser", "deny"),
];
const FULL = [...BASE, rule("*", "*", "allow"), rule("browser", "*", "deny")];
const CI_FULL = [
...BASE,
rule("*", "*", "allow"),
...["webfetch", "websearch", "subagent", "question", "execute", "skill"].map(id => rule(id, "*", "deny")),
rule("shell", "*", "deny"),
rule("shell", "git diff *", "allow"),
rule("browser", "*", "deny"),
];
const agent = (permissions, id = "ai-review-security") => ({ id, name: id, mode: "primary", permissions });
test("self-check passes only for read-only agents and built-in plugins", async () => {
const names = ["ai-review-security"];
await selfCheck(fakeClient({ agents: [agent(READ_ONLY)] }), names);
await assert.rejects(
selfCheck(fakeClient({ agents: [agent(READ_ONLY)], plugins: [...BUILTIN_PLUGINS, { id: "x", source: { type: "local", path: "/pr/.opencode/plugin/x.ts" }, state: { status: "active" } }] }), names),
/non-built-in plugins \(x\)/,
);
await assert.rejects(selfCheck(fakeClient({ agents: [agent(READ_ONLY)], mcp: [{ id: "m" }] }), names), /MCP/);
await assert.rejects(selfCheck(fakeClient({ agents: [agent(READ_ONLY)], plugins: [] }), names), /tools did not load: read, glob, grep/);
await assert.rejects(
selfCheck(fakeClient({ agents: [agent(READ_ONLY)], plugins: [...BUILTIN_PLUGINS, { id: "opencode.tool.x", source: { type: "builtin" }, state: { status: "failed", error: "boom" } }] }), names),
/failed to load: opencode.tool.x: boom/,
);
await assert.rejects(selfCheck(fakeClient({ agents: [agent([...READ_ONLY, rule("shell", "*", "allow")])] }), names), /shell allowed/);
await assert.rejects(selfCheck(fakeClient({ agents: [agent(READ_ONLY)] }), [...names, "ai-review-qa"]), /ai-review-qa: not registered/);
await assert.rejects(
selfCheck(fakeClient({ agents: [agent([...READ_ONLY, rule("external_directory", "/proc/*", "allow")])] }), names),
/external_directory allows \/proc\/\*/,
);
await assert.rejects(selfCheck(fakeClient({ agents: [agent(FULL)] }), names), /shell allowed/, "the default config must refuse a full grant");
});
test("ci-full self-check wants file tools granted, web and sub-agent tools denied and the shell allowlisted", async () => {
const names = ["ai-review-security", "ai-review-correctness"];
const agents = [agent(CI_FULL), agent(CI_FULL, "ai-review-correctness")];
const result = await selfCheck(fakeClient({ agents }), names, { readOnly: false });
assert.equal(result.agents, 2);
await assert.rejects(selfCheck(fakeClient({ agents: [agent(CI_FULL)] }), names, { readOnly: false }), /ai-review-correctness: not registered/);
await assert.rejects(
selfCheck(fakeClient({ agents: [agent(READ_ONLY), agent(CI_FULL, "ai-review-correctness")] }), names, { readOnly: false }),
/ai-review-security: edit not fully allowed/,
);
assert.deepEqual(ciFullProblems(FULL), ["webfetch allowed", "websearch allowed", "subagent allowed", "question allowed", "execute allowed", "skill allowed", "shell allows every command"]);
assert.deepEqual(ciFullProblems([...CI_FULL, rule("execute", "*", "allow")]), ["execute allowed"]);
assert.deepEqual(ciFullProblems([...CI_FULL, rule("shell", "*", "deny")]), ["shell denied"]);
assert.deepEqual(ciFullProblems(CI_FULL), []);
await assert.rejects(
selfCheck(fakeClient({ agents, plugins: [...BUILTIN_PLUGINS, { id: "p", source: { type: "package", target: "evil" }, state: { status: "active" } }] }), names, { readOnly: false }),
/non-built-in plugins/,
);
await assert.rejects(
selfCheck(fakeClient({ agents, plugins: BUILTIN_PLUGINS.filter(p => p.id !== "opencode.tool.shell") }), names, { readOnly: false }),
/tools did not load: shell/,
);
});
test("REVIEW_OPENCODE_CONFIG selects the config file and permission mode, and fails closed on anything else", () => {
assert.deepEqual(resolveOpencodeConfig(undefined), { name: "default", file: "opencode.jsonc", readOnly: true });
assert.deepEqual(resolveOpencodeConfig(""), { name: "default", file: "opencode.jsonc", readOnly: true });
assert.deepEqual(resolveOpencodeConfig("default"), { name: "default", file: "opencode.jsonc", readOnly: true });
assert.deepEqual(resolveOpencodeConfig("ci-full"), { name: "ci-full", file: "opencode.ci.jsonc", readOnly: false });
assert.throws(() => resolveOpencodeConfig("full"), /unknown REVIEW_OPENCODE_CONFIG "full"/);
});
test("both configs register the same agents, provider and model; only the default denies tools", () => {
const load = file => JSON.parse(stripJsonc(readFileSync(join(REPO_ROOT, file), "utf-8")));
const base = load(OPENCODE_CONFIGS.default.file);
const ci = load(OPENCODE_CONFIGS["ci-full"].file);
const expected = [
"ai-review-security", "ai-review-correctness", "ai-review-code-quality", "ai-review-performance", "ai-review-documentation",
"ai-review-release", "ai-review-frontend", "ai-review-qa", "ai-review-coordinator",
];
assert.deepEqual(Object.keys(base.agents).sort(), [...expected].sort());
assert.deepEqual(Object.keys(ci.agents).sort(), [...expected].sort());
assert.deepEqual(ci.providers, base.providers);
assert.deepEqual(ci.policies, base.policies);
assert.equal(ci.model, base.model);
for (const name of expected) {
assert.equal(ci.agents[name].model, base.agents[name].model, name);
const readOnly = [...BASE, ...base.agents[name].permissions];
for (const toolId of BUILTIN_TOOL_IDS) assert.equal(toolEnabled(toolId, readOnly), ["read", "glob", "grep"].includes(toolId), `${name} ${toolId}`);
assert.equal(externalDirectoryProblem(readOnly), "", name);
const full = [...BASE, ...ci.agents[name].permissions];
assert.deepEqual(ciFullProblems(full), [], name);
// opencode 2.2.0 ends the capped turn on an assistant message, a prefill the model rejects, so no agent sets a step cap.
for (const config of [base, ci]) for (const key of ["steps", "maxSteps"]) assert.equal(config.agents[name][key], undefined, `${name} ${key}`);
}
});
// Evaluated as opencode 2.2.0 does: the last rule whose action and resource both match decides.
function shellDecision(rules, command) {
return [...BASE, ...rules].findLast(r => wildcardMatch("shell", r.action) && wildcardMatch(command, r.resource))?.effect;
}
test("the ci-full shell runs the read-only allowlist and nothing that builds, installs or reaches the network", () => {
const ci = JSON.parse(stripJsonc(readFileSync(join(REPO_ROOT, OPENCODE_CONFIGS["ci-full"].file), "utf-8")));
for (const [name, agentConfig] of Object.entries(ci.agents)) {
for (const command of ["git diff", "git diff origin/main -- src", "git log -5 --oneline", "git show HEAD:src/a.rs", "git blame -L 1,5 a.rs", "rg -n foo src", "cat a.rs", "ls", "ls -la web", "head -50 a.rs", "sed -n 1,40p a.rs", "wc -l a.rs",
"grep -rn foo src", "tail -n 20 a.rs", "find src -name '*.rs'", "find . -type f -newer a.rs", "find src -name '*-executor*'", "find . -name ok-delete.rs",
"git ls-files src", "git status", "git status --short", "git rev-parse HEAD", "git merge-base HEAD origin/main", "git cat-file -p HEAD:a.rs", "git cat-file -t HEAD", "git cat-file -s HEAD",
"git --no-pager diff --stat", "git --no-pager log -3", "git --no-pager show HEAD", "rg -n foo src/env_util.rs"]) {
assert.equal(shellDecision(agentConfig.permissions, command), "allow", `${name}: ${command}`);
}
for (const command of ["cargo build", "cargo test -p config", "npm ci", "npx vitest", "curl https://example.com", "git push", "git fetch origin", "git difftool", "sed -i s/a/b/ a.rs", "make", "python3 x.py", "bash -c ls",
"find . -exec rm {} ;", "find -execdir sh -c x ;", "find . -name x -ok rm {} ;", "find . -delete", "find . -fprint out.txt", "find . -fls out.txt",
"tail -f a.log", "tail -n 5 -F a.log", "tail --follow=name a.log", "git grep foo", "git grep -nOsh foo", "git grep --open-files=sh foo", "git --no-pager grep foo",
"git --no-pager push", "git --no-pager -c core.pager=sh log", "git cat-file --batch", "git cat-file blob HEAD",
"cat /proc/self/environ", "head -c 100 /proc/1/environ", "tail /dev/stdin", "grep -a KEY /proc/self/environ", "rg KEY /proc", "sed -n p /proc/self/environ",
"cat ../../proc/self/environ", "cat src/environ", "rg --file=/proc/self/environ x", "wc -c /p*/self/env*", "cat /etc/passwd", "cat a.rs > b.rs", "rg foo src>>out.txt", "cat a.rs 2>/dev/null"]) {
assert.equal(shellDecision(agentConfig.permissions, command), "deny", `${name}: ${command}`);
}
}
});
test("a trailing ' *' also matches the bare command, as in opencode 2.2.0", () => {
assert.equal(wildcardMatch("git diff", "git diff *"), true);
assert.equal(wildcardMatch("git diff --stat", "git diff *"), true);
assert.equal(wildcardMatch("git difftool", "git diff *"), false);
assert.equal(wildcardMatch("read", "*"), true);
});
test("the review runs on the anthropic-review provider, the only one the policies allow, with the workflow's variant", () => {
const base = JSON.parse(stripJsonc(readFileSync(join(REPO_ROOT, OPENCODE_CONFIGS.default.file), "utf-8")));
const allowed = base.policies.filter(p => p.action === "provider.use" && p.effect === "allow").map(p => p.resource);
assert.deepEqual(allowed, ["anthropic-review"]);
assert.equal(base.model, "anthropic-review/claude-sonnet-5-5");
for (const agent of Object.values(base.agents)) assert.equal(agent.model, base.model);
const provider = base.providers["anthropic-review"];
assert.equal(provider.package, "@opencode/ai/providers/anthropic");
assert.deepEqual(provider.env, ["ANTHROPIC_REVIEW_API_KEY"]);
const model = provider.models["claude-sonnet-5-5"];
assert.deepEqual(model.variants, ["low", "medium", "high", "xhigh", "max"].map(id => ({ id, settings: { thinking: { type: "adaptive" }, effort: id } })));
assert.deepEqual(modelVariantIds(base, "anthropic-review", "claude-sonnet-5-5"), ["low", "medium", "high", "xhigh", "max"]);
assert.deepEqual(modelVariantIds(base, "anthropic-review", "no-such-model"), []);
const workflow = readFileSync(join(REPO_ROOT, ".github/workflows/ai-code-review.yml"), "utf-8");
assert.match(workflow, /provider_id: anthropic-review\n\s+model_id: claude-sonnet-5-5\n\s+api_key_env: ANTHROPIC_REVIEW_API_KEY\n/);
assert.doesNotMatch(workflow, /model_variant/, "the effort variable is the only source of the variant");
assert.match(workflow, /REVIEW_MODEL_VARIANT: \$\{\{ vars\.AI_REVIEW_EFFORT \|\| 'high' \}\}/);
assert.match(workflow, /REVIEW_COORDINATOR_VARIANT: \$\{\{ vars\.AI_REVIEW_COORDINATOR_EFFORT \|\| vars\.AI_REVIEW_EFFORT \|\| 'high' \}\}/);
assert.match(workflow, /ANTHROPIC_REVIEW_API_KEY: \$\{\{ secrets\.USAGE_CREDIT_API_KEY \}\}/);
});
test("SSE parser handles split frames, CRLF and multi-line data", () => {
const events = [];
const parse = createSseParser(e => events.push(e));
parse('data: {"type":"a"');
parse('}\n\nda');
parse('ta: {"type":"b"}\r\n\r\n');
parse("data: not json\n\n");
parse('data: {"type":\ndata: "c"}\n\n');
assert.deepEqual(events.map(e => e.type), ["a", "b", "c"]);
});