🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

opencode-plugin-zooplankton

Package Overview
Dependencies
Maintainers
1
Versions
22
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

opencode-plugin-zooplankton - npm Package Compare versions

Comparing version
0.6.2
to
0.6.3
+21
tests/helpers/fixtures.js
/**
* Shared test fixtures and helpers used across per-module test files.
*
* Zero new dependencies — uses Node.js built-ins only.
*/
import fs from "fs";
import os from "os";
import path from "path";
/** Create a unique temporary directory and return its path. */
export const makeTmpDir = () =>
fs.mkdtempSync(path.join(os.tmpdir(), "plugin-test-"));
/** Write a file inside a temp dir, creating intermediate directories as needed. */
export const writeFixture = (dir, relativePath, content) => {
const full = path.join(dir, relativePath);
fs.mkdirSync(path.dirname(full), { recursive: true });
fs.writeFileSync(full, content, "utf8");
return full;
};
/**
* Unit tests for .opencode/plugins/lib/agent-scaffolding.js (getAgentPermissions, generateAgentMd).
*
* Uses Node.js built-in test runner (node:test + node:assert). Zero new dependencies.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
getAgentPermissions,
generateAgentMd,
getGuidePath,
} from "../../.opencode/plugins/lib.js";
describe("getAgentPermissions", () => {
it("returns coder permissions for coreCoder", () => {
const perms = getAgentPermissions("coreCoder");
assert.equal(perms.edit, "allow");
assert.equal(perms.webfetch, "allow");
assert.equal(perms.bash["*"], "allow");
});
it("returns coder permissions for juniorCoder", () => {
const perms = getAgentPermissions("juniorCoder");
assert.equal(perms.edit, "allow");
assert.equal(perms.webfetch, "allow");
assert.equal(perms.bash["*"], "allow");
});
it("returns coder permissions for frontendCoder", () => {
const perms = getAgentPermissions("frontendCoder");
assert.equal(perms.edit, "allow");
assert.equal(perms.webfetch, "allow");
});
it("returns core reviewer permissions for coreReviewer", () => {
const perms = getAgentPermissions("coreReviewer");
assert.equal(perms.edit, "deny");
assert.equal(perms.webfetch, "deny");
assert.equal(perms.bash["*"], "allow");
});
it("returns core reviewer permissions for frontendReviewer", () => {
const perms = getAgentPermissions("frontendReviewer");
assert.equal(perms.edit, "deny");
assert.equal(perms.bash["*"], "allow");
});
it("returns restricted reviewer permissions for reviewer", () => {
const perms = getAgentPermissions("reviewer");
assert.equal(perms.edit, "deny");
assert.equal(perms.webfetch, "deny");
assert.equal(perms.bash["*"], "deny");
assert.ok(!("gh api *" in perms.bash));
assert.equal(perms.bash["gh pr diff *"], "allow");
assert.equal(perms.bash["gh pr view *"], "allow");
assert.equal(perms.bash["git diff *"], "allow");
assert.equal(perms.bash["git log *"], "allow");
assert.equal(perms.bash["git fetch *"], "allow");
});
it("returns restricted reviewer permissions for securityReviewer", () => {
const perms = getAgentPermissions("securityReviewer");
assert.equal(perms.edit, "deny");
assert.equal(perms.read, "allow");
assert.equal(perms.webfetch, "allow");
assert.equal(perms.bash["*"], "deny");
assert.ok(!("gh api *" in perms.bash));
assert.equal(perms.bash["gh pr diff *"], "allow");
assert.equal(perms.bash["gh pr view *"], "allow");
assert.equal(perms.bash["git diff *"], "allow");
assert.equal(perms.bash["git log *"], "allow");
assert.equal(perms.bash["git fetch *"], "allow");
});
it("returns explorer permissions for explorer", () => {
const perms = getAgentPermissions("explorer");
assert.equal(perms.edit, "deny");
assert.equal(perms.read, "allow");
assert.equal(perms.webfetch, "allow");
assert.equal(perms.bash["*"], "deny");
// Explorer must NOT have any bash allowlist entries
assert.ok(!("gh api *" in perms.bash));
assert.ok(!("git diff *" in perms.bash));
});
it("returns null for unknown role", () => {
const perms = getAgentPermissions("unknownRole");
assert.equal(perms, null);
});
it("reviewer permissions do not include gh pr checks *", () => {
// v2 removed gh pr checks * from reviewer permissions
const perms = getAgentPermissions("reviewer");
assert.ok(!("gh pr checks *" in perms.bash));
});
it("all roles return bash permissions as an object", () => {
for (const role of ["coreCoder", "juniorCoder", "coreReviewer", "reviewer", "securityReviewer", "frontendCoder", "frontendReviewer", "explorer"]) {
const perms = getAgentPermissions(role);
assert.equal(typeof perms.bash, "object", `${role} should have bash as an object`);
assert.ok(perms.bash !== null, `${role} bash should not be null`);
}
});
it("all roles include read: allow", () => {
for (const role of ["coreCoder", "juniorCoder", "coreReviewer", "reviewer", "securityReviewer", "frontendCoder", "frontendReviewer", "explorer"]) {
const perms = getAgentPermissions(role);
assert.equal(perms.read, "allow", `${role} should have read: allow`);
}
});
// reviewer and securityReviewer bash allowlists contain only read-only inspection
// commands. gh api * was removed in PR-5b — the orchestrator owns all posting.
// Agents choose which subset to use based on project.platform.
it("reviewer and securityReviewer bash allowlist is the read-only subset (no gh api posting)", () => {
for (const role of ["reviewer", "securityReviewer"]) {
const perms = getAgentPermissions(role);
assert.equal(perms.bash["*"], "deny", `${role}: default must deny`);
assert.equal(perms.bash["gh pr diff *"], "allow", `${role}: gh pr diff * must be allow`);
assert.equal(perms.bash["gh pr view *"], "allow", `${role}: gh pr view * must be allow`);
assert.equal(perms.bash["git diff *"], "allow", `${role}: git diff * must be allow`);
assert.equal(perms.bash["git log *"], "allow", `${role}: git log * must be allow`);
assert.equal(perms.bash["git fetch *"], "allow", `${role}: git fetch * must be allow`);
assert.ok(!("gh api *" in perms.bash), `${role}: gh api * key must be absent`);
}
});
});
// SUITE: getGuidePath
describe("generateAgentMd", () => {
it("generates frontmatter with description", () => {
const md = generateAgentMd({ role: "coreCoder", description: "Test agent" });
assert.ok(md.includes('description: "Test agent"'));
});
it("includes model when provided", () => {
const md = generateAgentMd({ role: "coreCoder", model: "test-model", description: "Test" });
assert.ok(md.includes('model: "test-model"'));
});
it("omits model when not provided", () => {
const md = generateAgentMd({ role: "coreCoder", description: "Test" });
assert.ok(!md.includes("model:"));
});
it("includes steps when provided", () => {
const md = generateAgentMd({ role: "reviewer", steps: 80, description: "Test" });
assert.ok(md.includes("steps: 80"));
});
it("omits steps when not provided", () => {
const md = generateAgentMd({ role: "reviewer", description: "Test" });
assert.ok(!md.includes("steps:"));
});
it("includes correct permissions for the role", () => {
const md = generateAgentMd({ role: "reviewer", description: "Test" });
assert.ok(md.includes("edit: deny"));
assert.ok(md.includes("read: allow"));
assert.ok(md.includes('"*": deny'));
assert.ok(!md.includes('"gh api *"'), 'reviewer agent MD must not contain gh api * allow');
});
it("emits a guide reference line with an absolute path to the platform-agnostic stub", () => {
const md = generateAgentMd({ role: "coreCoder", description: "Test" });
const expectedPath = getGuidePath("coreCoder");
assert.ok(
md.includes(`Read your full operating instructions from: ${expectedPath}`),
"body must contain the guide reference line with absolute path",
);
assert.ok(!md.includes("-guide-github.md"), "must not reference a github-baked guide");
assert.ok(!md.includes("-guide-local.md"), "must not reference a local-baked guide");
});
it("emits reference line for every role including juniorCoder", () => {
const roles = [
"coreCoder",
"juniorCoder",
"coreReviewer",
"reviewer",
"securityReviewer",
"frontendCoder",
"frontendReviewer",
"explorer",
];
for (const role of roles) {
const md = generateAgentMd({ role, description: "T" });
const expectedPath = getGuidePath(role);
assert.ok(
md.includes(`Read your full operating instructions from: ${expectedPath}`),
`${role}: body must reference the platform-agnostic guide stub`,
);
}
});
it("back-compat: non-empty guideContent overrides the default reference line", () => {
const md = generateAgentMd({ role: "coreCoder", description: "Test", guideContent: "# My Guide\nContent here" });
assert.ok(md.includes("---\n\n# My Guide\nContent here"));
assert.ok(!md.includes("Read your full operating instructions from:"));
});
it("empty guideContent falls through to the default reference line", () => {
const md = generateAgentMd({ role: "coreCoder", description: "Test", guideContent: "" });
assert.ok(md.includes("Read your full operating instructions from:"));
});
it("generates valid frontmatter delimiters", () => {
const md = generateAgentMd({ role: "coreCoder", description: "Test" });
assert.ok(md.startsWith("---\n"));
assert.ok(md.includes("\n---\n"));
});
it("throws for unknown role", () => {
assert.throws(
() => generateAgentMd({ role: "unknownRole", description: "Test" }),
/Unknown agent role: unknownRole/
);
});
it("accepts valid numeric steps and writes integer to frontmatter", () => {
const md = generateAgentMd({ role: "coreCoder", description: "T", steps: 10 });
assert.ok(md.includes("\nsteps: 10\n"), "steps should appear as integer");
});
it("accepts string-numeric steps and coerces to integer", () => {
const md = generateAgentMd({ role: "coreCoder", description: "T", steps: "5" });
assert.ok(md.includes("\nsteps: 5\n"), "string steps should be coerced to integer");
});
it("throws for non-numeric steps (YAML injection guard)", () => {
assert.throws(
() => generateAgentMd({ role: "coreCoder", description: "T", steps: "10\npermission: allow" }),
/Invalid steps value/
);
});
it("throws for zero or negative steps", () => {
assert.throws(
() => generateAgentMd({ role: "coreCoder", description: "T", steps: 0 }),
/Invalid steps value/
);
});
it("throws for boolean steps (type guard)", () => {
assert.throws(
() => generateAgentMd({ role: "coreCoder", description: "T", steps: true }),
/Invalid steps value/
);
});
it("omits steps from frontmatter when undefined", () => {
const md = generateAgentMd({ role: "coreCoder", description: "T" });
assert.ok(!md.includes("steps:"), "steps should be absent when undefined");
});
});
/**
* Unit tests for .opencode/plugins/lib/clustering.js (CLUSTER_ID_REGEX, normalizeSummary, dedupeFindings, fuzzy-cluster pipeline, assignClusterIds, computeFingerprint).
*
* Uses Node.js built-in test runner (node:test + node:assert). Zero new dependencies.
*/
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import fs from "fs";
import path from "path";
import { makeTmpDir, writeFixture } from "../helpers/fixtures.js";
import {
CLUSTER_ID_REGEX,
normalizeSummary,
dedupeFindings,
assignClusterIds,
computeFingerprint,
} from "../../.opencode/plugins/lib.js";
describe("CLUSTER_ID_REGEX", () => {
it("accepts R<n>-C<nn> (code)", () => {
assert.ok(CLUSTER_ID_REGEX.test("R1-C01"));
assert.ok(CLUSTER_ID_REGEX.test("R2-C15"));
assert.ok(CLUSTER_ID_REGEX.test("R9-C99"));
assert.ok(CLUSTER_ID_REGEX.test("R10-C123")); // 3-digit nn supported
});
it("accepts S<n>-C<nn> (security)", () => {
assert.ok(CLUSTER_ID_REGEX.test("S1-C01"));
assert.ok(CLUSTER_ID_REGEX.test("S1-C42"));
});
it("accepts P<n>-C<nn> (plan)", () => {
assert.ok(CLUSTER_ID_REGEX.test("P1-C01"));
assert.ok(CLUSTER_ID_REGEX.test("P3-C07"));
});
it("rejects the legacy SR-C<nn> format (B3)", () => {
assert.ok(!CLUSTER_ID_REGEX.test("SR-C01"));
assert.ok(!CLUSTER_ID_REGEX.test("SR-C99"));
});
it("rejects other malformed IDs", () => {
assert.ok(!CLUSTER_ID_REGEX.test("X1-C01")); // bad prefix
assert.ok(!CLUSTER_ID_REGEX.test("R0-C01")); // round starts at 1
assert.ok(!CLUSTER_ID_REGEX.test("R1-C1")); // nn must be >=2 digits
assert.ok(!CLUSTER_ID_REGEX.test("R1C01")); // missing hyphen
assert.ok(!CLUSTER_ID_REGEX.test("R1-C0a")); // non-digit
assert.ok(!CLUSTER_ID_REGEX.test(""));
});
});
describe("normalizeSummary", () => {
it("lowercases, collapses whitespace, strips trailing period", () => {
assert.equal(normalizeSummary(" Hello World. "), "hello world");
assert.equal(normalizeSummary("Missing\tnull\ncheck."), "missing null check");
});
it("handles non-string input gracefully", () => {
assert.equal(normalizeSummary(null), "");
assert.equal(normalizeSummary(undefined), "");
assert.equal(normalizeSummary(42), "");
});
});
describe("dedupeFindings", () => {
const mk = (overrides) => ({
path: "src/a.js",
line: 10,
summary: "Null check missing",
severity: "blocking",
anchor_scope: "line",
area: "error handling",
reviewer_name: "reviewer-glm",
reviewer_model: "glm-4",
reviewer_role: "reviewer",
...overrides,
});
it("merges findings sharing (path, line, normalized summary, severity)", () => {
const findings = [
mk({ reviewer_name: "reviewer-glm" }),
mk({ reviewer_name: "reviewer-deepseek", summary: " NULL CHECK missing." }), // case + whitespace + period differ
];
const result = dedupeFindings(findings);
assert.equal(result.length, 1);
assert.equal(result[0].sources.length, 2);
assert.equal(result[0].sources[0].reviewer_name, "reviewer-glm");
assert.equal(result[0].sources[1].reviewer_name, "reviewer-deepseek");
});
it("keeps the first occurrence's summary verbatim", () => {
const findings = [
mk({ summary: "Null check missing" }),
mk({ summary: "null check MISSING." }),
];
const result = dedupeFindings(findings);
assert.equal(result[0].summary, "Null check missing");
});
it("does NOT merge when severity differs", () => {
const findings = [
mk({ severity: "blocking" }),
mk({ severity: "advisory" }),
];
assert.equal(dedupeFindings(findings).length, 2);
});
it("does NOT merge when path or line differs", () => {
const findings = [
mk({ path: "src/a.js", line: 10 }),
mk({ path: "src/a.js", line: 11 }),
mk({ path: "src/b.js", line: 10 }),
];
assert.equal(dedupeFindings(findings).length, 3);
});
it("strips reviewer_name/model/role from merged body; keeps them in sources[]", () => {
const result = dedupeFindings([mk()]);
assert.ok(!("reviewer_name" in result[0]));
assert.ok(!("reviewer_model" in result[0]));
assert.ok(!("reviewer_role" in result[0]));
assert.equal(result[0].sources[0].reviewer_name, "reviewer-glm");
});
it("returns [] for non-array input", () => {
assert.deepEqual(dedupeFindings(null), []);
assert.deepEqual(dedupeFindings(undefined), []);
assert.deepEqual(dedupeFindings("nope"), []);
});
// B2 (round 2): null-line findings about the same file but different `area`
// must NOT merge. Previously the dedup key was (path, line, summary, severity)
// which over-merged file-scope findings whose only distinguishing field is
// `area`.
it("B2: two null-line findings on same file with different areas → two clusters", () => {
const findings = [
mk({
line: null,
anchor_scope: "file",
area: "logic",
summary: "Module has unclear responsibility",
reviewer_name: "reviewer-glm",
}),
mk({
line: null,
anchor_scope: "file",
area: "tests",
summary: "Module has unclear responsibility",
reviewer_name: "reviewer-deepseek",
}),
];
const result = dedupeFindings(findings);
assert.equal(result.length, 2);
assert.equal(result[0].area, "logic");
assert.equal(result[1].area, "tests");
});
it("B2: two null-line findings on same file with SAME area → still merge", () => {
const findings = [
mk({ line: null, anchor_scope: "file", area: "logic", reviewer_name: "r1" }),
mk({ line: null, anchor_scope: "file", area: "logic", reviewer_name: "r2" }),
];
const result = dedupeFindings(findings);
assert.equal(result.length, 1);
assert.equal(result[0].sources.length, 2);
});
it("B2: two branch-scope findings with different areas → two clusters", () => {
const findings = [
mk({ path: null, line: null, anchor_scope: "branch", area: "docs", summary: "Readme stale" }),
mk({ path: null, line: null, anchor_scope: "branch", area: "architecture", summary: "Readme stale" }),
];
assert.equal(dedupeFindings(findings).length, 2);
});
it("B2: line-specific findings still ignore `area` for dedup (line is precise enough)", () => {
// Regression guard: line-anchored findings at (path, line) with different
// `area` but same (summary, severity) SHOULD still merge, because a single
// line in a single file is specific enough that two reviewers flagging it
// with different area labels are reporting the same underlying issue.
const findings = [
mk({ path: "src/a.js", line: 10, area: "logic", reviewer_name: "r1" }),
mk({ path: "src/a.js", line: 10, area: "types", reviewer_name: "r2" }),
];
const result = dedupeFindings(findings);
assert.equal(result.length, 1);
assert.equal(result[0].sources.length, 2);
});
});
// SUITE: fuzzy-cluster pipeline
//
// PR-5c adds a fuzzy pre-merge step (see review-protocol.md → Finding Consolidation) where the
// orchestrator normalizes near-duplicate summaries to a canonical form before
// the exact-string dedup step runs. The "paraphrase pair" tests below
// simulate that normalization step by applying `normalizeSummary` to each
// finding before `dedupeFindings`, exercising the end-to-end pipeline that
// the orchestrator is expected to perform: trivially-different summaries
// (case, whitespace, trailing period) should collapse into one cluster after
// normalization. The "distinct issue" tests then verify that genuinely
// different findings (different paths, areas, or severity) stay as separate
// clusters.
//
// Note: the SKILL.md draft of step 2 originally allowed a ±3-line tolerance
// on `line` for off-by-one diff context drift, but that clause was dropped
// in PR-5c review (R1-C03) because the downstream exact-dedup key requires
// an exact `(path, line)` match — a ±3 fuzz at the orchestrator level cannot
// merge the two findings without also rewriting `line`, which the
// fuzzy step is explicitly forbidden from doing. Same-`line` (or both
// `null`) is the only semantically-correct rule, so no ±3 test is needed.
describe("fuzzy-cluster pipeline", () => {
// Helper: simulate orchestrator step 2 (normalize summaries to canonical
// form) followed by step 3 (exact-dedup) and step 5 (cluster ID assign).
const runPipeline = (findings) => {
const normalized = findings.map((f) => ({ ...f, summary: normalizeSummary(f.summary) }));
const deduped = dedupeFindings(normalized);
return assignClusterIds(deduped, { round: 1 });
};
it("paraphrase pair 1: case + whitespace + trailing-period variants collapse after normalization", () => {
const base = { anchor_scope: "line", path: "src/foo.js", line: 10, severity: "blocking", area: "logic" };
const findings = [
{ ...base, summary: "null dereference in bar()", reviewer_name: "r1", reviewer_model: "m1", reviewer_role: "reviewer" },
{ ...base, summary: " Null Dereference In Bar(). ", reviewer_name: "r2", reviewer_model: "m2", reviewer_role: "reviewer" },
];
const result = runPipeline(findings);
assert.equal(result.length, 1, "trivially-different summaries must collapse to one cluster after normalization");
assert.equal(result[0].sources.length, 2, "merged cluster must have two sources");
assert.ok(CLUSTER_ID_REGEX.test(result[0].cluster_id), "cluster ID must match pattern");
});
it("paraphrase pair 2: branch-scope findings with capitalization + trailing-period diff collapse", () => {
const base = { anchor_scope: "branch", path: null, line: null, severity: "blocking", area: "security" };
const findings = [
{ ...base, summary: "Missing input validation on user-controlled data", reviewer_name: "r1", reviewer_model: "m1", reviewer_role: "security" },
{ ...base, summary: "missing input validation on user-controlled data.", reviewer_name: "r2", reviewer_model: "m2", reviewer_role: "security" },
];
const result = runPipeline(findings);
assert.equal(result.length, 1, "branch-scope paraphrase pair must collapse");
assert.equal(result[0].sources.length, 2);
});
it("paraphrase pair 3: line-anchored findings differing only in casing collapse", () => {
const base = { anchor_scope: "line", path: "src/loop.js", line: 42, severity: "advisory", area: "logic" };
const findings = [
{ ...base, summary: "Off-by-one error in loop", reviewer_name: "r1", reviewer_model: "m1", reviewer_role: "reviewer" },
{ ...base, summary: "off-by-one error in loop", reviewer_name: "r2", reviewer_model: "m2", reviewer_role: "reviewer" },
];
const result = runPipeline(findings);
assert.equal(result.length, 1);
assert.equal(result[0].sources.length, 2);
});
it("distinct issue 1: different paths stay separate even with identical summary", () => {
const base = { anchor_scope: "line", line: 5, severity: "blocking", area: "logic", summary: "off-by-one error" };
const findings = [
{ ...base, path: "src/a.js", reviewer_name: "r1", reviewer_model: "m1", reviewer_role: "reviewer" },
{ ...base, path: "src/b.js", reviewer_name: "r2", reviewer_model: "m2", reviewer_role: "reviewer" },
];
const result = runPipeline(findings);
assert.equal(result.length, 2, "different paths must NOT collapse");
});
it("distinct issue 2: different areas on branch-scope finding stay separate", () => {
const base = { anchor_scope: "branch", path: null, line: null, severity: "advisory", summary: "missing test coverage" };
const findings = [
{ ...base, area: "tests", reviewer_name: "r1", reviewer_model: "m1", reviewer_role: "reviewer" },
{ ...base, area: "logic", reviewer_name: "r2", reviewer_model: "m2", reviewer_role: "reviewer" },
];
const result = runPipeline(findings);
assert.equal(result.length, 2, "different areas on branch-scope findings must NOT collapse");
});
it("distinct issue 3: different severity stays separate", () => {
const base = { anchor_scope: "line", path: "src/foo.js", line: 20, area: "tests", summary: "missing null check" };
const findings = [
{ ...base, severity: "blocking", reviewer_name: "r1", reviewer_model: "m1", reviewer_role: "reviewer" },
{ ...base, severity: "advisory", reviewer_name: "r2", reviewer_model: "m2", reviewer_role: "reviewer" },
];
const result = runPipeline(findings);
assert.equal(result.length, 2, "different severity on same path/line must NOT collapse");
});
it("fast-path: single finding passes through unchanged", () => {
const f = { anchor_scope: "line", path: "src/x.js", line: 1, severity: "blocking", area: "logic", summary: "only finding" };
const result = runPipeline([f]);
assert.equal(result.length, 1);
assert.ok(CLUSTER_ID_REGEX.test(result[0].cluster_id));
});
it("cluster IDs are stable — same input produces same IDs on second run", () => {
const base = { anchor_scope: "line", path: "src/foo.js", line: 10, severity: "blocking", area: "logic" };
const findings = [
{ ...base, summary: "Null dereference in bar()", reviewer_name: "r1", reviewer_model: "m1", reviewer_role: "reviewer" },
{ ...base, path: "src/bar.js", summary: "unrelated issue", reviewer_name: "r1", reviewer_model: "m1", reviewer_role: "reviewer" },
];
const run1 = runPipeline([...findings]);
const run2 = runPipeline([...findings]);
assert.deepEqual(
run1.map((f) => f.cluster_id),
run2.map((f) => f.cluster_id),
"cluster IDs must be stable across runs",
);
});
});
describe("assignClusterIds", () => {
const mk = (overrides) => ({
path: "src/a.js",
line: 10,
summary: "x",
severity: "blocking",
anchor_scope: "line",
...overrides,
});
it("assigns R<round>-C<nn> for code phase", () => {
const out = assignClusterIds([mk(), mk({ line: 20 })], { round: 1, phase: "code" });
assert.ok(/^R1-C01$/.test(out[0].cluster_id));
assert.ok(/^R1-C02$/.test(out[1].cluster_id));
});
it("assigns S<round>-C<nn> for security phase (B3)", () => {
const out = assignClusterIds([mk(), mk({ line: 20 })], { round: 1, phase: "security" });
assert.equal(out[0].cluster_id, "S1-C01");
assert.equal(out[1].cluster_id, "S1-C02");
});
it("assigns P<round>-C<nn> for plan phase", () => {
const out = assignClusterIds([mk()], { round: 2, phase: "plan" });
assert.equal(out[0].cluster_id, "P2-C01");
});
it("sorts deterministically by (path, line, severity, normalized summary)", () => {
const findings = [
mk({ path: "src/b.js", line: 5, summary: "Z" }),
mk({ path: "src/a.js", line: 20, summary: "A" }),
mk({ path: "src/a.js", line: 10, summary: "B" }),
];
const out = assignClusterIds(findings, { round: 1 });
assert.equal(out[0].path, "src/a.js"); assert.equal(out[0].line, 10);
assert.equal(out[1].path, "src/a.js"); assert.equal(out[1].line, 20);
assert.equal(out[2].path, "src/b.js"); assert.equal(out[2].line, 5);
});
it("produces identical IDs regardless of input order (determinism)", () => {
const a = [mk({ path: "x", line: 1 }), mk({ path: "y", line: 2 })];
const b = [mk({ path: "y", line: 2 }), mk({ path: "x", line: 1 })];
const outA = assignClusterIds(a, { round: 1 });
const outB = assignClusterIds(b, { round: 1 });
assert.deepEqual(outA.map((f) => [f.path, f.cluster_id]), outB.map((f) => [f.path, f.cluster_id]));
});
it("zero-pads to 2 digits minimum", () => {
const findings = Array.from({ length: 9 }, (_, i) => mk({ line: i + 1 }));
const out = assignClusterIds(findings, { round: 1 });
assert.equal(out[0].cluster_id, "R1-C01");
assert.equal(out[8].cluster_id, "R1-C09");
});
it("generated IDs all match CLUSTER_ID_REGEX", () => {
const out = assignClusterIds([mk(), mk({ line: 20 })], { round: 3, phase: "security" });
for (const f of out) {
assert.ok(CLUSTER_ID_REGEX.test(f.cluster_id), `bad id: ${f.cluster_id}`);
}
});
it("throws on invalid round or phase", () => {
assert.throws(() => assignClusterIds([], { round: 0 }), /positive integer/);
assert.throws(() => assignClusterIds([], { round: 1, phase: "nope" }), /code\|security\|plan/);
});
it("returns [] for non-array input", () => {
assert.deepEqual(assignClusterIds(null, { round: 1 }), []);
});
});
describe("computeFingerprint", () => {
const mk = (overrides) => ({
path: "src/a.js",
line: 10,
summary: "Null check missing",
severity: "blocking",
anchor_scope: "line",
area: "error handling",
...overrides,
});
it("is deterministic for identical inputs", () => {
const fp1 = computeFingerprint(1, 1, [mk()]);
const fp2 = computeFingerprint(1, 1, [mk()]);
assert.equal(fp1, fp2);
});
it("is input-order-independent (stable after reordering)", () => {
const fpA = computeFingerprint(1, 1, [mk({ line: 10 }), mk({ line: 20 })]);
const fpB = computeFingerprint(1, 1, [mk({ line: 20 }), mk({ line: 10 })]);
assert.equal(fpA, fpB);
});
it("ignores cluster_id, ephemeral_id, sources, detail (retries produce same fp)", () => {
const base = mk();
const enriched = {
...base,
cluster_id: "R1-C01",
ephemeral_id: "e-1",
sources: [{ reviewer_name: "x" }],
detail: "long explanation...",
};
assert.equal(computeFingerprint(1, 1, [base]), computeFingerprint(1, 1, [enriched]));
});
it("normalizes summary (case, whitespace, trailing period)", () => {
const a = mk({ summary: "Null check missing" });
const b = mk({ summary: " null\tcheck MISSING. " });
assert.equal(computeFingerprint(1, 1, [a]), computeFingerprint(1, 1, [b]));
});
it("changes when findings change", () => {
const fp1 = computeFingerprint(1, 1, [mk()]);
const fp2 = computeFingerprint(1, 1, [mk({ summary: "Different issue" })]);
assert.notEqual(fp1, fp2);
});
it("changes when pr or round changes", () => {
const fp1 = computeFingerprint(1, 1, [mk()]);
const fp2 = computeFingerprint(2, 1, [mk()]);
const fp3 = computeFingerprint(1, 2, [mk()]);
assert.notEqual(fp1, fp2);
assert.notEqual(fp1, fp3);
});
it("returns a 64-char hex sha256 digest", () => {
const fp = computeFingerprint(1, 1, [mk()]);
assert.ok(/^[0-9a-f]{64}$/.test(fp));
});
it("handles empty findings list", () => {
const fp = computeFingerprint(1, 1, []);
assert.ok(/^[0-9a-f]{64}$/.test(fp));
});
it("throws on invalid pr or round", () => {
assert.throws(() => computeFingerprint(0, 1, []), /pr must be a positive integer/);
assert.throws(() => computeFingerprint(1, 0, []), /round must be a positive integer/);
});
});
/**
* Unit tests for .opencode/plugins/lib/file-readers.js (loadCommands, readGuide).
*
* Uses Node.js built-in test runner (node:test + node:assert). Zero new dependencies.
*/
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import fs from "fs";
import path from "path";
import { makeTmpDir, writeFixture } from "../helpers/fixtures.js";
import {
loadCommands,
readGuide,
extractFrontmatter,
} from "../../.opencode/plugins/lib.js";
describe("readGuide", () => {
it("returns empty string for missing guide file", () => {
assert.equal(readGuide("nonexistent-guide.md"), "");
});
it("returns trimmed content for existing guide files", () => {
const result = readGuide("core-coder-guide.md");
assert.ok(typeof result === "string");
assert.ok(result.length > 0);
assert.equal(result, result.trim());
});
});
// SUITE: detectPlatform
describe("loadCommands", () => {
it("returns an object with command names as keys", () => {
const commands = loadCommands();
assert.ok(typeof commands === "object" && commands !== null);
const keys = Object.keys(commands);
assert.ok(keys.includes("zooplankton-init"));
assert.ok(keys.includes("zooplankton-update"));
});
it("each command has description and template fields", () => {
const commands = loadCommands();
for (const [name, cmd] of Object.entries(commands)) {
assert.ok(typeof cmd.description === "string", `${name} missing description`);
assert.ok(typeof cmd.template === "string", `${name} missing template`);
assert.ok(cmd.template.length > 0, `${name} has empty template`);
}
});
it("commands without agent/model frontmatter omit those fields", () => {
const commands = loadCommands();
for (const [name, cmd] of Object.entries(commands)) {
assert.ok(!("agent" in cmd), `${name} should not have agent`);
assert.ok(!("model" in cmd), `${name} should not have model`);
}
});
it("uses filename-without-extension as key", () => {
const commands = loadCommands();
for (const key of Object.keys(commands)) {
assert.ok(!key.endsWith(".md"), `key ${key} should not have .md suffix`);
}
});
it("extractFrontmatter correctly parses agent/model fields for command loading", () => {
const content =
'---\ndescription: Test command\nagent: "my-agent"\nmodel: "my-model"\n---\nTemplate body';
const { meta, body } = extractFrontmatter(content);
assert.equal(meta.description, "Test command");
assert.equal(meta.agent, "my-agent");
assert.equal(meta.model, "my-model");
assert.equal(body, "Template body");
const cmd = { description: meta.description, template: body.trim() };
if (meta.agent) cmd.agent = meta.agent;
if (meta.model) cmd.model = meta.model;
assert.equal(cmd.agent, "my-agent");
assert.equal(cmd.model, "my-model");
});
});
// SUITE: readZooplanktonJson
/**
* Unit tests for .opencode/plugins/lib/frontmatter.js (extractFrontmatter, parsePlanScope).
*
* Uses Node.js built-in test runner (node:test + node:assert). Zero new dependencies.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { extractFrontmatter, parsePlanScope } from "../../.opencode/plugins/lib.js";
describe("extractFrontmatter", () => {
it("returns original content when no frontmatter present", () => {
const input = "Hello world\nNo frontmatter here.";
const { meta, body } = extractFrontmatter(input);
assert.deepStrictEqual(meta, {});
assert.equal(body, input);
});
it("parses valid frontmatter into meta keys and body", () => {
const input = "---\ntitle: My Title\nauthor: Jane\n---\nBody content here.";
const { meta, body } = extractFrontmatter(input);
assert.equal(meta.title, "My Title");
assert.equal(meta.author, "Jane");
assert.equal(body, "Body content here.");
});
it("parses frontmatter with CRLF line endings correctly", () => {
const input = "---\r\ntitle: My Title\r\nauthor: Jane\r\n---\r\nBody content here.";
const { meta, body } = extractFrontmatter(input);
assert.equal(meta.title, "My Title");
assert.equal(meta.author, "Jane");
assert.equal(body, "Body content here.");
});
it("strips surrounding quotes from values", () => {
const input = '---\nname: "quoted value"\nother: \'single quoted\'\n---\nBody';
const { meta } = extractFrontmatter(input);
assert.equal(meta.name, "quoted value");
assert.equal(meta.other, "single quoted");
});
it("splits only on first colon (colon in value)", () => {
const input = "---\ndescription: key: value pair\n---\nBody";
const { meta } = extractFrontmatter(input);
assert.equal(meta.description, "key: value pair");
});
it("returns empty body when nothing follows frontmatter", () => {
const input = "---\nkey: val\n---\n";
const { meta, body } = extractFrontmatter(input);
assert.equal(meta.key, "val");
assert.equal(body, "");
});
it("returns empty meta for empty frontmatter block", () => {
const input = "---\n\n---\nSome body";
const { meta, body } = extractFrontmatter(input);
assert.deepStrictEqual(meta, {});
assert.equal(body, "Some body");
});
});
// SUITE: parsePlanScope
//
// Validates `parsePlanScope` semantics introduced in PR-4 of the
// unified-orchestrate-flow plan: a plan file's `plan_scope:` frontmatter
// field must be one of `simple|medium|large`. The legacy `scope:` alias was
// removed in PR-5a — `scope:` is now treated as an unknown field.
describe("parsePlanScope", () => {
it("returns the value when plan_scope is a valid scope (no warning)", () => {
for (const value of ["simple", "medium", "large"]) {
const result = parsePlanScope(`---\nplan_scope: ${value}\n---\nbody`);
assert.equal(result.scope, value);
assert.ok(!("warning" in result), `valid value "${value}" should not emit warning`);
}
});
it("defaults to 'medium' (no warning) when frontmatter is absent", () => {
const result = parsePlanScope("just a plan body, no frontmatter");
assert.equal(result.scope, "medium");
assert.ok(!("warning" in result));
});
it("defaults to 'medium' with a warning when plan_scope value is invalid", () => {
const result = parsePlanScope("---\nplan_scope: huge\n---\nbody");
assert.equal(result.scope, "medium");
assert.ok(typeof result.warning === "string" && result.warning.length > 0);
assert.match(result.warning, /huge/);
assert.match(result.warning, /simple\|medium\|large/);
});
it("legacy 'scope:' field is treated as unknown — defaults to 'medium' with missing-plan_scope warning (not a deprecation warning)", () => {
const result = parsePlanScope("---\nscope: large\n---\nbody");
assert.equal(result.scope, "medium");
assert.ok(typeof result.warning === "string");
assert.match(result.warning, /missing or invalid `plan_scope:`/);
});
it("plan_scope wins when both plan_scope and legacy scope are present (no warning)", () => {
const result = parsePlanScope(
"---\nplan_scope: simple\nscope: large\n---\nbody"
);
assert.equal(result.scope, "simple");
assert.ok(!("warning" in result));
});
it("defaults to 'medium' with a warning when input is null", () => {
const result = parsePlanScope(null);
assert.equal(result.scope, "medium");
assert.ok(typeof result.warning === "string" && result.warning.length > 0);
assert.match(result.warning, /null\/undefined/);
});
it("defaults to 'medium' with a warning when input is undefined", () => {
const result = parsePlanScope(undefined);
assert.equal(result.scope, "medium");
assert.ok(typeof result.warning === "string" && result.warning.length > 0);
assert.match(result.warning, /null\/undefined/);
});
});
// SUITE: readGuide
/**
* Unit tests for .opencode/plugins/lib/github-posting.js (GitHub posting helpers).
*
* Uses Node.js built-in test runner (node:test + node:assert). Zero new dependencies.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import {
createReviewMarker,
hasReviewMarker,
makePostingStateEntry,
upsertPostingState,
decidePostingAction,
renderConsolidatedReviewBody,
buildInlineCommentAnchors,
planReviewPostRetry,
} from "../../.opencode/plugins/lib.js";
describe("GitHub posting helpers", () => {
const fpA = "a".repeat(64);
const fpB = "b".repeat(64);
const findings = [
{
cluster_id: "R1-C01",
severity: "blocking",
anchor_scope: "line",
path: "src/a.js",
line: 10,
area: "logic",
summary: "Null check missing",
detail: "Value may be null.",
reviewer_model: "model-a",
},
{
cluster_id: "R1-C02",
severity: "advisory",
anchor_scope: "branch",
path: null,
line: null,
area: "tests",
summary: "Add regression test",
detail: null,
reviewer_model: "model-b",
},
];
it("creates code and security markers", () => {
assert.equal(
createReviewMarker({ pr: 38, round: 2, phase: "code", fingerprint: fpA }),
`<!-- zooplankton-orchestrate: pr=38 round=2 fingerprint=${fpA} -->`,
);
assert.equal(
createReviewMarker({ pr: 38, phase: "security", fingerprint: fpA }),
`<!-- zooplankton-orchestrate: pr=38 phase=security fingerprint=${fpA} -->`,
);
});
it("rejects invalid review marker inputs", () => {
assert.throws(() => createReviewMarker({ pr: 0, round: 1, fingerprint: fpA }), /positive integer/);
assert.throws(() => createReviewMarker({ pr: 1, round: 1, fingerprint: "short" }), /64-character lowercase hex/);
assert.throws(() => createReviewMarker({ pr: 1, round: 1, fingerprint: "A".repeat(64) }), /64-character lowercase hex/);
assert.throws(() => createReviewMarker({ pr: 1, round: 0, fingerprint: fpA }), /positive integer/);
assert.throws(() => createReviewMarker({ pr: 1, round: 1, phase: "deploy", fingerprint: fpA }), /code\|security/);
});
it("detects markers in existing PR bodies", () => {
const marker = createReviewMarker({ pr: 1, round: 1, fingerprint: fpA });
assert.equal(hasReviewMarker([{ body: `body\n${marker}`, author: "zoe-bot" }], marker, ["zoe-bot"]), true);
assert.equal(hasReviewMarker([{ body: marker, author: { login: "zoe-bot" } }], marker, ["zoe-bot"]), true);
assert.equal(hasReviewMarker([{ body: marker, user: { login: "zoe-bot" } }], marker, ["zoe-bot"]), true);
assert.equal(hasReviewMarker([{ body: marker, author: "attacker" }], marker, ["zoe-bot"]), false);
assert.equal(hasReviewMarker([{ body: `quoted ${marker}`, author: "zoe-bot" }], marker, ["zoe-bot"]), false);
assert.equal(hasReviewMarker(["hello"], marker, ["zoe-bot"]), false);
});
it("upserts posting state by pr/round/phase", () => {
const first = makePostingStateEntry({ pr: 1, round: 1, phase: "code", fingerprint: fpA, status: "IN_PROGRESS", timestamp: "t1" });
const second = makePostingStateEntry({ pr: 1, round: 1, phase: "code", fingerprint: fpA, status: "COMPLETE", timestamp: "t2" });
const state = upsertPostingState([first], second);
assert.equal(state.length, 1);
assert.equal(state[0].status, "COMPLETE");
assert.equal(state[0].timestamp, "t2");
});
it("skips posting when exact marker already exists even without local state", () => {
const marker = createReviewMarker({ pr: 1, round: 1, fingerprint: fpA });
const decision = decidePostingAction({ state: [], existingBodies: [{ body: marker, author: "zoe-bot" }], trustedAuthors: ["zoe-bot"], pr: 1, round: 1, fingerprint: fpA });
assert.equal(decision.action, "skip");
assert.equal(decision.reason, "marker-exists");
assert.equal(decision.nextState[0].status, "COMPLETE");
});
it("posts instead of trusting markers from untrusted authors", () => {
const marker = createReviewMarker({ pr: 1, round: 1, fingerprint: fpA });
const decision = decidePostingAction({ state: [], existingBodies: [{ body: marker, author: "attacker" }], trustedAuthors: ["zoe-bot"], pr: 1, round: 1, fingerprint: fpA });
assert.equal(decision.action, "post");
assert.equal(decision.reason, "new-post");
});
it("throws when existing bodies are provided without trusted authors", () => {
const marker = createReviewMarker({ pr: 1, round: 1, fingerprint: fpA });
assert.throws(
() => decidePostingAction({ state: [], existingBodies: [{ body: marker, author: "zoe-bot" }], pr: 1, round: 1, fingerprint: fpA }),
/trustedAuthors must be a non-empty array/,
);
});
it("retries crashed post only when local IN_PROGRESS exists and PR marker is absent", () => {
const state = [makePostingStateEntry({ pr: 1, round: 1, phase: "code", fingerprint: fpA, status: "IN_PROGRESS", timestamp: "t1" })];
const decision = decidePostingAction({ state, existingBodies: [], pr: 1, round: 1, fingerprint: fpA });
assert.equal(decision.action, "post");
assert.equal(decision.reason, "retry-crashed-post");
});
it("posts with supersedes when completed fingerprint changes", () => {
const state = [makePostingStateEntry({ pr: 1, round: 1, phase: "code", fingerprint: fpA, status: "COMPLETE", timestamp: "t1" })];
const decision = decidePostingAction({ state, existingBodies: [], pr: 1, round: 1, fingerprint: fpB });
assert.equal(decision.action, "post");
assert.equal(decision.reason, "changed-fingerprint");
assert.equal(decision.supersedes, fpA);
});
it("normalizes security round values to the single security state slot", () => {
const decision = decidePostingAction({ state: [], existingBodies: [], pr: 1, round: 2, phase: "security", fingerprint: fpA });
assert.equal(decision.action, "post");
assert.equal(decision.nextState[0].round, 1);
});
it("throws when trusted authors contain no non-empty strings", () => {
const marker = createReviewMarker({ pr: 1, round: 1, fingerprint: fpA });
assert.throws(
() => decidePostingAction({ state: [], existingBodies: [{ body: marker, author: "zoe-bot" }], trustedAuthors: [""], pr: 1, round: 1, fingerprint: fpA }),
/trustedAuthors must be a non-empty array/,
);
});
const reviewers = [
{ name: "core-reviewer", model: "claude-sonnet-4.6" },
{ name: "reviewer", model: "glm-4.6" },
];
it("body starts with marker and contains total/severity counts, reviewer attribution, and pointer", () => {
const marker = createReviewMarker({ pr: 1, round: 1, fingerprint: fpA });
const body = renderConsolidatedReviewBody(findings, reviewers, { marker });
// Marker is the first line.
assert.equal(body.split("\n")[0], marker);
// Total + severity rows.
assert.match(body, /^Total findings: 2$/m);
assert.match(body, /^- blocking: 1$/m);
assert.match(body, /^- advisory: 1$/m);
// Reviewer attribution.
assert.match(body, /^Reviewers: core-reviewer \(claude-sonnet-4\.6\), reviewer \(glm-4\.6\)$/m);
// Pointer line.
assert.match(body, /See inline comments below for line-by-line details\. File-\/branch-scope findings are listed in the next section\./);
});
it("renders supersedes line and skips zero-severity rows", () => {
const marker = createReviewMarker({ pr: 1, round: 1, fingerprint: fpA });
const onlyAdvisory = [findings[1]];
const body = renderConsolidatedReviewBody(onlyAdvisory, reviewers, { marker, supersedes: fpB });
assert.match(body, /Supersedes earlier review \(fingerprint bbbbbbbb\); see comment timestamps for ordering\./);
assert.match(body, /^- advisory: 1$/m);
assert.doesNotMatch(body, /^- blocking:/m);
});
it("body excludes line-anchored prose by default but renders file/branch-scope findings", () => {
const marker = createReviewMarker({ pr: 1, round: 1, fingerprint: fpA });
const body = renderConsolidatedReviewBody(findings, reviewers, { marker });
// Line-anchored detail is NOT in the body.
assert.doesNotMatch(body, /Value may be null/);
// The line-anchored finding's *summary* + cluster ID are also absent
// from the body (they live in inline comments only).
assert.doesNotMatch(body, /R1-C01/);
// File/branch-scope section is present with the advisory finding.
assert.match(body, /### File-\/branch-scope findings/);
assert.match(body, /\[R1-C02\] \[advisory\] Add regression test — branch-scope/);
});
it("Tier 3 fallback (includeLineAnchored: true) appends line-anchored findings in full prose", () => {
const marker = createReviewMarker({ pr: 1, round: 1, fingerprint: fpA });
const body = renderConsolidatedReviewBody(findings, reviewers, { marker, includeLineAnchored: true });
assert.match(body, /### Line-anchored findings \(body-only fallback\)/);
assert.match(body, /\[R1-C01\] \[blocking\] Null check missing — src\/a\.js:10/);
// Detail prose is included for Tier 3.
assert.match(body, /Value may be null\./);
// File/branch section is also still present.
assert.match(body, /\[R1-C02\] \[advisory\] Add regression test — branch-scope/);
});
it("renders a compact body with no findings (no severity rows, no sections, no pointer)", () => {
const marker = createReviewMarker({ pr: 1, round: 1, fingerprint: fpA });
const body = renderConsolidatedReviewBody([], [], { marker });
assert.match(body, /^Total findings: 0$/m);
assert.doesNotMatch(body, /^- blocking:/m);
assert.doesNotMatch(body, /^- advisory:/m);
assert.doesNotMatch(body, /Reviewers:/);
assert.doesNotMatch(body, /### File-\/branch-scope findings/);
assert.doesNotMatch(body, /Blocking issues span/);
assert.doesNotMatch(body, /See inline comments below/);
});
it("falls back to empty arrays when reviewers is null/undefined (no Reviewers line emitted)", () => {
const marker = createReviewMarker({ pr: 1, round: 1, fingerprint: fpA });
const bodyNull = renderConsolidatedReviewBody([], null, { marker });
const bodyUndef = renderConsolidatedReviewBody([], undefined, { marker });
assert.doesNotMatch(bodyNull, /Reviewers:/);
assert.doesNotMatch(bodyUndef, /Reviewers:/);
});
it("throws TypeError when marker is missing/invalid", () => {
assert.throws(() => renderConsolidatedReviewBody([], [], {}), /marker.*required/);
assert.throws(() => renderConsolidatedReviewBody([], [], { marker: "" }), /marker.*required/);
assert.throws(() => renderConsolidatedReviewBody([], [], { marker: 123 }), /marker.*required/);
assert.throws(() => renderConsolidatedReviewBody([], []), /marker.*required/);
});
it("throws TypeError when marker is non-empty but malformed (R2-C09)", () => {
// Hand-rolled markers that do not match the createReviewMarker shape are
// rejected so idempotency (line-exact match in hasReviewMarker) cannot be
// silently broken by typos or partial markers.
const badMarkers = [
"not a marker",
"<!-- zooplankton-orchestrate: pr=1 -->", // missing round/fingerprint
"<!-- zooplankton-orchestrate: pr=1 round=1 fingerprint=short -->", // bad fingerprint
`<!-- zooplankton-orchestrate: pr=1 round=1 fingerprint=${"A".repeat(64)} -->`, // upper-case hex
`<!-- zooplankton-orchestrate: pr=abc round=1 fingerprint=${fpA} -->`, // non-numeric pr
`<!-- zooplankton-orchestrate: pr=1 round=1 phase=security fingerprint=${fpA} -->`, // mixed code+security
` <!-- zooplankton-orchestrate: pr=1 round=1 fingerprint=${fpA} -->`, // leading whitespace
];
for (const marker of badMarkers) {
assert.throws(
() => renderConsolidatedReviewBody([], [], { marker }),
/does not match the expected format/,
`expected reject for marker: ${marker}`,
);
}
});
it("treats findings as empty when given a primitive (string/number/boolean) (R2-C07)", () => {
// Array.isArray(findings) guard is the only contract — primitive inputs
// collapse to zero findings, render a valid (empty) body, and never throw.
const marker = createReviewMarker({ pr: 1, round: 1, fingerprint: fpA });
for (const primitive of ["", "garbage", 42, true, false, null, undefined]) {
const body = renderConsolidatedReviewBody(primitive, [], { marker });
assert.equal(body.split("\n")[0], marker);
assert.match(body, /^Total findings: 0$/m);
assert.doesNotMatch(body, /^- blocking:/m);
assert.doesNotMatch(body, /### File-\/branch-scope findings/);
}
});
it("tolerates malformed reviewers entries (null, empty-string name, non-object) (R2-C03)", () => {
const marker = createReviewMarker({ pr: 1, round: 1, fingerprint: fpA });
const malformed = [
null, // null entry
undefined, // undefined entry
"raw-string", // non-object
42, // number
{ name: "", model: "m" }, // empty-string name
{ name: "valid", model: "" }, // empty model (no parenthesis)
{ name: "ok", model: "claude-sonnet-4.6" }, // sentinel valid entry
];
const body = renderConsolidatedReviewBody([], malformed, { marker });
// The line is emitted because reviewerList.length > 0; missing/empty names
// fall back to the literal "reviewer" placeholder.
assert.match(body, /^Reviewers: .+$/m);
const reviewersLine = body.split("\n").find((l) => l.startsWith("Reviewers:"));
assert.ok(reviewersLine.includes("reviewer"), "fallback name appears for malformed entries");
assert.ok(reviewersLine.includes("ok (claude-sonnet-4.6)"), "valid entry rendered with model");
assert.ok(reviewersLine.includes("valid"), "entry with empty model rendered as bare name");
assert.ok(!reviewersLine.includes("()"), "no empty parens for blank model");
});
it("computeWhyItMatters: empty-string area falls back to path; absent-area dedupes by path (R2-C02)", () => {
const marker = createReviewMarker({ pr: 1, round: 1, fingerprint: fpA });
// (a) area: "" (empty string) is falsy → falls back to path.
const emptyArea = [{
cluster_id: "R1-C30", severity: "blocking", anchor_scope: "file",
path: "src/empty.js", line: null, area: "", summary: "x", detail: null, reviewer_model: "m",
}];
const bodyEmpty = renderConsolidatedReviewBody(emptyArea, reviewers, { marker });
assert.match(bodyEmpty, /^Blocking issues span: src\/empty\.js\. See inline comments for details\.$/m);
// (b) absent-area + same path on multiple findings → dedup by path,
// emitting the path key only once.
const absentAreaSamePath = [
{ cluster_id: "R1-C31", severity: "blocking", anchor_scope: "file", path: "src/dup.js", line: null, summary: "a", detail: null, reviewer_model: "m" },
{ cluster_id: "R1-C32", severity: "blocking", anchor_scope: "line", path: "src/dup.js", line: 5, summary: "b", detail: null, reviewer_model: "m" },
{ cluster_id: "R1-C33", severity: "blocking", anchor_scope: "file", path: "src/other.js", line: null, summary: "c", detail: null, reviewer_model: "m" },
];
const bodyDedup = renderConsolidatedReviewBody(absentAreaSamePath, reviewers, { marker });
assert.match(bodyDedup, /^Blocking issues span: src\/dup\.js, src\/other\.js\. See inline comments for details\.$/m);
});
it("includeLineAnchored:true with no line-anchored findings omits the Tier-3 header (R2-C04)", () => {
const marker = createReviewMarker({ pr: 1, round: 1, fingerprint: fpA });
// All findings are file/branch-scope — Tier 3 header MUST be absent even
// when the caller opts in to body-only fallback.
const fileBranchOnly = [
{ cluster_id: "R1-C40", severity: "blocking", anchor_scope: "file", path: "src/x.js", line: null, area: "auth", summary: "x", detail: null, reviewer_model: "m" },
{ cluster_id: "R1-C41", severity: "advisory", anchor_scope: "branch", path: null, line: null, area: "tests", summary: "y", detail: null, reviewer_model: "m" },
];
const body = renderConsolidatedReviewBody(fileBranchOnly, reviewers, { marker, includeLineAnchored: true });
assert.doesNotMatch(body, /### Line-anchored findings \(body-only fallback\)/);
// File/branch section is still rendered as normal.
assert.match(body, /### File-\/branch-scope findings/);
assert.match(body, /\[R1-C40\] \[blocking\] x — src\/x\.js/);
assert.match(body, /\[R1-C41\] \[advisory\] y — branch-scope/);
});
it("computes deterministic 'Blocking issues span' sentence", () => {
const marker = createReviewMarker({ pr: 1, round: 1, fingerprint: fpA });
const mkBlocking = (cluster_id, area) => ({
cluster_id,
severity: "blocking",
anchor_scope: "branch",
path: null,
line: null,
area,
summary: `Issue ${cluster_id}`,
detail: null,
reviewer_model: "m",
});
// Zero blocking → no "why it matters" sentence.
const advisoryOnly = renderConsolidatedReviewBody([findings[1]], reviewers, { marker });
assert.doesNotMatch(advisoryOnly, /Blocking issues span/);
// Single blocking with area=auth → exact sentence.
const oneAuth = renderConsolidatedReviewBody([mkBlocking("R1-C10", "auth")], reviewers, { marker });
assert.match(oneAuth, /^Blocking issues span: auth\. See inline comments for details\.$/m);
// Multiple blocking across areas ["auth", "billing", "auth"] → dedup, first-occurrence order.
const multi = renderConsolidatedReviewBody(
[
mkBlocking("R1-C11", "auth"),
mkBlocking("R1-C12", "billing"),
mkBlocking("R1-C13", "auth"),
],
reviewers,
{ marker },
);
assert.match(multi, /^Blocking issues span: auth, billing\. See inline comments for details\.$/m);
});
it("falls back from missing area to path; skips finding for sentence when both absent", () => {
const marker = createReviewMarker({ pr: 1, round: 1, fingerprint: fpA });
const items = [
// No area, falls back to path.
{ cluster_id: "R1-C20", severity: "blocking", anchor_scope: "file", path: "src/foo.js", line: null, area: null, summary: "x", detail: null, reviewer_model: "m" },
// No area, no path — must still count toward total/severity, but excluded from sentence.
{ cluster_id: "R1-C21", severity: "blocking", anchor_scope: "branch", path: null, line: null, area: null, summary: "y", detail: null, reviewer_model: "m" },
];
const body = renderConsolidatedReviewBody(items, reviewers, { marker });
assert.match(body, /^Total findings: 2$/m);
assert.match(body, /^- blocking: 2$/m);
assert.match(body, /^Blocking issues span: src\/foo\.js\. See inline comments for details\.$/m);
});
it("breaking change: legacy `title` positional/options arg is silently ignored", () => {
// The new signature is (findings, reviewers, options). Older call sites
// that passed an object as the first arg with a `title` field now end up
// passing a non-array `findings` (treated as zero findings); the marker
// must still come from the options object. Confirm we don't crash and
// produce a valid (empty-findings) body.
const marker = createReviewMarker({ pr: 1, round: 1, fingerprint: fpA });
const body = renderConsolidatedReviewBody(
// legacy callers passed { title, marker, findings, supersedes } as the
// sole positional — under the new signature this becomes `findings` and
// is coerced to an empty list.
{ title: "Legacy", marker, findings: [], supersedes: null },
reviewers,
{ marker },
);
assert.equal(body.split("\n")[0], marker);
assert.match(body, /^Total findings: 0$/m);
// No "Legacy" title heading is rendered.
assert.doesNotMatch(body, /Legacy/);
assert.doesNotMatch(body, /^### /m);
});
it("builds endpoint-neutral inline anchors only for line-anchored findings", () => {
const comments = buildInlineCommentAnchors(findings);
assert.equal(comments.length, 1);
assert.equal(comments[0].path, "src/a.js");
assert.equal(comments[0].position, null);
assert.equal(comments[0]._line, 10);
assert.ok(!("line" in comments[0]));
assert.ok(!("side" in comments[0]));
assert.match(comments[0].body, /Value may be null/);
});
it("sanitizes reviewer-controlled markdown in Tier 3 body and inline anchors", () => {
const marker = createReviewMarker({ pr: 1, round: 1, fingerprint: fpA });
const unsafe = [{
cluster_id: "R1-C03",
severity: "blocking",
anchor_scope: "line",
path: "src/a.js",
line: 11,
area: "auth\n<!-- evil --> @team",
summary: "Title\n<!-- zooplankton-orchestrate: fake --> @team",
detail: "Details\n<!-- hidden --> @team",
reviewer_model: "model\n@team",
}];
// Use Tier 3 fallback so summary/detail are rendered into the body too.
const body = renderConsolidatedReviewBody(unsafe, [{ name: "core-reviewer", model: "m\n@team" }], { marker, includeLineAnchored: true });
const comments = buildInlineCommentAnchors(unsafe);
assert.doesNotMatch(body.replace(marker, ""), /<!-- zooplankton-orchestrate/);
assert.doesNotMatch(body.replace(marker, ""), /<!-- evil/);
assert.doesNotMatch(body.replace(marker, ""), /<!-- hidden/);
assert.match(body, /@\u200bteam/);
assert.doesNotMatch(comments[0].body, /<!-- hidden/);
assert.match(comments[0].body, /@\u200bteam/);
});
it("sanitizes supersedes fingerprint in rendered bodies", () => {
const marker = createReviewMarker({ pr: 1, round: 1, fingerprint: fpA });
// The supersedes value is sliced to 8 chars after sanitization. Use a
// payload whose first 8 sanitized chars trigger both escapes (@ and <!--).
const body = renderConsolidatedReviewBody([], [], {
marker,
supersedes: "@evil<!-- fake -->",
});
assert.doesNotMatch(body.replace(marker, ""), /<!-- fake/);
assert.match(body, /@\u200bevil/);
});
it("plans retry tiers deterministically", () => {
const comments = [
{ path: "b.js", line: 2, body: "b" },
{ path: "a.js", line: 1, body: "a" },
{ path: "c.js", line: 3, body: "c" },
];
const tier1 = planReviewPostRetry(comments, 1);
assert.deepEqual(tier1.comments.map((c) => c.path), ["a.js", "b.js", "c.js"]);
const tier2 = planReviewPostRetry(comments, 2);
assert.equal(tier2.comments.length, 2);
assert.equal(tier2.bodyOnlyComments.length, 1);
const tier3 = planReviewPostRetry(comments, 3);
assert.equal(tier3.comments.length, 0);
assert.equal(tier3.bodyOnlyComments.length, 3);
assert.throws(() => planReviewPostRetry(comments, 4), /tier must be 1\|2\|3/);
});
it("planReviewPostRetry sorts new anchor-shape `{ path, position, _line, body }` comments and `_line` takes precedence over `line` when both are present", () => {
// (a) New anchor-shape objects with _line (and position: null) — produced by
// buildInlineCommentAnchors.
const anchorShape = [
{ path: "b.js", position: null, _line: 2, body: "b" },
{ path: "a.js", position: null, _line: 5, body: "a" },
{ path: "a.js", position: null, _line: 1, body: "a-early" },
];
const t1 = planReviewPostRetry(anchorShape, 1);
assert.deepEqual(
t1.comments.map((c) => `${c.path}:${c._line}`),
["a.js:1", "a.js:5", "b.js:2"],
"anchor-shape sort by path then _line"
);
// (b) When BOTH `_line` and `line` are present, `_line` wins (precedence).
// Construct two entries on the same path where `line` order would be
// [10, 1] but `_line` order is [1, 10] — the result must follow `_line`.
const mixed = [
{ path: "x.js", position: null, _line: 10, line: 1, body: "later-by-_line" },
{ path: "x.js", position: null, _line: 1, line: 10, body: "earlier-by-_line" },
];
const tMixed = planReviewPostRetry(mixed, 1);
assert.deepEqual(
tMixed.comments.map((c) => c.body),
["earlier-by-_line", "later-by-_line"],
"_line takes precedence over line in sort key"
);
});
});
/**
* Unit tests for .opencode/plugins/lib/guide-paths.js (getGuidePath, ROLE_TO_GUIDE_BASENAME, unified guide sanity).
*
* Uses Node.js built-in test runner (node:test + node:assert). Zero new dependencies.
*/
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import fs from "fs";
import path from "path";
import { makeTmpDir, writeFixture } from "../helpers/fixtures.js";
import {
getGuidePath,
ROLE_TO_GUIDE_BASENAME,
} from "../../.opencode/plugins/lib.js";
describe("getGuidePath", () => {
const roles = [
"coreCoder",
"juniorCoder",
"coreReviewer",
"reviewer",
"securityReviewer",
"frontendCoder",
"frontendReviewer",
"explorer",
];
it("returns an absolute path ending in guides/<basename>.md with no platform suffix for all roles", () => {
for (const role of roles) {
const p = getGuidePath(role);
assert.ok(path.isAbsolute(p), `${role}: path must be absolute`);
assert.ok(p.endsWith(".md"), `${role}: must end with .md`);
assert.ok(p.includes(`${path.sep}guides${path.sep}`), `${role}: must live under /guides/`);
assert.ok(!/-guide-(github|local)\.md$/.test(p), `${role}: must not contain -github or -local suffix`);
assert.ok(/-guide\.md$/.test(p), `${role}: basename must end with -guide.md`);
}
});
it("throws for unknown role", () => {
assert.throws(() => getGuidePath("unknownRole"), /Unknown agent role: unknownRole/);
});
});
// SUITE: unified guide sanity
//
// As of PR-4 (v0.5) the 8 role guides are unified into a single platform-
// agnostic file per role at `guides/<role>-guide.md`. The previous per-
// platform variants (`-github.md`, `-local.md`) are kept temporarily as
// deprecation shims and will be removed in v0.6. This suite enforces:
//
// 1. All 8 unified guides exist.
// 2. Each guide opens with `# <Title> Guide` (no "— Dispatcher" suffix
// from the old skeleton).
// 3. The non-junior, non-explorer guides each contain at least one
// `> **GitHub mode:**` and one `> **Local mode:**` blockquote callout
// — the mechanism by which mode-specific behavior is described in
// unified guides. `junior-coder` is exempt because it delegates to
// `core-coder`. `explorer` is exempt because it is a read-only
// codebase-exploration agent with no platform-specific behavior
// (no git, no gh, no PR involvement).
// 4. No unified guide references the deprecated variants (`-github.md` /
// `-local.md`) or the legacy `project.platform` dispatch instruction.
// Mode-specific content lives in inline callouts instead.
describe("unified guide sanity", () => {
const guidesDir = path.resolve(
path.dirname(new URL(import.meta.url).pathname),
"..",
"..",
"guides"
);
const allBasenames = Object.values(ROLE_TO_GUIDE_BASENAME);
// Exempt from callout requirement: junior-coder (delegates to
// core-coder) and explorer (read-only, no platform-specific behavior).
const calloutRoles = allBasenames.filter(
(b) => b !== "junior-coder-guide" && b !== "explorer-guide"
);
const readGuideFile = (basename) =>
fs.readFileSync(path.join(guidesDir, `${basename}.md`), "utf8");
const titleFor = (basename) =>
basename
.replace(/-guide$/, "")
.split("-")
.map((w) => w[0].toUpperCase() + w.slice(1))
.join(" ");
it("all 8 unified guides exist", () => {
for (const basename of allBasenames) {
const p = path.join(guidesDir, `${basename}.md`);
assert.ok(fs.existsSync(p), `missing unified guide: ${basename}.md`);
}
});
it("each guide opens with '# <Title> Guide' (no Dispatcher suffix)", () => {
for (const basename of allBasenames) {
const content = readGuideFile(basename);
const firstLine = content.split("\n")[0];
const expected = `# ${titleFor(basename)} Guide`;
assert.equal(
firstLine,
expected,
`${basename}: first line must be exactly "${expected}" (got "${firstLine}")`
);
}
});
it("non-junior guides contain GitHub-mode and Local-mode callouts", () => {
for (const basename of calloutRoles) {
const content = readGuideFile(basename);
assert.ok(
content.includes("> **GitHub mode:**"),
`${basename}: missing "> **GitHub mode:**" callout`
);
assert.ok(
content.includes("> **Local mode:**"),
`${basename}: missing "> **Local mode:**" callout`
);
}
});
it("no unified guide references deprecated -github.md or -local.md variants", () => {
for (const basename of allBasenames) {
const content = readGuideFile(basename);
assert.ok(
!/-github\.md/.test(content),
`${basename}: must not reference deprecated -github.md variant`
);
assert.ok(
!/-local\.md/.test(content),
`${basename}: must not reference deprecated -local.md variant`
);
}
});
it("non-junior guides do not contain the legacy project.platform dispatch instruction", () => {
// Unified guides describe mode-specific behavior via inline callouts;
// they no longer instruct the agent to read project.platform and pick
// a variant. (Junior-coder is exempt: it delegates to core-coder, and
// its short stub may still mention the platform field for context.)
for (const basename of calloutRoles) {
const content = readGuideFile(basename);
assert.ok(
!/read.*project\.platform.*and.*(load|pick|dispatch)/i.test(content),
`${basename}: must not retain legacy "read project.platform and dispatch" instruction`
);
}
});
});
// SUITE: generateAgentMd
/**
* Unit tests for .opencode/plugins/lib/logger.js (createLogger).
*
* Uses Node.js built-in test runner (node:test + node:assert). Zero new dependencies.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { createLogger } from "../../.opencode/plugins/lib.js";
describe("createLogger", () => {
it("falls back to console.warn when client is null", async () => {
const messages = [];
const origWarn = console.warn;
console.warn = (...args) => messages.push(args.join(" "));
try {
const log = createLogger(null);
await log("warn", "test warning");
assert.ok(messages.some((m) => m.includes("test warning")), "warn message must appear");
} finally {
console.warn = origWarn;
}
});
it("falls back to console.error when client is null and level is error", async () => {
const messages = [];
const origError = console.error;
console.error = (...args) => messages.push(args.join(" "));
try {
const log = createLogger(null);
await log("error", "test error");
assert.ok(messages.some((m) => m.includes("test error")), "error message must appear");
} finally {
console.error = origError;
}
});
it("uses client.app.log when client is provided", async () => {
const calls = [];
const fakeClient = {
app: {
log: async ({ body }) => { calls.push(body); },
},
};
const log = createLogger(fakeClient);
await log("info", "hello from plugin", { key: "val" });
assert.equal(calls.length, 1);
assert.equal(calls[0].level, "info");
assert.equal(calls[0].message, "hello from plugin");
assert.equal(calls[0].service, "opencode-plugin-zooplankton");
assert.deepEqual(calls[0].extra, { key: "val" });
});
it("falls back to console when client.app.log throws", async () => {
const messages = [];
const origWarn = console.warn;
console.warn = (...args) => messages.push(args.join(" "));
const fakeClient = {
app: {
log: async () => { throw new Error("log failed"); },
},
};
try {
const log = createLogger(fakeClient);
await log("warn", "fallback message");
assert.ok(messages.some((m) => m.includes("fallback message")), "must fall back to console on error");
} finally {
console.warn = origWarn;
}
});
});
/**
* Unit tests for .opencode/plugins/lib/platform.js (detectPlatform, readZooplanktonJson, readZooplanktonLocalJson).
*
* Uses Node.js built-in test runner (node:test + node:assert). Zero new dependencies.
*/
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import fs from "fs";
import path from "path";
import { makeTmpDir, writeFixture } from "../helpers/fixtures.js";
import {
detectPlatform,
readZooplanktonJson,
readZooplanktonLocalJson,
} from "../../.opencode/plugins/lib.js";
describe("detectPlatform", () => {
it("returns 'github' when project.platform is explicitly 'github'", () => {
assert.equal(detectPlatform({ project: { platform: "github" } }), "github");
});
it("returns 'local' when project.platform is explicitly 'local'", () => {
assert.equal(detectPlatform({ project: { platform: "local" } }), "local");
});
it("auto-detects 'github' when project.repo contains github.com", () => {
assert.equal(
detectPlatform({ project: { repo: "https://github.com/Org/repo" } }),
"github"
);
});
it("auto-detects 'local' when project.repo is a non-GitHub URL", () => {
assert.equal(
detectPlatform({ project: { repo: "https://gitlab.com/Org/repo" } }),
"local"
);
});
it("auto-detects 'local' when project.repo is a bare Org/repo slug", () => {
assert.equal(
detectPlatform({ project: { repo: "Org/repo" } }),
"local"
);
});
it("auto-detects 'local' when project.repo is empty", () => {
assert.equal(detectPlatform({ project: { repo: "" } }), "local");
});
it("auto-detects 'local' when project.repo is absent", () => {
assert.equal(detectPlatform({ project: {} }), "local");
});
it("auto-detects 'local' when project is absent", () => {
assert.equal(detectPlatform({}), "local");
});
it("auto-detects 'local' when workflow is null", () => {
assert.equal(detectPlatform(null), "local");
});
it("explicit platform overrides repo auto-detection", () => {
assert.equal(
detectPlatform({
project: { repo: "https://github.com/Org/repo", platform: "local" },
}),
"local"
);
});
it("auto-detects 'github' when repo matches a githubEnterpriseHosts entry", () => {
assert.equal(
detectPlatform({
project: {
repo: "https://github.mycompany.com/Org/repo",
githubEnterpriseHosts: ["github.mycompany.com"],
},
}),
"github"
);
});
it("auto-detects 'local' when repo does not match any githubEnterpriseHosts entry", () => {
assert.equal(
detectPlatform({
project: {
repo: "https://gitlab.mycompany.com/Org/repo",
githubEnterpriseHosts: ["github.mycompany.com"],
},
}),
"local"
);
});
it("ignores githubEnterpriseHosts when it is not an array", () => {
assert.equal(
detectPlatform({
project: {
repo: "https://github.mycompany.com/Org/repo",
githubEnterpriseHosts: "github.mycompany.com",
},
}),
"local"
);
});
it("ignores non-string entries in githubEnterpriseHosts", () => {
assert.equal(
detectPlatform({
project: {
repo: "https://github.mycompany.com/Org/repo",
githubEnterpriseHosts: [null, 123, "", "github.mycompany.com"],
},
}),
"github"
);
});
});
// SUITE: loadCommands
describe("readZooplanktonJson", () => {
let tmpDir;
before(() => { tmpDir = makeTmpDir(); });
after(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
it("returns null when no zooplankton.json exists", () => {
assert.equal(readZooplanktonJson(tmpDir), null);
});
it("reads zooplankton.json from project root", () => {
writeFixture(tmpDir, "zooplankton.json", '{"project":{"name":"test"}}');
const result = readZooplanktonJson(tmpDir);
assert.equal(result.project.name, "test");
});
it("falls back to .opencode/zooplankton.json", () => {
const dir2 = makeTmpDir();
writeFixture(dir2, ".opencode/zooplankton.json", '{"project":{"name":"fallback"}}');
const result = readZooplanktonJson(dir2);
assert.equal(result.project.name, "fallback");
fs.rmSync(dir2, { recursive: true, force: true });
});
it("prefers project root zooplankton.json over .opencode/", () => {
const dir2 = makeTmpDir();
writeFixture(dir2, "zooplankton.json", '{"project":{"name":"root"}}');
writeFixture(dir2, ".opencode/zooplankton.json", '{"project":{"name":"nested"}}');
const result = readZooplanktonJson(dir2);
assert.equal(result.project.name, "root");
fs.rmSync(dir2, { recursive: true, force: true });
});
it("returns null for invalid JSON", () => {
const dir2 = makeTmpDir();
writeFixture(dir2, "zooplankton.json", "not-json{");
// Fail closed: invalid root file must not fall through to .opencode/ path
writeFixture(dir2, ".opencode/zooplankton.json", '{"project":{"name":"fallback"}}');
assert.equal(readZooplanktonJson(dir2), null);
fs.rmSync(dir2, { recursive: true, force: true });
});
it("reads deeply nested JSON values", () => {
const dir2 = makeTmpDir();
writeFixture(dir2, "zooplankton.json", '{"agents":{"coreCoder":{"name":"cc","model":"m1"}}}');
const result = readZooplanktonJson(dir2);
assert.equal(result.agents.coreCoder.name, "cc");
assert.equal(result.agents.coreCoder.model, "m1");
fs.rmSync(dir2, { recursive: true, force: true });
});
});
// SUITE: readZooplanktonLocalJson
describe("readZooplanktonLocalJson", () => {
let tmpDir;
before(() => { tmpDir = makeTmpDir(); });
after(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
it("returns null when no file exists", () => {
assert.equal(readZooplanktonLocalJson(tmpDir), null);
});
it("reads zooplankton-local.json from project root", () => {
writeFixture(tmpDir, "zooplankton-local.json", '{"github":{"account":{"default":"user1"}}}');
const result = readZooplanktonLocalJson(tmpDir);
assert.equal(result.github.account.default, "user1");
});
it("falls back to .opencode/zooplankton-local.json", () => {
const dir2 = makeTmpDir();
writeFixture(dir2, ".opencode/zooplankton-local.json", '{"github":{"account":{"default":"user2"}}}');
const result = readZooplanktonLocalJson(dir2);
assert.equal(result.github.account.default, "user2");
fs.rmSync(dir2, { recursive: true, force: true });
});
it("returns null for invalid JSON", () => {
const dir2 = makeTmpDir();
writeFixture(dir2, "zooplankton-local.json", "{bad");
// Fail closed: invalid root file must not fall through to .opencode/ path
writeFixture(dir2, ".opencode/zooplankton-local.json", '{"github":{"account":{"default":"fallback"}}}');
assert.equal(readZooplanktonLocalJson(dir2), null);
fs.rmSync(dir2, { recursive: true, force: true });
});
it("prefers project root over .opencode/ path", () => {
const dir2 = makeTmpDir();
writeFixture(dir2, "zooplankton-local.json", '{"github":{"account":{"default":"root-user"}}}');
writeFixture(dir2, ".opencode/zooplankton-local.json", '{"github":{"account":{"default":"nested-user"}}}');
const result = readZooplanktonLocalJson(dir2);
assert.equal(result.github.account.default, "root-user");
fs.rmSync(dir2, { recursive: true, force: true });
});
});
// SUITE: getAgentPermissions
/**
* Unit tests for .opencode/plugins/lib/reviewer-protocol.js (parseReviewerResult).
*
* Uses Node.js built-in test runner (node:test + node:assert). Zero new dependencies.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { parseReviewerResult } from "../../.opencode/plugins/lib.js";
describe("parseReviewerResult", () => {
const makeValidPayload = (overrides = {}) => ({
schema_version: 1,
review_kind: "code",
reviewer: { name: "reviewer-glm", model: "glm-5", role: "reviewer", round: 1 },
verification: { typecheck: "PASS", lint: "PASS", test: "PASS", build: "N/A" },
findings: [],
security_verdict: null,
prior_issue_resolution: [],
cross_reviewer_notes: "",
...overrides,
});
it("parses a well-formed payload from a fenced json block", () => {
const payload = makeValidPayload({
findings: [
{ ephemeral_id: "f1", severity: "blocking", anchor_scope: "line", path: "src/a.ts", line: 10, area: "logic", summary: "Null deref possible" },
],
});
const text = "# Review\n\nSome prose\n\n```json\n" + JSON.stringify(payload) + "\n```\n\nMore prose.";
const result = parseReviewerResult(text);
assert.equal(result.ok, true);
assert.equal(result.errors.length, 0);
assert.equal(result.value.findings.length, 1);
assert.equal(result.value.findings[0].summary, "Null deref possible");
assert.equal(result.value.review_kind, "code");
});
it("accepts a pre-parsed object and skips fence extraction", () => {
const result = parseReviewerResult(makeValidPayload());
assert.equal(result.ok, true);
assert.equal(result.value.findings.length, 0);
});
it("returns ok:false with an error when no fenced json block is present", () => {
const result = parseReviewerResult("just prose, no code fences here");
assert.equal(result.ok, false);
assert.equal(result.value.findings.length, 0);
assert.ok(result.errors.some((e) => /no fenced.*json block/.test(e)));
});
it("returns ok:false on malformed JSON inside the fence", () => {
const result = parseReviewerResult("```json\n{ not valid json\n```");
assert.equal(result.ok, false);
assert.equal(result.value.findings.length, 0);
assert.ok(result.errors.some((e) => /JSON parse error/.test(e)));
});
it("unknown schema_version → ok:false with a warning, empty findings", () => {
const payload = { ...makeValidPayload(), schema_version: 99, findings: [
{ ephemeral_id: "f1", severity: "blocking", anchor_scope: "file", path: "x.ts", line: null, summary: "x" },
] };
const result = parseReviewerResult("```json\n" + JSON.stringify(payload) + "\n```");
assert.equal(result.ok, false);
assert.equal(result.value.findings.length, 0);
assert.ok(result.warnings.some((w) => /unknown or missing schema_version/.test(w)));
});
it("missing review_kind defaults to 'code' (back-compat)", () => {
const payload = makeValidPayload();
delete payload.review_kind;
const result = parseReviewerResult("```json\n" + JSON.stringify(payload) + "\n```");
assert.equal(result.ok, true);
assert.equal(result.value.review_kind, "code");
});
it("review_kind='code' requires a verification object; missing object → error, empty findings", () => {
const payload = { ...makeValidPayload(), verification: null };
const result = parseReviewerResult("```json\n" + JSON.stringify(payload) + "\n```");
assert.equal(result.ok, false);
assert.ok(result.errors.some((e) => /verification object/.test(e)));
});
it("review_kind='code' fills missing verification keys with 'N/A'", () => {
const payload = { ...makeValidPayload(), verification: { typecheck: "PASS" } };
const result = parseReviewerResult(payload);
assert.equal(result.ok, true);
assert.equal(result.value.verification.lint, "N/A");
assert.equal(result.value.verification.test, "N/A");
assert.equal(result.value.verification.build, "N/A");
});
it("review_kind='plan' strips non-null verification with a warning", () => {
const payload = makeValidPayload({
review_kind: "plan",
verification: { typecheck: "PASS", lint: "PASS", test: "PASS", build: "PASS" },
});
const result = parseReviewerResult(payload);
assert.equal(result.ok, true);
assert.equal(result.value.verification, null);
assert.ok(result.warnings.some((w) => /non-null verification on plan review/.test(w)));
});
it("findings with only legacy `scope` field (no `anchor_scope`) are dropped as invalid (back-compat alias removed in PR-5a)", () => {
const payload = makeValidPayload({
findings: [{ ephemeral_id: "f1", severity: "blocking", scope: "line", path: "x.ts", line: 1, summary: "hi" }],
});
const result = parseReviewerResult(payload);
assert.equal(result.ok, true);
assert.equal(result.value.findings.length, 0);
assert.ok(result.warnings.some((w) => /invalid anchor_scope/.test(w)));
});
it("drops individual findings with invalid anchor_scope/path/line combos (fail-closed per finding)", () => {
const payload = makeValidPayload({
findings: [
// Valid
{ ephemeral_id: "f1", severity: "blocking", anchor_scope: "line", path: "a.ts", line: 10, summary: "ok" },
// anchor_scope=line with null line → drop
{ ephemeral_id: "f2", severity: "blocking", anchor_scope: "line", path: "a.ts", line: null, summary: "bad" },
// anchor_scope=branch with a path → drop
{ ephemeral_id: "f3", severity: "advisory", anchor_scope: "branch", path: "a.ts", line: null, summary: "bad" },
// Invalid severity for code review → drop
{ ephemeral_id: "f4", severity: "critical", anchor_scope: "file", path: "a.ts", line: null, summary: "bad" },
// Missing summary → drop
{ ephemeral_id: "f5", severity: "advisory", anchor_scope: "file", path: "a.ts", line: null, summary: "" },
],
});
const result = parseReviewerResult(payload);
assert.equal(result.ok, true);
assert.equal(result.value.findings.length, 1);
assert.equal(result.value.findings[0].ephemeral_id, "f1");
// 4 dropped findings → 4 warnings
assert.ok(result.warnings.length >= 4);
});
it("security reviewer allows critical/high/medium/low/info severities", () => {
const payload = makeValidPayload({
reviewer: { name: "sec", model: "gpt", role: "security", round: 1 },
security_verdict: "BLOCK",
findings: [
{ severity: "critical", anchor_scope: "file", path: "a.ts", line: null, summary: "shell injection" },
{ severity: "info", anchor_scope: "branch", path: null, line: null, summary: "observation" },
],
});
const result = parseReviewerResult(payload);
assert.equal(result.ok, true);
assert.equal(result.value.findings.length, 2);
assert.equal(result.value.security_verdict, "BLOCK");
});
it("top-level findings not an array → error, empty findings", () => {
const payload = { ...makeValidPayload(), findings: "not an array" };
const result = parseReviewerResult(payload);
assert.equal(result.ok, false);
assert.ok(result.errors.some((e) => /findings must be an array/.test(e)));
assert.equal(result.value.findings.length, 0);
});
it("only the first fenced ```json block is used (subsequent blocks ignored)", () => {
const first = makeValidPayload({
findings: [{ severity: "blocking", anchor_scope: "line", path: "a.ts", line: 1, summary: "first" }],
});
const second = makeValidPayload({
findings: [{ severity: "blocking", anchor_scope: "line", path: "b.ts", line: 2, summary: "second" }],
});
const text = "```json\n" + JSON.stringify(first) + "\n```\n\n```json\n" + JSON.stringify(second) + "\n```";
const result = parseReviewerResult(text);
assert.equal(result.ok, true);
assert.equal(result.value.findings.length, 1);
assert.equal(result.value.findings[0].summary, "first");
});
// --- Round-1 review follow-ups -----------------------------------------
// B1: normalize documented-but-extended verification values
it("normalizes verification 'FAIL: <reason>' to 'FAIL' with warning", () => {
const payload = makeValidPayload({
verification: { typecheck: "FAIL: 3 errors", lint: "PASS", test: "PASS", build: "N/A" },
});
const result = parseReviewerResult(payload);
assert.equal(result.ok, true);
assert.equal(result.value.verification.typecheck, "FAIL");
assert.ok(result.warnings.some((w) => /typecheck.*normalized to "FAIL"/.test(w)));
});
it("normalizes verification 'N/A — not configured' to 'N/A' with warning", () => {
const payload = makeValidPayload({
verification: { typecheck: "N/A — not configured", lint: "N/A: none", test: "PASS", build: "N/A" },
});
const result = parseReviewerResult(payload);
assert.equal(result.ok, true);
assert.equal(result.value.verification.typecheck, "N/A");
assert.equal(result.value.verification.lint, "N/A");
assert.ok(result.warnings.some((w) => /typecheck.*normalized to "N\/A"/.test(w)));
assert.ok(result.warnings.some((w) => /lint.*normalized to "N\/A"/.test(w)));
});
it("rejects verification values that still don't match any canonical form", () => {
const payload = makeValidPayload({
verification: { typecheck: "WOBBLY", lint: "PASS", test: "PASS", build: "N/A" },
});
const result = parseReviewerResult(payload);
assert.equal(result.ok, false);
assert.ok(result.errors.some((e) => /verification\.typecheck.*WOBBLY/.test(e)));
});
// B2: security_verdict must be PASS or BLOCK for security reviewers
it("security reviewer with security_verdict='BLOCK' parses ok", () => {
const payload = makeValidPayload({
reviewer: { name: "sec", model: "m", role: "security", round: 1 },
findings: [{ severity: "critical", anchor_scope: "file", path: "a.ts", line: null, summary: "x" }],
security_verdict: "BLOCK",
});
const result = parseReviewerResult(payload);
assert.equal(result.ok, true);
assert.equal(result.value.security_verdict, "BLOCK");
});
it("security reviewer with security_verdict='PASS' parses ok", () => {
const payload = makeValidPayload({
reviewer: { name: "sec", model: "m", role: "security", round: 1 },
security_verdict: "PASS",
});
const result = parseReviewerResult(payload);
assert.equal(result.ok, true);
assert.equal(result.value.security_verdict, "PASS");
});
it("security reviewer with null security_verdict → ok:false with clear error", () => {
const payload = makeValidPayload({
reviewer: { name: "sec", model: "m", role: "security", round: 1 },
security_verdict: null,
});
const result = parseReviewerResult(payload);
assert.equal(result.ok, false);
assert.ok(result.errors.some((e) => /security reviewer must set security_verdict to "PASS" or "BLOCK"/.test(e)));
assert.equal(result.value.findings.length, 0);
});
it("security reviewer with missing security_verdict → ok:false", () => {
const payload = makeValidPayload({
reviewer: { name: "sec", model: "m", role: "security", round: 1 },
});
delete payload.security_verdict;
const result = parseReviewerResult(payload);
assert.equal(result.ok, false);
assert.ok(result.errors.some((e) => /security reviewer must set security_verdict/.test(e)));
});
it("security reviewer with bogus security_verdict value → ok:false", () => {
const payload = makeValidPayload({
reviewer: { name: "sec", model: "m", role: "security", round: 1 },
security_verdict: "MAYBE",
});
const result = parseReviewerResult(payload);
assert.equal(result.ok, false);
assert.ok(result.errors.some((e) => /MAYBE/.test(e)));
});
it("non-security reviewer with null security_verdict still parses ok", () => {
const payload = makeValidPayload({
reviewer: { name: "r", model: "m", role: "reviewer", round: 1 },
security_verdict: null,
});
const result = parseReviewerResult(payload);
assert.equal(result.ok, true);
assert.equal(result.value.security_verdict, null);
});
// B3: unclosed fence → "no fenced json block found" (not a confusing parse error)
it("opening ```json with no closing fence → ok:false, 'no fenced json block found'", () => {
const text = "# Review\n\n```json\n{ \"schema_version\": 1, \"findings\": [] }\n\nforgot to close the fence";
const result = parseReviewerResult(text);
assert.equal(result.ok, false);
assert.ok(result.errors.some((e) => /no fenced.*json block found/.test(e)));
assert.ok(!result.errors.some((e) => /JSON parse error/.test(e)));
});
it("fenced block closed on its own line is still found", () => {
const payload = makeValidPayload();
const text = "preamble\n```json\n" + JSON.stringify(payload) + "\n```\nepilogue";
const result = parseReviewerResult(text);
assert.equal(result.ok, true);
});
// A1: line must be a positive integer
it("anchor_scope=line with line=0 is dropped (positive integer required)", () => {
const payload = makeValidPayload({
findings: [
{ ephemeral_id: "f0", severity: "blocking", anchor_scope: "line", path: "a.ts", line: 0, summary: "zero" },
{ ephemeral_id: "f1", severity: "blocking", anchor_scope: "line", path: "a.ts", line: 1, summary: "one" },
],
});
const result = parseReviewerResult(payload);
assert.equal(result.ok, true);
assert.equal(result.value.findings.length, 1);
assert.equal(result.value.findings[0].ephemeral_id, "f1");
assert.ok(result.warnings.some((w) => /positive integer/.test(w)));
});
it("anchor_scope=line with non-integer line is dropped", () => {
const payload = makeValidPayload({
findings: [
{ severity: "blocking", anchor_scope: "line", path: "a.ts", line: 10.5, summary: "frac" },
],
});
const result = parseReviewerResult(payload);
assert.equal(result.ok, true);
assert.equal(result.value.findings.length, 0);
});
// A2: path normalization strips diff-header prefixes
it("finding path 'a/src/foo.ts' is normalized to 'src/foo.ts' with warning", () => {
const payload = makeValidPayload({
findings: [
{ severity: "blocking", anchor_scope: "line", path: "a/src/foo.ts", line: 5, summary: "x" },
],
});
const result = parseReviewerResult(payload);
assert.equal(result.ok, true);
assert.equal(result.value.findings[0].path, "src/foo.ts");
assert.ok(result.warnings.some((w) => /normalized to "src\/foo\.ts"/.test(w)));
});
it("finding path './src/foo.ts' is normalized to 'src/foo.ts'", () => {
const payload = makeValidPayload({
findings: [
{ severity: "blocking", anchor_scope: "line", path: "./src/foo.ts", line: 5, summary: "x" },
],
});
const result = parseReviewerResult(payload);
assert.equal(result.ok, true);
assert.equal(result.value.findings[0].path, "src/foo.ts");
});
// A3: invalid review_kind warns and defaults to "code"
it("unrecognised review_kind warns and defaults to 'code'", () => {
const payload = makeValidPayload({ review_kind: "doc" });
const result = parseReviewerResult(payload);
assert.equal(result.ok, true);
assert.equal(result.value.review_kind, "code");
assert.ok(result.warnings.some((w) => /unrecognised review_kind "doc"/.test(w)));
});
// A4: extra edge-case coverage
it("review_kind='' (empty string) defaults to 'code'", () => {
const payload = makeValidPayload({ review_kind: "" });
const result = parseReviewerResult(payload);
assert.equal(result.ok, true);
assert.equal(result.value.review_kind, "code");
});
it("verification field as array → ok:false (not an object)", () => {
const payload = makeValidPayload({ verification: ["PASS", "PASS"] });
const result = parseReviewerResult(payload);
assert.equal(result.ok, false);
assert.ok(result.errors.some((e) => /verification object/.test(e)));
});
});
// ---------------------------------------------------------------------------
// SUITE: orchestrator helpers — cluster IDs, dedup, fingerprint, security
// fail-closed. Added in PR-2 (B4) to make core orchestrator logic unit-testable.
// ---------------------------------------------------------------------------
/**
* Unit tests for .opencode/plugins/lib/security.js (synthesizeSecurityBlockFinding, checkSecurityVerdict).
*
* Uses Node.js built-in test runner (node:test + node:assert). Zero new dependencies.
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import path from "path";
import {
synthesizeSecurityBlockFinding,
checkSecurityVerdict,
} from "../../.opencode/plugins/lib.js";
describe("synthesizeSecurityBlockFinding", () => {
it("produces a critical branch-scope finding with null path/line (B5 shape)", () => {
const f = synthesizeSecurityBlockFinding("reviewer-sec", "parse_failure", "unexpected token");
// B1 (round 2): severity must use security vocabulary (critical/high/medium/
// low/info), NOT the code-review "blocking". A parse failure is treated as
// the most severe class so the security gate routes it back to the coder.
assert.equal(f.severity, "critical");
assert.equal(f.anchor_scope, "branch");
assert.equal(f.path, null);
assert.equal(f.line, null);
assert.equal(f.area, "error handling");
assert.ok(/reviewer-sec/.test(f.summary));
assert.ok(/malformed or unparseable JSON/.test(f.summary));
assert.ok(/treating as BLOCK/.test(f.summary));
assert.equal(f.ephemeral_id, "synthetic-security-parse_failure");
assert.equal(f.detail, "unexpected token");
});
it("formats verdict_missing reason correctly", () => {
const f = synthesizeSecurityBlockFinding("reviewer-sec", "verdict_missing");
assert.ok(/no security_verdict/.test(f.summary));
assert.equal(f.ephemeral_id, "synthetic-security-verdict_missing");
assert.equal(f.severity, "critical");
});
});
describe("checkSecurityVerdict (fail-closed)", () => {
it("parse failure → BLOCK with synthetic finding", () => {
const parseResult = { ok: false, errors: ["JSON parse error: unexpected token"], warnings: [], value: null };
const out = checkSecurityVerdict(parseResult, "reviewer-sec");
assert.equal(out.verdict, "BLOCK");
assert.equal(out.synthetic, true);
assert.equal(out.findings.length, 1);
assert.equal(out.findings[0].severity, "critical");
assert.ok(/malformed or unparseable JSON/.test(out.findings[0].summary));
});
it("ok:true but security_verdict missing (null) → BLOCK with synthetic finding", () => {
const parseResult = {
ok: true,
errors: [],
warnings: [],
value: { security_verdict: null, findings: [{ summary: "real finding", severity: "high" }] },
};
const out = checkSecurityVerdict(parseResult, "reviewer-sec");
assert.equal(out.verdict, "BLOCK");
assert.equal(out.synthetic, true);
// Real finding is preserved + synthetic appended
assert.equal(out.findings.length, 2);
assert.equal(out.findings[0].summary, "real finding");
assert.ok(/no security_verdict/.test(out.findings[1].summary));
});
// A2 (round 2): explicit coverage for the key-absent `undefined` case, not
// just explicit `null`. Both must route to BLOCK via the same fail-closed path.
it("ok:true but security_verdict key absent (undefined) → BLOCK with synthetic finding", () => {
const parseResult = {
ok: true,
errors: [],
warnings: [],
value: { findings: [] }, // security_verdict key entirely absent
};
const out = checkSecurityVerdict(parseResult, "reviewer-sec");
assert.equal(out.verdict, "BLOCK");
assert.equal(out.synthetic, true);
assert.equal(out.findings.length, 1);
assert.ok(/no security_verdict/.test(out.findings[0].summary));
});
it("ok:true and security_verdict='BLOCK' → pass-through", () => {
const parseResult = {
ok: true,
errors: [],
warnings: [],
value: { security_verdict: "BLOCK", findings: [{ summary: "x", severity: "critical" }] },
};
const out = checkSecurityVerdict(parseResult, "reviewer-sec");
assert.equal(out.verdict, "BLOCK");
assert.equal(out.synthetic, false);
assert.equal(out.findings.length, 1);
});
it("ok:true and security_verdict='PASS' → pass-through", () => {
const parseResult = {
ok: true,
errors: [],
warnings: [],
value: { security_verdict: "PASS", findings: [] },
};
const out = checkSecurityVerdict(parseResult, "reviewer-sec");
assert.equal(out.verdict, "PASS");
assert.equal(out.synthetic, false);
assert.equal(out.findings.length, 0);
});
it("invalid security_verdict value → BLOCK with synthetic finding", () => {
const parseResult = {
ok: true,
errors: [],
warnings: [],
value: { security_verdict: "MAYBE", findings: [] },
};
const out = checkSecurityVerdict(parseResult, "reviewer-sec");
assert.equal(out.verdict, "BLOCK");
assert.equal(out.synthetic, true);
});
});
/**
* Unit tests for .opencode/plugins/opencode-plugin-zooplankton.js (ZooplanktonCodingPlugin server hooks).
*
* Uses Node.js built-in test runner (node:test + node:assert). Zero new dependencies.
*/
import { describe, it, before, after } from "node:test";
import assert from "node:assert/strict";
import fs from "fs";
import os from "os";
import path from "path";
import { makeTmpDir, writeFixture } from "./helpers/fixtures.js";
import { ZooplanktonCodingPlugin } from "../.opencode/plugins/opencode-plugin-zooplankton.js";
describe("ZooplanktonCodingPlugin", () => {
let tmpDir;
before(() => { tmpDir = makeTmpDir(); });
after(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); });
it("adds skills path to config.skills.paths", async () => {
const plugin = await ZooplanktonCodingPlugin({ directory: tmpDir });
const config = {};
await plugin.config(config);
assert.ok(Array.isArray(config.skills.paths));
assert.ok(config.skills.paths.length > 0);
const skillsPath = config.skills.paths[0];
assert.ok(skillsPath.endsWith("skills"), `Expected skills path, got: ${skillsPath}`);
});
it("does not duplicate skills path on second call", async () => {
const plugin = await ZooplanktonCodingPlugin({ directory: tmpDir });
const config = {};
await plugin.config(config);
const countBefore = config.skills.paths.length;
await plugin.config(config);
assert.equal(config.skills.paths.length, countBefore);
});
it("loads commands into config.command on every install (global and project)", async () => {
const plugin = await ZooplanktonCodingPlugin({ directory: tmpDir });
const config = {};
await plugin.config(config);
assert.ok(config.command["zooplankton-init"]);
assert.ok(config.command["zooplankton-update"]);
});
it("does not override pre-set commands", async () => {
const plugin = await ZooplanktonCodingPlugin({ directory: tmpDir });
const config = {
command: {
"zooplankton-init": { description: "user override" },
},
};
await plugin.config(config);
assert.equal(config.command["zooplankton-init"].description, "user override");
});
it("exposes permission.asked hook", async () => {
const plugin = await ZooplanktonCodingPlugin({ directory: tmpDir });
assert.ok(typeof plugin["permission.asked"] === "function");
});
it("permission.asked allows when autonomousMode is true", async () => {
const dir2 = makeTmpDir();
writeFixture(dir2, "zooplankton.json", '{"autonomousMode": true}');
const plugin = await ZooplanktonCodingPlugin({ directory: dir2 });
const output = {};
await plugin["permission.asked"]({ input: {}, output });
assert.equal(output.action, "allow");
fs.rmSync(dir2, { recursive: true, force: true });
});
it("permission.asked does not set action when autonomousMode is false", async () => {
const dir2 = makeTmpDir();
writeFixture(dir2, "zooplankton.json", '{"autonomousMode": false}');
const plugin = await ZooplanktonCodingPlugin({ directory: dir2 });
const output = {};
await plugin["permission.asked"]({ input: {}, output });
assert.ok(!("action" in output));
fs.rmSync(dir2, { recursive: true, force: true });
});
it("permission.asked does not set action when no zooplankton.json", async () => {
const plugin = await ZooplanktonCodingPlugin({ directory: tmpDir });
const output = {};
await plugin["permission.asked"]({ input: {}, output });
assert.ok(!("action" in output));
});
it("permission.asked denies doom_loop even when autonomousMode is true", async () => {
const dir2 = makeTmpDir();
writeFixture(dir2, "zooplankton.json", '{"autonomousMode": true}');
const plugin = await ZooplanktonCodingPlugin({ directory: dir2 });
const output = {};
await plugin["permission.asked"]({ input: { metadata: { type: "doom_loop" } }, output });
assert.equal(output.action, "deny");
fs.rmSync(dir2, { recursive: true, force: true });
});
it("permission.asked denies doom_loop when autonomousMode is false", async () => {
const plugin = await ZooplanktonCodingPlugin({ directory: tmpDir });
const output = {};
await plugin["permission.asked"]({ input: { metadata: { type: "doom_loop" } }, output });
assert.equal(output.action, "deny");
});
it("exposes tool.execute.before hook", async () => {
const plugin = await ZooplanktonCodingPlugin({ directory: tmpDir });
assert.ok(typeof plugin["tool.execute.before"] === "function");
});
it("tool.execute.before blocks ~/.ssh/ files", async () => {
const plugin = await ZooplanktonCodingPlugin({ directory: tmpDir });
const sshKey = path.join(os.homedir(), ".ssh", "id_rsa");
await assert.rejects(
() => plugin["tool.execute.before"]({ tool: "read" }, { args: { filePath: sshKey } }),
/Blocked access to sensitive file/,
);
});
it("tool.execute.before blocks ~/.ssh directory itself", async () => {
const plugin = await ZooplanktonCodingPlugin({ directory: tmpDir });
const sshDir = path.join(os.homedir(), ".ssh");
await assert.rejects(
() => plugin["tool.execute.before"]({ tool: "read" }, { args: { filePath: sshDir } }),
/Blocked access to sensitive file/,
);
});
it("tool.execute.before blocks ~/.npmrc", async () => {
const plugin = await ZooplanktonCodingPlugin({ directory: tmpDir });
const npmrc = path.join(os.homedir(), ".npmrc");
await assert.rejects(
() => plugin["tool.execute.before"]({ tool: "read" }, { args: { filePath: npmrc } }),
/Blocked access to sensitive file/,
);
});
it("tool.execute.before blocks ~/.netrc", async () => {
const plugin = await ZooplanktonCodingPlugin({ directory: tmpDir });
const netrc = path.join(os.homedir(), ".netrc");
await assert.rejects(
() => plugin["tool.execute.before"]({ tool: "read" }, { args: { filePath: netrc } }),
/Blocked access to sensitive file/,
);
});
it("tool.execute.before allows project-level .npmrc (not in home dir)", async () => {
const plugin = await ZooplanktonCodingPlugin({ directory: tmpDir });
// Should not throw — project .npmrc is not a credential file
await plugin["tool.execute.before"]({ tool: "read" }, { args: { filePath: path.join(tmpDir, ".npmrc") } });
});
it("tool.execute.before allows normal files", async () => {
const plugin = await ZooplanktonCodingPlugin({ directory: tmpDir });
// Should not throw
await plugin["tool.execute.before"]({ tool: "read" }, { args: { filePath: "/tmp/some-file.txt" } });
});
it("tool.execute.before is a no-op when filePath is absent", async () => {
const plugin = await ZooplanktonCodingPlugin({ directory: tmpDir });
// Should not throw
await plugin["tool.execute.before"]({ tool: "bash" }, { args: {} });
});
it("exposes experimental.session.compacting hook", async () => {
const plugin = await ZooplanktonCodingPlugin({ directory: tmpDir });
assert.ok(typeof plugin["experimental.session.compacting"] === "function");
});
it("experimental.session.compacting is a no-op when no zooplankton.json", async () => {
const plugin = await ZooplanktonCodingPlugin({ directory: tmpDir });
const output = { context: [] };
await plugin["experimental.session.compacting"]({}, output);
assert.equal(output.context.length, 0);
});
it("experimental.session.compacting injects project config into context", async () => {
const dir2 = makeTmpDir();
writeFixture(dir2, "zooplankton.json", JSON.stringify({
project: { platform: "github", repo: "Org/repo" },
autonomousMode: false,
}));
const plugin = await ZooplanktonCodingPlugin({ directory: dir2 });
const output = { context: [] };
await plugin["experimental.session.compacting"]({}, output);
assert.equal(output.context.length, 1);
assert.ok(output.context[0].includes("platform: github"));
assert.ok(output.context[0].includes("repo: Org/repo"));
assert.ok(output.context[0].includes("autonomousMode: false"));
fs.rmSync(dir2, { recursive: true, force: true });
});
it("experimental.session.compacting omits absent fields", async () => {
const dir2 = makeTmpDir();
writeFixture(dir2, "zooplankton.json", JSON.stringify({ autonomousMode: true }));
const plugin = await ZooplanktonCodingPlugin({ directory: dir2 });
const output = { context: [] };
await plugin["experimental.session.compacting"]({}, output);
assert.equal(output.context.length, 1);
assert.ok(!output.context[0].includes("platform"));
assert.ok(!output.context[0].includes("repo:"));
assert.ok(output.context[0].includes("autonomousMode: true"));
fs.rmSync(dir2, { recursive: true, force: true });
});
it("exposes event hook", async () => {
const plugin = await ZooplanktonCodingPlugin({ directory: tmpDir });
assert.ok(typeof plugin.event === "function");
});
it("event hook is a no-op for non-idle events", async () => {
const toastCalls = [];
const mockClient = { tui: { showToast: async (opts) => { toastCalls.push(opts); return true; } } };
const dir2 = makeTmpDir();
writeFixture(dir2, "zooplankton.json", JSON.stringify({ notifications: true }));
const plugin = await ZooplanktonCodingPlugin({ directory: dir2, client: mockClient });
await plugin.event({ event: { type: "session.created" } });
assert.equal(toastCalls.length, 0);
fs.rmSync(dir2, { recursive: true, force: true });
});
it("event hook is a no-op when notifications is false", async () => {
const toastCalls = [];
const mockClient = { tui: { showToast: async (opts) => { toastCalls.push(opts); return true; } } };
const dir2 = makeTmpDir();
writeFixture(dir2, "zooplankton.json", JSON.stringify({ notifications: false }));
const plugin = await ZooplanktonCodingPlugin({ directory: dir2, client: mockClient });
await plugin.event({ event: { type: "session.idle" } });
assert.equal(toastCalls.length, 0);
fs.rmSync(dir2, { recursive: true, force: true });
});
it("event hook shows toast on session.idle when notifications: true", async () => {
const toastCalls = [];
const mockClient = { tui: { showToast: async (opts) => { toastCalls.push(opts); return true; } } };
const dir2 = makeTmpDir();
writeFixture(dir2, "zooplankton.json", JSON.stringify({ notifications: true }));
const plugin = await ZooplanktonCodingPlugin({ directory: dir2, client: mockClient });
await plugin.event({ event: { type: "session.idle" } });
assert.equal(toastCalls.length, 1);
assert.equal(toastCalls[0].body.variant, "info");
fs.rmSync(dir2, { recursive: true, force: true });
});
it("event hook is a no-op when client is absent", async () => {
const dir2 = makeTmpDir();
writeFixture(dir2, "zooplankton.json", JSON.stringify({ notifications: true }));
const plugin = await ZooplanktonCodingPlugin({ directory: dir2 });
// Should not throw
await plugin.event({ event: { type: "session.idle" } });
fs.rmSync(dir2, { recursive: true, force: true });
});
it("event hook silently ignores toast errors", async () => {
const mockClient = { tui: { showToast: async () => { throw new Error("TUI not running"); } } };
const dir2 = makeTmpDir();
writeFixture(dir2, "zooplankton.json", JSON.stringify({ notifications: true }));
const plugin = await ZooplanktonCodingPlugin({ directory: dir2, client: mockClient });
// Should not throw even when showToast fails
await plugin.event({ event: { type: "session.idle" } });
fs.rmSync(dir2, { recursive: true, force: true });
});
});
// SUITE: parseReviewerResult (Reviewer Result Protocol v1)
// The parser is the orchestrator's trust boundary for reviewer task returns.
// Semantics follow plan "Structured Reviewer Result Protocol" (PR-1 scope):
// malformed → fail-closed (ok:false, findings:[], visible errors);
// unknown schema_version → empty findings + warning;
// legacy `scope` field aliased to `anchor_scope` with deprecation warning;
// review_kind absent → defaults to "code" for back-compat.
/**
* Unit tests for .opencode/plugins/opencode-plugin-zooplankton.js
* (ZooplanktonPlugin back-compat export + _createPlugin: experimental.chat.system.transform hook).
*
* Uses Node.js built-in test runner (node:test + node:assert). Zero new dependencies.
*/
import { describe, it, beforeEach } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { ZooplanktonPlugin, _createPlugin } from "../.opencode/plugins/opencode-plugin-zooplankton.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, "..");
const expectedPath = path.join(repoRoot, "instructions", "coding-standards.md");
const codingStandardsContent = fs.readFileSync(expectedPath, "utf8").trim();
describe("ZooplanktonPlugin", () => {
let plugin;
beforeEach(async () => {
plugin = await ZooplanktonPlugin();
});
it("exports experimental.chat.system.transform as a function (no config hook)", async () => {
assert.equal(typeof plugin["experimental.chat.system.transform"], "function");
assert.equal(plugin.config, undefined, "config hook must not exist to avoid OpenCode crash");
});
describe("experimental.chat.system.transform hook", () => {
const hookName = "experimental.chat.system.transform";
it("injects coding standards into empty system array", async () => {
const output = { system: [] };
await plugin[hookName]({}, output);
assert.equal(output.system.length, 1);
assert.equal(output.system[0], codingStandardsContent);
});
it("appends to existing system entries", async () => {
const output = { system: ["You are a helpful assistant."] };
await plugin[hookName]({}, output);
assert.equal(output.system.length, 2);
assert.equal(output.system[0], "You are a helpful assistant.");
assert.equal(output.system[1], codingStandardsContent);
});
it("skips injection when content is already present", async () => {
const output = {
system: ["Instructions from: ...\n" + codingStandardsContent],
};
await plugin[hookName]({}, output);
assert.equal(output.system.length, 1);
});
it("skips injection when content is embedded in a longer entry", async () => {
const output = {
system: [
"Some preamble\n\n" + codingStandardsContent + "\n\nMore text",
],
};
await plugin[hookName]({}, output);
assert.equal(output.system.length, 1);
});
it("works with subagent-like input containing sessionID", async () => {
const input = { sessionID: "ses_abc123", model: { id: "test-model" } };
const output = { system: ["Subagent prompt content."] };
await plugin[hookName](input, output);
assert.equal(output.system.length, 2);
assert.equal(output.system[1], codingStandardsContent);
});
it("does not duplicate on repeated calls", async () => {
const output = { system: [] };
await plugin[hookName]({}, output);
await plugin[hookName]({}, output);
// Second call should detect content is already present and skip
assert.equal(output.system.length, 1);
});
it("handles null output without throwing", async () => {
await assert.doesNotReject(() => plugin[hookName]({}, null));
});
it("handles output with missing system array without throwing", async () => {
await assert.doesNotReject(() => plugin[hookName]({}, {}));
await assert.doesNotReject(() => plugin[hookName]({}, { system: "str" }));
});
it("handles non-string elements in system array without throwing", async () => {
const output = { system: [42, null, "__UNRELATED_MARKER__"] };
await assert.doesNotReject(() => plugin[hookName]({}, output));
// Should still append since no string element contains the coding standards
assert.equal(output.system.length, 4);
assert.equal(output.system[3], codingStandardsContent);
});
it("deduplicates correctly with CRLF line endings in system entries", async () => {
const crlfContent = codingStandardsContent.replace(/\n/g, "\r\n");
const output = { system: [crlfContent] };
await plugin[hookName]({}, output);
// Should detect as duplicate despite CRLF vs LF difference
assert.equal(output.system.length, 1);
});
});
describe("coding-standards.md content", () => {
it("contains skeleton-then-replace rule", () => {
assert.ok(codingStandardsContent.includes("Skeleton-then-Replace"));
});
it("contains prefer-edit rule", () => {
assert.ok(codingStandardsContent.includes("Prefer Edit Over Write"));
});
it("contains prefer-master rule", () => {
assert.ok(
codingStandardsContent.includes("Prefer `master` Over `main`"),
);
});
it("contains context-handoff rule (Rule 4)", () => {
assert.ok(
codingStandardsContent.includes("Context Handoff via Temp Files"),
"Rule 4 heading must be present",
);
assert.ok(
codingStandardsContent.includes("zooplankton-handoff-"),
"Rule 4 must document the temp file naming convention",
);
});
it("contains web-research-tools rule (Rule 5)", () => {
assert.ok(
codingStandardsContent.includes("Web Research Tools"),
"Rule 5 heading must be present",
);
assert.ok(
codingStandardsContent.includes("context7"),
"Rule 5 must mention Context7",
);
assert.ok(
codingStandardsContent.includes("gh_grep"),
"Rule 5 must mention gh_grep",
);
});
});
});
describe("_createPlugin with empty content (file-missing degradation)", () => {
const hookName = "experimental.chat.system.transform";
let emptyPlugin;
beforeEach(() => {
emptyPlugin = _createPlugin("");
});
it("transform hook does not inject when content is empty", async () => {
const output = { system: ["existing"] };
await emptyPlugin[hookName]({}, output);
assert.equal(output.system.length, 1);
assert.equal(output.system[0], "existing");
});
it("transform hook does not inject into empty system array when content is empty", async () => {
const output = { system: [] };
await emptyPlugin[hookName]({}, output);
assert.equal(output.system.length, 0);
});
});
+110
-12

@@ -120,19 +120,117 @@ /**

export const renderConsolidatedReviewBody = ({ title, marker, findings, supersedes = null }) => {
const lines = [marker, "", `### ${sanitizeInlineMarkdown(title)}`];
const grouped = new Map();
for (const finding of Array.isArray(findings) ? findings : []) {
const key = finding.reviewer_model || finding.reviewer_name || finding.sources?.[0]?.reviewer_model || "reviewer";
if (!grouped.has(key)) grouped.set(key, []);
grouped.get(key).push(finding);
// Compute the "why it matters" sentence deterministically from blocking findings.
// Dedup by (area || path), preserve first-occurrence order. Return null if empty.
const computeWhyItMatters = (findings) => {
const seen = new Set();
const ordered = [];
for (const finding of findings) {
if (finding?.severity !== "blocking") continue;
const key = finding.area || finding.path || null;
if (!key) continue;
const sanitized = sanitizeInlineMarkdown(key);
if (!sanitized || seen.has(sanitized)) continue;
seen.add(sanitized);
ordered.push(sanitized);
}
for (const [reviewer, items] of grouped.entries()) {
lines.push("", `**${sanitizeInlineMarkdown(reviewer)}:**`);
items.forEach((finding, index) => {
lines.push(`${index + 1}. [${sanitizeInlineMarkdown(finding.cluster_id)}] [${sanitizeInlineMarkdown(finding.severity)}] ${sanitizeInlineMarkdown(finding.summary)} - ${sanitizeInlineMarkdown(formatFindingLocation(finding))}`);
});
if (ordered.length === 0) return null;
return `Blocking issues span: ${ordered.join(", ")}. See inline comments for details.`;
};
const renderFindingLine = (finding) =>
`- [${sanitizeInlineMarkdown(finding?.cluster_id)}] [${sanitizeInlineMarkdown(finding?.severity)}] ${sanitizeInlineMarkdown(finding?.summary)} — ${sanitizeInlineMarkdown(formatFindingLocation(finding))}`;
const renderFindingBlock = (finding) => {
const head = renderFindingLine(finding);
if (!finding?.detail) return head;
return `${head}\n\n${sanitizeBlockMarkdown(finding.detail)}`;
};
/**
* Render the consolidated review `body` per the spec in
* skills/orchestrate/references/review-protocol.md (§ Body structure).
*
* Signature: positional `findings, reviewers` plus an options object.
* - `findings` — the consolidated cluster list (already deduped + cluster-IDed).
* - `reviewers` — array of `{ name, model }` objects used for the attribution line.
* - options:
* - `marker` — required marker string (from `createReviewMarker`).
* - `supersedes` — optional fingerprint of a superseded prior post.
* - `includeLineAnchored` — Tier 3 fallback flag. When `true`, line-anchored
* findings are appended to the body in full prose.
* Default `false` produces the compact body.
*
* Breaking change vs. the previous `({ title, marker, findings, supersedes })`
* shape: `title` is removed (the marker + summary header replace it) and the
* positional findings/reviewers args are now required.
*/
export const renderConsolidatedReviewBody = (findings, reviewers, { marker, supersedes = null, includeLineAnchored = false } = {}) => {
if (typeof marker !== "string" || marker.length === 0) {
throw new TypeError(
"renderConsolidatedReviewBody: `marker` is required and must be a non-empty string. " +
"Pass the value returned by createReviewMarker({ pr, round, fingerprint }) via options.marker."
);
}
// Validate marker format to catch programmer errors that would silently break
// idempotency (hasReviewMarker matches by exact line content). Accepts the two
// shapes emitted by createReviewMarker: code review and security review.
if (!/^<!-- zooplankton-orchestrate: pr=\d+ (?:round=\d+|phase=security) fingerprint=[0-9a-f]{64} -->$/.test(marker)) {
throw new TypeError(
"renderConsolidatedReviewBody: `marker` does not match the expected format produced by " +
"createReviewMarker(). Construct it via createReviewMarker({ pr, round, fingerprint }) " +
"instead of building it by hand."
);
}
const list = Array.isArray(findings) ? findings : [];
const reviewerList = Array.isArray(reviewers) ? reviewers : [];
const blocking = list.filter((f) => f?.severity === "blocking");
const advisory = list.filter((f) => f?.severity === "advisory");
const fileBranch = list.filter((f) => f?.anchor_scope === "file" || f?.anchor_scope === "branch");
const lineAnchored = list.filter((f) => f?.anchor_scope === "line");
const lines = [marker, ""];
lines.push(`Total findings: ${list.length}`);
if (blocking.length > 0) lines.push(`- blocking: ${blocking.length}`);
if (advisory.length > 0) lines.push(`- advisory: ${advisory.length}`);
const why = computeWhyItMatters(list);
if (why) {
lines.push("", why);
}
if (reviewerList.length > 0) {
const attribution = reviewerList
.map((r) => {
const name = sanitizeInlineMarkdown(r?.name ?? "reviewer");
const model = r?.model ? ` (${sanitizeInlineMarkdown(r.model)})` : "";
return `${name}${model}`;
})
.join(", ");
lines.push("", `Reviewers: ${attribution}`);
}
if (list.length > 0) {
lines.push("", "See inline comments below for line-by-line details. File-/branch-scope findings are listed in the next section.");
}
if (fileBranch.length > 0) {
lines.push("", "### File-/branch-scope findings", "");
for (const finding of fileBranch) lines.push(renderFindingLine(finding));
}
if (includeLineAnchored && lineAnchored.length > 0) {
lines.push("", "### Line-anchored findings (body-only fallback)", "");
for (const finding of lineAnchored) {
lines.push(renderFindingBlock(finding));
lines.push("");
}
// Trim trailing blank from the last block.
if (lines[lines.length - 1] === "") lines.pop();
}
if (supersedes) {
lines.push("", `Supersedes earlier review (fingerprint ${sanitizeInlineMarkdown(supersedes).slice(0, 8)}); see comment timestamps for ordering.`);
}
return lines.join("\n");

@@ -139,0 +237,0 @@ };

+2
-3
{
"name": "opencode-plugin-zooplankton",
"version": "0.6.2",
"version": "0.6.3",
"description": "Unified global OpenCode plugin for ZooplanktonAI — coding standards, multi-agent skills (brainstorm / plan / orchestrate), commands, guides, and the autonomous-mode permission hook.",

@@ -8,3 +8,3 @@ "type": "module",

"scripts": {
"test": "node --test 'test/**/*.test.js' 'tests/**/*.test.js'"
"test": "node --test 'tests/**/*.test.js'"
},

@@ -22,3 +22,2 @@ "license": "MIT",

"templates/",
"test/",
"tests/"

@@ -25,0 +24,0 @@ ],

@@ -111,3 +111,3 @@ # Review Protocol Reference

"event": "COMMENT",
"body": "<marker + per-reviewer prose headers + file/branch-scope findings>",
"body": "<marker + high-level summary header + file/branch-scope findings>",
"comments": [

@@ -120,7 +120,46 @@ { "path": "src/foo.ts", "position": 17, "body": "[R2-C03] [blocking] …" }

- Each `anchor_scope: "line"` finding becomes one entry in `comments[]`. **The `POST /pulls/{pull_number}/reviews` endpoint requires `comments[].position` (the diff position offset), not the file's `line`/`side`.** To resolve a file path + line number to a `position`, fetch the PR's unified diff with `gh pr diff $PR_NUMBER` and count: `position` is the count of lines from the file's first hunk header (`@@ ... @@`) through the target line in that diff, where the line **immediately below** the `@@` line is `position` 1, the next is `position` 2, and so on. Counting includes context, addition (`+`), and deletion (`-`) lines, and continues across additional hunks within the same file (the `@@` separator lines themselves are also counted). When the same line appears across re-hunks (e.g. on a force-push), recompute against the current diff before posting.
- Each `anchor_scope: "file"` and `anchor_scope: "branch"` finding is rendered into the review `body` only — GitHub has no per-file anchor for review comments outside hunks, so file/branch-scope findings live in the summary section.
- The summary `body` always begins with the zooplankton marker followed by per-reviewer prose headers (`**[Round N] <model-id>:**`).
- Each `anchor_scope: "file"` and `anchor_scope: "branch"` finding is rendered into the review `body` only — GitHub has no per-file anchor for review comments outside hunks, so file/branch-scope findings live in the body's "File-/branch-scope findings" section.
> **Note on `line`/`side`:** The separate per-comment endpoint `POST /repos/{owner}/{repo}/pulls/{pull_number}/comments` (used to create a single review comment outside a review object) accepts `line` + `side` directly and does not require diff-position computation. The consolidated review endpoint we use (`POST /pulls/{pull_number}/reviews`) does **not** — its `comments[]` payloads must use `position`.
### Body structure (high-level summary)
The review `body` is a high-level executive summary — it tells the PR author *at a glance* what the review found and points them at the inline comments for detail. It does **not** repeat per-finding prose for line-anchored findings. The body is composed of four sections, in order:
1. **Zooplankton marker** — `<!-- zooplankton-orchestrate: pr=<n> round=<n> fingerprint=<sha> -->`. Always first, on its own line.
2. **Summary header** — derived deterministically (no LLM) from the consolidated cluster list:
- **Total finding count** — `Total findings: <n>`.
- **Severity breakdown** — one row per non-zero severity. Only the two valid values `blocking` and `advisory` (the reviewer schema defines no others). Omit a row whose count is zero.
- **Why it matters** — a single fixed-template sentence generated from the **blocking** findings only:
- Collect the unique `area` values from all `severity === "blocking"` findings, preserving first-occurrence order. Fall back to the finding's `path` when `area` is absent; if both are absent, skip that finding for this computation only (it still counts toward the severity total).
- If the resulting list is non-empty: emit `Blocking issues span: <areas>. See inline comments for details.` (`<areas>` is the comma-separated list, no Oxford comma, no trailing period inside the list).
- If the list is empty (zero blocking findings): omit this sentence entirely. Do **not** emit a placeholder like "No blocking issues" — the severity-count line already conveys this.
- **Reviewer attribution line** — `Reviewers: <name> (<model-id>), <name> (<model-id>), …`. Names + model IDs only, no per-reviewer prose.
- **Pointer line** — `See inline comments below for line-by-line details. File-/branch-scope findings are listed in the next section.`
3. **File-/branch-scope findings section** — every `anchor_scope: "file"` and `anchor_scope: "branch"` finding rendered in full, each prefixed with its cluster ID and severity (`[<cluster_id>] [<severity>] <summary> — <path-or-branch-scope>`). Required because GitHub has no per-file anchor outside hunks.
4. **Line-anchored findings (Tier 3 only)** — when the orchestrator falls back to a body-only review (Tier 3 retry, see below), every `anchor_scope: "line"` finding is appended here in full prose so nothing is lost. In the default (non-fallback) body, this section is omitted.
The per-reviewer prose dump (`**[Round N] <model-id>:** …` followed by per-finding prose) used in the previous spec is **removed** — the attribution line in §2 replaces the per-reviewer headers, and the per-finding prose lives only in `comments[]` for line-anchored findings or in §3 for file/branch-scope findings.
Example body (default, non-fallback):
```markdown
<!-- zooplankton-orchestrate: pr=123 round=2 fingerprint=abc... -->
Total findings: 5
- blocking: 2
- advisory: 3
Blocking issues span: auth, billing. See inline comments for details.
Reviewers: core-reviewer (claude-sonnet-4.6), reviewer (glm-4.6)
See inline comments below for line-by-line details. File-/branch-scope findings are listed in the next section.
### File-/branch-scope findings
- [R2-C04] [advisory] Update README to mention new flag — README.md
- [R2-C05] [advisory] Tag release after merge — branch-scope
```
Example invocation (single-quoted heredoc to prevent shell expansion):

@@ -133,3 +172,3 @@

"event": "COMMENT",
"body": "<!-- zooplankton-orchestrate: pr=123 round=2 fingerprint=abc -->\n\n**[Round 2] core-reviewer (claude-sonnet-4.6):** …",
"body": "<!-- zooplankton-orchestrate: pr=123 round=2 fingerprint=abc -->\n\nTotal findings: 5\n- blocking: 2\n- advisory: 3\n\nBlocking issues span: auth, billing. See inline comments for details.\n\nReviewers: core-reviewer (claude-sonnet-4.6), reviewer (glm-4.6)\n\nSee inline comments below for line-by-line details. File-/branch-scope findings are listed in the next section.\n\n### File-/branch-scope findings\n\n- [R2-C04] [advisory] Update README to mention new flag — README.md",
"comments": [

@@ -156,3 +195,3 @@ { "path": "src/foo.ts", "position": 17, "body": "[R2-C03] [blocking] Null deref when `config.repo` is absent." }

1. Build the body with `renderConsolidatedReviewBody(...)`.
1. Build the body with `renderConsolidatedReviewBody(findings, reviewers, { marker })` (default `{ includeLineAnchored: false }`). `marker` is the string returned by `createReviewMarker({ pr, round, fingerprint })` and is required — the renderer throws `TypeError` if it is missing or empty.
2. Build endpoint-neutral line anchors with `buildInlineCommentAnchors(findings)`. Each anchor has shape `{ path, position: null, _line, body }` — `position` is `null` as a placeholder; `_line` is the file line number retained as a resolution hint. Do NOT forward `_line` to GitHub.

@@ -162,3 +201,3 @@ 3. Before calling GitHub, resolve each anchor's `position`. `POST /pulls/{pull_number}/reviews` review comments require `path` + `position` (with `commit_id` at the top level) — `position` is the diff-position integer described above, not the file line number. Use `_line` to find the target line in the unified diff, then count positions from the hunk header. The legacy `POST /pulls/{pull_number}/comments` per-comment endpoint (which does accept `path` + `line` + `side`) is **not** used by the consolidated post.

5. Tier 2: deterministically halve inline comments and move the rest into the body with `planReviewPostRetry(comments, 2)`.
6. Tier 3: body-only review (omit `comments[]` entirely, render every finding in `body`) with `planReviewPostRetry(comments, 3)`.
6. Tier 3: body-only review (omit `comments[]` entirely) with `planReviewPostRetry(comments, 3)`. The orchestrator MUST re-render the body via `renderConsolidatedReviewBody(findings, reviewers, { marker, includeLineAnchored: true })` before re-posting so line-anchored findings are appended to the body in full prose. `planReviewPostRetry` itself does not call the renderer — it only computes the retry tier; the orchestrator inspects the planned tier and chooses the renderer flag.

@@ -165,0 +204,0 @@ If all tiers fail, leave the local state marker as `IN_PROGRESS` and abort before evaluate/merge.

@@ -120,6 +120,17 @@ ---

Invoke **all reviewers in a single parallel message** (one Task call per reviewer). Read `agents.coreReviewers` and `agents.reviewers` from `zooplankton.json` (or discover from MD files). Each entry is a `{ name }` object — use `.name` for `@<name>`. Skip the role if `null`; skip individual entries with `"disabled": true`.
Invoke **all reviewers in a single parallel message** (one Task call per reviewer).
- **Core reviewers** (blocking quorum): for each entry in `agents.coreReviewers`, invoke `@<entry.name>` and assign worktree `.worktrees/<branch>/<name>`.
- **Normal reviewers** (non-blocking, opportunistic): for each `agents.reviewers` entry, invoke `@<entry.name>` and assign 1–2 review areas via adaptive round-robin.
**Agent discovery (run fresh every round — do NOT rely on memory or prior-round lists):**
1. Read `agents.coreReviewers`, `agents.reviewers`, and `agents.frontendReviewers` from `zooplankton.json` if the `agents` block is present.
2. If any role key is absent from `zooplankton.json` (or the `agents` block is missing entirely), discover agents by globbing agent MD files:
- Core reviewers: `~/.config/opencode/agents/core-reviewer-*.md` and `.opencode/agents/core-reviewer-*.md`
- Normal reviewers: `~/.config/opencode/agents/reviewer-*.md` and `.opencode/agents/reviewer-*.md`
- Frontend reviewers: `~/.config/opencode/agents/frontend-reviewer-*.md` and `.opencode/agents/frontend-reviewer-*.md`
3. **De-duplicate** across global and project paths by filename stem (e.g. `reviewer-glm` from either location counts once).
4. **Always enumerate all discovered agents** — never omit an agent because it returned empty in a prior round. Empty results in a prior round are not a reason to skip in subsequent rounds.
5. Each entry is a `{ name }` object — use `.name` for `@<name>`. Skip the role if `null`; skip individual entries with `"disabled": true`.
- **Core reviewers** (blocking quorum): for each entry in `agents.coreReviewers` (or discovered `core-reviewer-*`), invoke `@<entry.name>` and assign worktree `.worktrees/<branch>/<name>`.
- **Normal reviewers** (non-blocking, opportunistic): for each `agents.reviewers` entry (or discovered `reviewer-*`), invoke `@<entry.name>` and assign 1–2 review areas via adaptive round-robin.
- **Frontend reviewers** (blocking when frontend files are touched): if `agents.frontendReviewers` is not null **and** the change touches frontend files, invoke each non-disabled entry. They follow `guides/frontend-reviewer-guide.md` and join the quorum.

@@ -126,0 +137,0 @@

import { describe, it, beforeEach } from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { ZooplanktonPlugin, _createPlugin } from "../.opencode/plugins/opencode-plugin-zooplankton.js";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, "..");
const expectedPath = path.join(repoRoot, "instructions", "coding-standards.md");
const codingStandardsContent = fs.readFileSync(expectedPath, "utf8").trim();
describe("ZooplanktonPlugin", () => {
let plugin;
beforeEach(async () => {
plugin = await ZooplanktonPlugin();
});
it("exports experimental.chat.system.transform as a function (no config hook)", async () => {
assert.equal(typeof plugin["experimental.chat.system.transform"], "function");
assert.equal(plugin.config, undefined, "config hook must not exist to avoid OpenCode crash");
});
describe("experimental.chat.system.transform hook", () => {
const hookName = "experimental.chat.system.transform";
it("injects coding standards into empty system array", async () => {
const output = { system: [] };
await plugin[hookName]({}, output);
assert.equal(output.system.length, 1);
assert.equal(output.system[0], codingStandardsContent);
});
it("appends to existing system entries", async () => {
const output = { system: ["You are a helpful assistant."] };
await plugin[hookName]({}, output);
assert.equal(output.system.length, 2);
assert.equal(output.system[0], "You are a helpful assistant.");
assert.equal(output.system[1], codingStandardsContent);
});
it("skips injection when content is already present", async () => {
const output = {
system: ["Instructions from: ...\n" + codingStandardsContent],
};
await plugin[hookName]({}, output);
assert.equal(output.system.length, 1);
});
it("skips injection when content is embedded in a longer entry", async () => {
const output = {
system: [
"Some preamble\n\n" + codingStandardsContent + "\n\nMore text",
],
};
await plugin[hookName]({}, output);
assert.equal(output.system.length, 1);
});
it("works with subagent-like input containing sessionID", async () => {
const input = { sessionID: "ses_abc123", model: { id: "test-model" } };
const output = { system: ["Subagent prompt content."] };
await plugin[hookName](input, output);
assert.equal(output.system.length, 2);
assert.equal(output.system[1], codingStandardsContent);
});
it("does not duplicate on repeated calls", async () => {
const output = { system: [] };
await plugin[hookName]({}, output);
await plugin[hookName]({}, output);
// Second call should detect content is already present and skip
assert.equal(output.system.length, 1);
});
it("handles null output without throwing", async () => {
await assert.doesNotReject(() => plugin[hookName]({}, null));
});
it("handles output with missing system array without throwing", async () => {
await assert.doesNotReject(() => plugin[hookName]({}, {}));
await assert.doesNotReject(() => plugin[hookName]({}, { system: "str" }));
});
it("handles non-string elements in system array without throwing", async () => {
const output = { system: [42, null, "__UNRELATED_MARKER__"] };
await assert.doesNotReject(() => plugin[hookName]({}, output));
// Should still append since no string element contains the coding standards
assert.equal(output.system.length, 4);
assert.equal(output.system[3], codingStandardsContent);
});
it("deduplicates correctly with CRLF line endings in system entries", async () => {
const crlfContent = codingStandardsContent.replace(/\n/g, "\r\n");
const output = { system: [crlfContent] };
await plugin[hookName]({}, output);
// Should detect as duplicate despite CRLF vs LF difference
assert.equal(output.system.length, 1);
});
});
describe("coding-standards.md content", () => {
it("contains skeleton-then-replace rule", () => {
assert.ok(codingStandardsContent.includes("Skeleton-then-Replace"));
});
it("contains prefer-edit rule", () => {
assert.ok(codingStandardsContent.includes("Prefer Edit Over Write"));
});
it("contains prefer-master rule", () => {
assert.ok(
codingStandardsContent.includes("Prefer `master` Over `main`"),
);
});
it("contains context-handoff rule (Rule 4)", () => {
assert.ok(
codingStandardsContent.includes("Context Handoff via Temp Files"),
"Rule 4 heading must be present",
);
assert.ok(
codingStandardsContent.includes("zooplankton-handoff-"),
"Rule 4 must document the temp file naming convention",
);
});
it("contains web-research-tools rule (Rule 5)", () => {
assert.ok(
codingStandardsContent.includes("Web Research Tools"),
"Rule 5 heading must be present",
);
assert.ok(
codingStandardsContent.includes("context7"),
"Rule 5 must mention Context7",
);
assert.ok(
codingStandardsContent.includes("gh_grep"),
"Rule 5 must mention gh_grep",
);
});
});
});
describe("_createPlugin with empty content (file-missing degradation)", () => {
const hookName = "experimental.chat.system.transform";
let emptyPlugin;
beforeEach(() => {
emptyPlugin = _createPlugin("");
});
it("transform hook does not inject when content is empty", async () => {
const output = { system: ["existing"] };
await emptyPlugin[hookName]({}, output);
assert.equal(output.system.length, 1);
assert.equal(output.system[0], "existing");
});
it("transform hook does not inject into empty system array when content is empty", async () => {
const output = { system: [] };
await emptyPlugin[hookName]({}, output);
assert.equal(output.system.length, 0);
});
});

Sorry, the diff of this file is too big to display