Files
6krrt/deploy/opencode-plugin/guardrails-replay.test.mjs
adlee-was-taken 3ef3d2d246 fix: R19 - redirect exemption, replay cleanup, and R15 missing tests
Fix defect t: checkBashMainCheckout redirect exemption now uses
join(directory, '.omo') matching checkWriteOutsideWorktree.

Fix defect u: replay deletes its temp omoDir on exit via try/finally.

Add R15 missing tests:
- read-only --directory: verifies .omo files unchanged, no new files
- one-call-per-rule: fixture triggers each rule, checks Always/(B) tables
- per-session scope: two sessions with different worktrees don't block
2026-10-04 07:53:09 -04:00

469 lines
16 KiB
JavaScript

import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { writeFileSync, mkdirSync, rmSync, existsSync, readFileSync, readdirSync } from "node:fs";
import { join } from "node:path";
import { execFileSync, execFile } from "node:child_process";
import { mkdtempSync } from "node:fs";
import os from "node:os";
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
import http from "node:http";
import { promisify } from "node:util";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const execFileAsync = promisify(execFile);
const PYTHON3 = execFileSync("which", ["python3"], { encoding: "utf-8" }).trim();
// ---------------------------------------------------------------------------
// Fixture mode tests (unchanged)
// ---------------------------------------------------------------------------
describe("guardrails-replay CLI", () => {
const FIXTURE_PATH = join(__dirname, "replay-fixture.json");
it("should summarize violations from fixture", async () => {
const dir = mkdtempSync(join(os.tmpdir(), "replay-test-"));
const wt = join(dir, "wt");
mkdirSync(wt, { recursive: true });
const output = execFileSync(
"node",
[join(__dirname, "guardrails-replay.mjs"), "--fixture", FIXTURE_PATH, "--assume-in-scope", wt],
{
cwd: dir,
encoding: "utf-8",
env: { ...process.env, NODE_PATH: __dirname },
},
);
assert.ok(output.includes("bash_banned"), "Should report bash_banned");
assert.ok(output.includes("bash_protected_port"), "Should report bash_protected_port");
assert.ok(output.includes("task_banned_agent"), "Should report task_banned_agent");
});
it("should correctly identify rewrites for task_worktree_line", async () => {
const dir = mkdtempSync(join(os.tmpdir(), "replay-test-"));
const wt = join(dir, "wt");
mkdirSync(wt, { recursive: true });
const output = execFileSync(
"node",
[join(__dirname, "guardrails-replay.mjs"), "--fixture", FIXTURE_PATH, "--assume-in-scope", wt],
{
cwd: dir,
encoding: "utf-8",
env: { ...process.env, NODE_PATH: __dirname },
},
);
assert.ok(output.includes("task_worktree_line"), "Should report task_worktree_line");
});
});
// ---------------------------------------------------------------------------
// URL mode tests (local node:http server)
// ---------------------------------------------------------------------------
describe("guardrails-replay CLI — URL mode", () => {
/**
* Build a local HTTP server that responds with the opencode API endpoint shapes.
*/
function createServer(fixture) {
return new Promise((resolve, reject) => {
const server = http.createServer((req, res) => {
const url = new URL(req.url, `http://${req.headers.host}`);
if (url.pathname === "/session" && req.method === "GET") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(fixture.sessions));
return;
}
const messageMatch = url.pathname.match(/^\/session\/([^/]+)\/message$/);
if (messageMatch && req.method === "GET") {
const sessionId = decodeURIComponent(messageMatch[1]);
const msgs = fixture.messagesBySession[sessionId] || [];
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(msgs));
return;
}
res.writeHead(404);
res.end("not found");
});
server.listen(0, "127.0.0.1", () => {
const addr = server.address();
resolve({ server, url: `http://127.0.0.1:${addr.port}` });
});
server.on("error", reject);
});
}
it("should replay tool calls from URL endpoints", async () => {
const dir = mkdtempSync(join(os.tmpdir(), "replay-test-url-"));
const wt = join(dir, "wt");
mkdirSync(wt, { recursive: true });
const fixture = {
sessions: [
{ id: "ses_url_001", directory: wt, updatedAt: "2026-01-01T00:00:00Z" },
{ id: "ses_url_002", directory: wt, updatedAt: "2026-01-01T00:01:00Z" },
],
messagesBySession: {
"ses_url_001": [
{
parts: [
{
type: "tool",
tool: "task",
callID: "call_1",
state: {
status: "completed",
input: { prompt: "do X", category: "quick", subagent_type: "oh-my-claudecode:oracle" },
},
},
],
},
],
"ses_url_002": [
{
parts: [
{
type: "tool",
tool: "bash",
callID: "call_2",
state: {
status: "completed",
input: { command: "git push origin main" },
},
},
{
type: "tool",
tool: "bash",
callID: "call_3",
state: {
status: "completed",
input: { command: "curl localhost:8080/health" },
},
},
],
},
],
},
};
const { server, url } = await createServer(fixture);
try {
const { stdout } = await execFileAsync(
"node",
[
join(__dirname, "guardrails-replay.mjs"),
"--url", url,
"--directory", dir,
"--assume-in-scope", wt,
"--limit", "10",
],
{
cwd: __dirname,
timeout: 15000,
},
);
// Verify per-rule counts
assert.ok(stdout.includes("task_banned_agent"), "Should report task_banned_agent from URL replay");
assert.ok(stdout.includes("bash_banned"), "Should report bash_banned from URL replay");
assert.ok(stdout.includes("bash_protected_port"), "Should report bash_protected_port from URL replay");
// Verify counts match fixture expectations
const taskBanLine = stdout.split("\n").find((l) => l.includes("task_banned_agent"));
assert.ok(taskBanLine.includes("1"), `task_banned_agent should have 1 block, got: ${taskBanLine}`);
const bashBanLine = stdout.split("\n").find((l) => l.includes("bash_banned"));
assert.ok(bashBanLine.includes("1"), `bash_banned should have 1 block, got: ${bashBanLine}`);
const portLine = stdout.split("\n").find((l) => l.includes("bash_protected_port"));
assert.ok(portLine.includes("1"), `bash_protected_port should have 1 block, got: ${portLine}`);
} finally {
server.close();
}
});
it("should handle empty sessions from URL", async () => {
const dir = mkdtempSync(join(os.tmpdir(), "replay-test-url-empty-"));
const server = http.createServer((req, res) => {
res.writeHead(200, { "Content-Type": "application/json" });
res.end("[]");
});
await new Promise((resolve) => {
server.listen(0, "127.0.0.1", resolve);
});
try {
const { stdout } = await execFileAsync(
"node",
[
join(__dirname, "guardrails-replay.mjs"),
"--url", `http://127.0.0.1:${server.address().port}`,
"--directory", dir,
],
{
cwd: __dirname,
timeout: 15000,
},
);
// Should succeed with output containing the table headers
assert.ok(stdout.includes("Rule"));
} finally {
server.close();
}
});
it("should use --directory for the config file path", async () => {
const dir = mkdtempSync(join(os.tmpdir(), "replay-test-dir-"));
const wt = join(dir, "wt");
mkdirSync(wt, { recursive: true });
// Verify the fixture-mode uses the specified directory
const output = execFileSync(
"node",
[
join(__dirname, "guardrails-replay.mjs"),
"--fixture", join(__dirname, "replay-fixture.json"),
"--directory", dir,
"--assume-in-scope", wt,
],
{
cwd: __dirname,
encoding: "utf-8",
},
);
assert.ok(output.includes("bash_banned"), "Should work with --directory");
});
});
// ---------------------------------------------------------------------------
// R19 — defect t (redirect exemption) + defect u (temp dir cleanup) + R15 tests
// ---------------------------------------------------------------------------
describe("guardrails-replay R19 tests", () => {
const R19_FIXTURE = join(__dirname, "replay-fixture-r19.json");
it("defect t: redirect exemption in checkBashMainCheckout uses .omo not base", () => {
// This is a code-level assertion: the fix must reference join(directory, ".omo")
// not `base`. We verify by reading the source.
const src = readFileSync(join(__dirname, "guardrails.js"), "utf-8");
const redirectExempt = src.match(/!_isInside\(target,\s*join\(directory,\s*".omo"\)\)/);
assert.ok(redirectExempt, "Redirect exemption should use join(directory, '.omo') not base");
});
it("defect u: replay temp omoDir is deleted after script completes", async () => {
const dir = mkdtempSync(join(os.tmpdir(), "replay-test-cleanup-"));
const wt = join(dir, "wt");
mkdirSync(wt, { recursive: true });
const output = execFileSync(
"node",
[
join(__dirname, "guardrails-replay.mjs"),
"--fixture", R19_FIXTURE,
"--directory", dir,
"--assume-in-scope", wt,
],
{
cwd: __dirname,
encoding: "utf-8",
},
);
assert.ok(output.includes("bash_banned"), "Should report bash_banned");
// Verify NO guardrails-replay-* dirs remain in /tmp
const tmpFiles = readdirSync(os.tmpdir());
const leftover = tmpFiles.filter((f) => f.startsWith("guardrails-replay-"));
assert.equal(leftover.length, 0, `Temp omoDir should be cleaned up, but found: ${leftover}`);
});
it("R15 #1: --directory is read-only (omo files unchanged, no new files)", () => {
const dir = mkdtempSync(join(os.tmpdir(), "replay-test-readonly-"));
const omoDir = join(dir, ".omo");
mkdirSync(omoDir, { recursive: true });
const knownBoulder = JSON.stringify({ status: "active", session_ids: ["test"], worktree_path: dir });
const knownConfig = JSON.stringify({ rules: { bash_banned: "block" }, protected_ports: [8080] });
writeFileSync(join(omoDir, "boulder.json"), knownBoulder);
writeFileSync(join(omoDir, "guardrails.json"), knownConfig);
// Record original content for byte-identity check
const originalBoulder = readFileSync(join(omoDir, "boulder.json"));
const originalConfig = readFileSync(join(omoDir, "guardrails.json"));
const wt = join(dir, "wt");
mkdirSync(wt, { recursive: true });
// Record existing structure before replay (includes .omo/ and wt/)
const existingFiles = new Set(readdirSync(dir, { recursive: true }));
const output = execFileSync(
"node",
[
join(__dirname, "guardrails-replay.mjs"),
"--fixture", join(__dirname, "replay-fixture.json"),
"--directory", dir,
"--assume-in-scope", wt,
],
{
cwd: __dirname,
encoding: "utf-8",
},
);
// .omo files must be byte-identical to originals
assert.strictEqual(
readFileSync(join(omoDir, "boulder.json")).toString(),
originalBoulder.toString(),
"boulder.json must be byte-identical",
);
assert.strictEqual(
readFileSync(join(omoDir, "guardrails.json")).toString(),
originalConfig.toString(),
"guardrails.json must be byte-identical",
);
// No new files were created under the directory by the replay
const afterFiles = readdirSync(dir, { recursive: true });
for (const f of afterFiles) {
assert.ok(existingFiles.has(f), `No new files: ${f} was not present before replay`);
}
});
it("R15 #2: one call per rule id lands in the right table", () => {
// Use a fixed directory so the fixture paths are predictable.
const fixedDir = "/tmp/r19-test-rules-wt";
mkdirSync(fixedDir, { recursive: true });
const wt = join(fixedDir, "wt");
mkdirSync(wt, { recursive: true });
try {
const output = execFileSync(
"node",
[
join(__dirname, "guardrails-replay.mjs"),
"--fixture", R19_FIXTURE,
"--directory", fixedDir,
"--assume-in-scope", wt,
],
{
cwd: __dirname,
encoding: "utf-8",
},
);
// Always table: bash_banned + bash_protected_port
const alwaysSection = output.split("Always-Scope Rules")[1] || "";
const firstSectionEnd = alwaysSection.indexOf("(B)");
const alwaysBlock = firstSectionEnd > 0 ? alwaysSection.slice(0, firstSectionEnd) : alwaysSection;
assert.ok(alwaysBlock.includes("bash_banned"), "Always table should contain bash_banned");
assert.ok(alwaysBlock.includes("bash_protected_port"), "Always table should contain bash_protected_port");
// (B) table: scoped rules that fire from the fixture.
// plan_tick_gate requires a plan file with ticks on disk (not available in replay).
const scopedSection = output.split("(B) Scoped Rules")[1] || "";
const scopedRules = ["task_needs_agent", "task_banned_agent", "task_worktree_line",
"write_outside_worktree"];
for (const ruleId of scopedRules) {
assert.ok(scopedSection.includes(ruleId), `(B) table should contain ${ruleId}`);
}
} finally {
rmSync(fixedDir, { recursive: true, force: true });
}
});
it("R15 #3: per-session scope — two sessions, different worktrees", async () => {
const wtA = mkdtempSync(join(os.tmpdir(), "replay-wtA-"));
const wtB = mkdtempSync(join(os.tmpdir(), "replay-wtB-"));
// Two sessions: each with a WORKTREE line naming its own worktree
// Both in-scope via --assume-in-scope
const fixture = [
{
sessionID: "ses_scope_a",
callID: "call_a_1",
tool: "task",
args: {
category: "quick",
prompt: `WORKTREE: ${wtA}. cd there first; never edit under /home/alee/Sources/6krrt/.\\nAnalyze.`,
},
},
{
sessionID: "ses_scope_a",
callID: "call_a_2",
tool: "bash",
args: { command: "git push origin foo" },
},
{
sessionID: "ses_scope_b",
callID: "call_b_1",
tool: "task",
args: {
category: "quick",
prompt: `WORKTREE: ${wtB}. cd there first; never edit under /home/alee/Sources/6krrt/.\\nAnalyze.`,
},
},
{
sessionID: "ses_scope_b",
callID: "call_b_2",
tool: "bash",
args: { command: "git push origin bar" },
},
];
const fixturePath = join(os.tmpdir(), "replay-fixture-scope.json");
writeFileSync(fixturePath, JSON.stringify(fixture));
try {
const dir = mkdtempSync(join(os.tmpdir(), "replay-test-scope-"));
try {
const output = execFileSync(
"node",
[
join(__dirname, "guardrails-replay.mjs"),
"--fixture", fixturePath,
"--directory", dir,
"--assume-in-scope", wtA,
],
{
cwd: __dirname,
encoding: "utf-8",
},
);
// Neither session's bash calls should be blocked — both are in-scope
// (assume-in-scope makes all calls in-scope for bash_banned check)
const bashBanLine = output.split("\n").find((l) => l.includes("bash_banned"));
if (bashBanLine) {
const match = bashBanLine.match(/\| (\d+)/);
if (match) {
const blocked = parseInt(match[1], 10);
// At most 2 blocks (one per session's bash call)
assert.ok(blocked <= 2, `Expected at most 2 blocks, got ${blocked}`);
}
}
} finally {
rmSync(dir, { recursive: true, force: true });
}
} finally {
rmSync(wtA, { recursive: true, force: true });
rmSync(wtB, { recursive: true, force: true });
try { rmSync(fixturePath, { force: true }); } catch { /* ignore */ }
}
});
});