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 */ } } }); });