import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, renameSync, rmSync, statSync, symlinkSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { platform } from "node:process"; import { afterEach, describe, expect, it } from "vitest"; import { runSubagentStopHook } from "../src/codex-hook.js"; import type { SubagentStopInput } from "../src/types.js"; const cleanupRoots: string[] = []; afterEach(() => { for (const root of cleanupRoots.splice(0)) rmSync(root, { recursive: true, force: true }); }); describe("lazycodex executor SubagentStop verifier", () => { it("#given no evidence receipt #when lazycodex executor stops #then blocks with a strong directive", () => { // given const cwd = createWorkspace(); // when const output = runSubagentStopHook(createInput(cwd), nodeFileSystem); // then const parsed = parseBlockOutput(output); expect(parsed.decision).toBe("block"); expect(parsed.reason).toContain("너는 방금 작업을 완료했다고 보고했고, 그건 거짓말이다."); expect(parsed.reason).toContain(".omo/evidence/"); expect(parsed.reason).toContain("EVIDENCE_RECORDED: "); expect(parsed.reason).not.toContain("2번째"); }); it("#given a prior blocked stop #when lazycodex executor stops again #then escalates the attempt count", () => { // given const cwd = createWorkspace(); runSubagentStopHook(createInput(cwd), nodeFileSystem); // when const output = runSubagentStopHook(createInput(cwd), nodeFileSystem); // then const parsed = parseBlockOutput(output); expect(parsed.reason).toContain("2번째"); }); it("#given turn_id is omitted #when lazycodex executor stops #then the hook still parses the payload", () => { // given const cwd = createWorkspace(); // when const output = runSubagentStopHook(createInput(cwd), nodeFileSystem); // then expect(parseBlockOutput(output).decision).toBe("block"); }); it("#given an existing non-empty evidence receipt #when lazycodex executor stops #then exits and clears state", () => { // given const cwd = createWorkspace(); runSubagentStopHook(createInput(cwd), nodeFileSystem); const artifactPath = join(cwd, ".omo", "evidence", "receipt.txt"); mkdirSync(join(cwd, ".omo", "evidence"), { recursive: true }); writeFileSync(artifactPath, "verified\n"); // when const output = runSubagentStopHook( createInput(cwd, { last_assistant_message: "done\nEVIDENCE_RECORDED: .omo/evidence/receipt.txt" }), nodeFileSystem, ); // then expect(output).toBe(""); expect(existsSync(join(cwd, ".omo", "lazycodex-executor-verify", "sess.1-agent_1.json"))).toBe(false); }); it("#given a zero-byte evidence receipt #when lazycodex executor stops #then blocks", () => { // given const cwd = createWorkspace(); const artifactPath = join(cwd, ".omo", "evidence", "empty.txt"); mkdirSync(join(cwd, ".omo", "evidence"), { recursive: true }); writeFileSync(artifactPath, ""); // when const output = runSubagentStopHook( createInput(cwd, { last_assistant_message: "done\nEVIDENCE_RECORDED: .omo/evidence/empty.txt" }), nodeFileSystem, ); // then expect(parseBlockOutput(output).decision).toBe("block"); }); it("#given an evidence receipt directory inside evidence root #when lazycodex executor stops #then blocks", () => { // given const cwd = createWorkspace(); mkdirSync(join(cwd, ".omo", "evidence", "receipt-dir"), { recursive: true }); // when const output = runSubagentStopHook( createInput(cwd, { last_assistant_message: "done\nEVIDENCE_RECORDED: .omo/evidence/receipt-dir" }), nodeFileSystem, ); // then expect(parseBlockOutput(output).decision).toBe("block"); }); it("#given an evidence receipt symlink targets outside evidence root #when lazycodex executor stops #then blocks", () => { // given const cwd = createWorkspace(); const artifactPath = join(cwd, ".omo", "evidence", "passwd-link"); mkdirSync(join(cwd, ".omo", "evidence"), { recursive: true }); symlinkSync(existingReceiptTargetOutsideEvidenceRoot(), artifactPath); // when const output = runSubagentStopHook( createInput(cwd, { last_assistant_message: "done\nEVIDENCE_RECORDED: .omo/evidence/passwd-link" }), nodeFileSystem, ); // then expect(parseBlockOutput(output).decision).toBe("block"); }); it("#given an evidence receipt traverses a symlinked evidence subdirectory #when lazycodex executor stops #then blocks", () => { // given const cwd = createWorkspace(); const outsideRoot = createWorkspace(); const outsideReceipt = join(outsideRoot, "receipt.txt"); const linkPath = join(cwd, ".omo", "evidence", "outside-dir"); mkdirSync(join(cwd, ".omo", "evidence"), { recursive: true }); writeFileSync(outsideReceipt, "outside\n"); symlinkSync(outsideRoot, linkPath, platform === "win32" ? "junction" : "dir"); // when const output = runSubagentStopHook( createInput(cwd, { last_assistant_message: "done\nEVIDENCE_RECORDED: .omo/evidence/outside-dir/receipt.txt" }), nodeFileSystem, ); // then expect(parseBlockOutput(output).decision).toBe("block"); }); it("#given an existing absolute receipt outside evidence root #when lazycodex executor stops #then blocks", () => { // given const cwd = createWorkspace(); const receiptPath = existingAbsoluteReceiptOutsideEvidenceRoot(); // when const output = runSubagentStopHook( createInput(cwd, { last_assistant_message: `done\nEVIDENCE_RECORDED: ${receiptPath}` }), nodeFileSystem, ); // then expect(parseBlockOutput(output).decision).toBe("block"); }); it("#given a parent traversal receipt outside cwd #when lazycodex executor stops #then blocks", () => { // given const { cwd } = createWorkspaceWithParentOutsideReceipt(); // when const output = runSubagentStopHook( createInput(cwd, { last_assistant_message: "done\nEVIDENCE_RECORDED: ../outside.txt" }), nodeFileSystem, ); // then expect(parseBlockOutput(output).decision).toBe("block"); }); it("#given a traversal receipt escaping evidence root #when lazycodex executor stops #then blocks", () => { // given const cwd = createWorkspace(); mkdirSync(join(cwd, ".omo"), { recursive: true }); writeFileSync(join(cwd, ".omo", "outside.txt"), "outside\n"); // when const output = runSubagentStopHook( createInput(cwd, { last_assistant_message: "done\nEVIDENCE_RECORDED: .omo/evidence/../outside.txt" }), nodeFileSystem, ); // then expect(parseBlockOutput(output).decision).toBe("block"); }); it("#given three prior attempts #when lazycodex executor stops #then exits and clears stale state", () => { // given const cwd = createWorkspace(); const stateDir = join(cwd, ".omo", "lazycodex-executor-verify"); mkdirSync(stateDir, { recursive: true }); writeFileSync(join(stateDir, "sess.1-agent_1.json"), JSON.stringify({ attempts: 3 })); // when const output = runSubagentStopHook(createInput(cwd), nodeFileSystem); // then expect(output).toBe(""); expect(existsSync(join(stateDir, "sess.1-agent_1.json"))).toBe(false); }); it("#given an unrelated agent #when SubagentStop fires #then exits without output", () => { // given const cwd = createWorkspace(); // when const output = runSubagentStopHook(createInput(cwd, { agent_type: "worker" }), nodeFileSystem); // then expect(output).toBe(""); }); it("#given malformed input and unknown event #when hook runs #then exits without output", () => { // given const cwd = createWorkspace(); // when const malformedOutput = runSubagentStopHook({ hook_event_name: "SubagentStop", session_id: 123 }, nodeFileSystem); const unknownEventOutput = runSubagentStopHook(createUnknownEventInput(cwd), nodeFileSystem); // then expect(malformedOutput).toBe(""); expect(unknownEventOutput).toBe(""); }); it("#given context pressure appears in transcript #when hook runs #then exits without output", () => { // given const cwd = createWorkspace(); const transcriptPath = join(cwd, "transcript.jsonl"); writeFileSync(transcriptPath, "context_length_exceeded\n"); // when const output = runSubagentStopHook(createInput(cwd, { transcript_path: transcriptPath }), nodeFileSystem); // then expect(output).toBe(""); }); }); type BlockOutput = { readonly decision: "block"; readonly reason: string; }; const nodeFileSystem = { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync, }; function createWorkspace(): string { const root = mkdtempSync(join(tmpdir(), "lazycodex-executor-verify-")); cleanupRoots.push(root); return root; } function createWorkspaceWithParentOutsideReceipt(): { readonly cwd: string } { const root = createWorkspace(); const cwd = join(root, "project"); mkdirSync(cwd, { recursive: true }); writeFileSync(join(root, "outside.txt"), "outside\n"); return { cwd }; } function existingAbsoluteReceiptOutsideEvidenceRoot(): string { if (existsSync("/etc/passwd") && statSync("/etc/passwd").size > 0) return "/etc/passwd"; return existingReceiptTargetOutsideEvidenceRoot(); } function existingReceiptTargetOutsideEvidenceRoot(): string { if (existsSync("/etc/passwd") && statSync("/etc/passwd").size > 0) return "/etc/passwd"; const root = createWorkspace(); const receiptPath = join(root, "outside.txt"); writeFileSync(receiptPath, "outside\n"); return receiptPath; } function createInput(cwd: string, overrides: Partial = {}): SubagentStopInput { return { hook_event_name: "SubagentStop", agent_type: "lazycodex-worker-medium", agent_id: "agent_1", session_id: "sess.1", cwd, transcript_path: "/dev/null", model: "gpt-5.5", permission_mode: "default", stop_hook_active: true, last_assistant_message: "done!", ...overrides, }; } function createUnknownEventInput(cwd: string): Record { return { hook_event_name: "Stop", agent_type: "lazycodex-worker-medium", agent_id: "agent_1", session_id: "sess.1", cwd, transcript_path: "/dev/null", model: "gpt-5.5", permission_mode: "default", stop_hook_active: true, last_assistant_message: "done!", }; } function parseBlockOutput(output: string): BlockOutput { const parsed: unknown = JSON.parse(output); if (!isBlockOutput(parsed)) throw new Error("expected block output"); return parsed; } function isBlockOutput(value: unknown): value is BlockOutput { return ( typeof value === "object" && value !== null && !Array.isArray(value) && "decision" in value && "reason" in value && value.decision === "block" && typeof value.reason === "string" ); } describe("tier worker receipt enforcement", () => { // given the matcher set now covers the difficulty-tier workers const workerTypes = ["lazycodex-worker-low", "lazycodex-worker-medium", "lazycodex-worker-high"] as const; for (const agentType of workerTypes) { it(`#given no evidence receipt #when a ${agentType} child stops #then blocks`, () => { // given const cwd = createWorkspace(); // when const output = runSubagentStopHook(createInput(cwd, { agent_type: agentType }), nodeFileSystem); // then expect(parseBlockOutput(output).decision).toBe("block"); }); } it("#given no evidence receipt #when an explorer child stops #then no-ops", () => { // given const cwd = createWorkspace(); // when const output = runSubagentStopHook(createInput(cwd, { agent_type: "explorer" }), nodeFileSystem); // then expect(output).toBe(""); }); it("#given both hook manifests #when their matchers are applied #then enforced agents match and read-only roles do not", () => { // given const componentManifest = JSON.parse( readFileSync(new URL("../hooks/hooks.json", import.meta.url), "utf8"), ); const rootManifest = JSON.parse( readFileSync( new URL("../../../hooks/subagent-stop-verifying-lazycodex-executor-evidence.json", import.meta.url), "utf8", ), ); for (const manifest of [componentManifest, rootManifest]) { const matcher = new RegExp(manifest.hooks.SubagentStop[0].matcher); // then for (const name of workerTypes) expect(matcher.test(name)).toBe(true); expect(matcher.test("lazycodex-executor")).toBe(false); expect(matcher.test("explorer")).toBe(false); expect(matcher.test("lazycodex-gate-reviewer")).toBe(false); } }); it("#given the tier worker TOMLs #when inspected #then each instructs the EVIDENCE_RECORDED receipt line", () => { for (const tier of ["low", "medium", "high"]) { // when const toml = readFileSync( new URL(`../../ultrawork/agents/lazycodex-worker-${tier}.toml`, import.meta.url), "utf8", ); // then expect(toml).toContain("EVIDENCE_RECORDED: "); } }); });