import fs from "node:fs"; import path from "node:path"; import test from "node:test"; import assert from "node:assert/strict"; import { spawn } from "node:child_process"; import { fileURLToPath } from "node:url"; import { buildEnv, installFakeCodex } from "./fake-codex-fixture.mjs"; import { initGitRepo, makeTempDir, run } from "./helpers.mjs"; import { loadBrokerSession } from "../plugins/codex/scripts/lib/broker-lifecycle.mjs"; import { resolveStateDir } from "../plugins/codex/scripts/lib/state.mjs"; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const PLUGIN_ROOT = path.join(ROOT, "plugins", "codex"); const SCRIPT = path.join(PLUGIN_ROOT, "scripts", "codex-companion.mjs"); const STOP_HOOK = path.join(PLUGIN_ROOT, "scripts", "stop-review-gate-hook.mjs"); const SESSION_HOOK = path.join(PLUGIN_ROOT, "scripts", "session-lifecycle-hook.mjs"); async function waitFor(predicate, { timeoutMs = 5000, intervalMs = 50 } = {}) { const start = Date.now(); while (Date.now() - start < timeoutMs) { const value = await predicate(); if (value) { return value; } await new Promise((resolve) => setTimeout(resolve, intervalMs)); } throw new Error("Timed out waiting for condition."); } test("setup reports ready when fake codex is installed and authenticated", () => { const binDir = makeTempDir(); installFakeCodex(binDir); const result = run("node", [SCRIPT, "setup", "--json"], { cwd: ROOT, env: buildEnv(binDir) }); assert.equal(result.status, 0); const payload = JSON.parse(result.stdout); assert.equal(payload.ready, true); assert.match(payload.codex.detail, /advanced runtime available/); assert.equal(payload.sessionRuntime.mode, "direct"); }); test("setup is ready without npm when Codex is already installed and authenticated", () => { const binDir = makeTempDir(); installFakeCodex(binDir); fs.symlinkSync(process.execPath, path.join(binDir, "node")); const result = run("node", [SCRIPT, "setup", "--json"], { cwd: ROOT, env: { ...process.env, PATH: binDir } }); assert.equal(result.status, 0, result.stderr); const payload = JSON.parse(result.stdout); assert.equal(payload.ready, true); assert.equal(payload.npm.available, false); assert.equal(payload.codex.available, true); assert.equal(payload.auth.loggedIn, true); }); test("setup trusts app-server API key auth even when login status alone would fail", () => { const binDir = makeTempDir(); installFakeCodex(binDir, "api-key-account-only"); const result = run("node", [SCRIPT, "setup", "--json"], { cwd: ROOT, env: buildEnv(binDir) }); assert.equal(result.status, 0, result.stderr); const payload = JSON.parse(result.stdout); assert.equal(payload.ready, true); assert.equal(payload.auth.loggedIn, true); assert.equal(payload.auth.authMethod, "apiKey"); assert.equal(payload.auth.source, "app-server"); assert.match(payload.auth.detail, /API key configured \(unverified\)/); }); test("setup is ready when the active provider does not require OpenAI login", () => { const binDir = makeTempDir(); installFakeCodex(binDir, "provider-no-auth"); const result = run("node", [SCRIPT, "setup", "--json"], { cwd: ROOT, env: buildEnv(binDir) }); assert.equal(result.status, 0, result.stderr); const payload = JSON.parse(result.stdout); assert.equal(payload.ready, true); assert.equal(payload.auth.loggedIn, true); assert.equal(payload.auth.authMethod, null); assert.equal(payload.auth.source, "app-server"); assert.match(payload.auth.detail, /configured and does not require OpenAI authentication/i); }); test("setup treats custom providers with app-server-ready config as ready", () => { const binDir = makeTempDir(); installFakeCodex(binDir, "env-key-provider"); const result = run("node", [SCRIPT, "setup", "--json"], { cwd: ROOT, env: buildEnv(binDir) }); assert.equal(result.status, 0, result.stderr); const payload = JSON.parse(result.stdout); assert.equal(payload.ready, true); assert.equal(payload.auth.loggedIn, true); assert.equal(payload.auth.authMethod, null); assert.equal(payload.auth.source, "app-server"); assert.match(payload.auth.detail, /configured and does not require OpenAI authentication/i); }); test("setup reports not ready when app-server config read fails", () => { const binDir = makeTempDir(); installFakeCodex(binDir, "config-read-fails"); const result = run("node", [SCRIPT, "setup", "--json"], { cwd: ROOT, env: buildEnv(binDir) }); assert.equal(result.status, 0, result.stderr); const payload = JSON.parse(result.stdout); assert.equal(payload.ready, false); assert.equal(payload.auth.loggedIn, false); assert.equal(payload.auth.source, "app-server"); assert.match(payload.auth.detail, /config\/read failed for cwd/); }); test("review renders a no-findings result from app-server review/start", () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir); initGitRepo(repo); fs.mkdirSync(path.join(repo, "src")); fs.writeFileSync(path.join(repo, "src", "app.js"), "export const value = 1;\n"); run("git", ["add", "src/app.js"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); fs.writeFileSync(path.join(repo, "src", "app.js"), "export const value = 2;\n"); const result = run("node", [SCRIPT, "review"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(result.status, 0); assert.match(result.stdout, /Reviewed uncommitted changes/); assert.match(result.stdout, /No material issues found/); }); test("task runs when the active provider does not require OpenAI login", () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir, "provider-no-auth"); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); const result = run("node", [SCRIPT, "task", "check auth preflight"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(result.status, 0, result.stderr); assert.match(result.stdout, /Handled the requested task/); }); test("task runs without auth preflight so Codex can refresh an expired session", () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir, "refreshable-auth"); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); const result = run("node", [SCRIPT, "task", "check refreshable auth"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(result.status, 0, result.stderr); assert.match(result.stdout, /Handled the requested task/); }); test("task reports the actual Codex auth error when the run is rejected", () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir, "auth-run-fails"); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); const result = run("node", [SCRIPT, "task", "check failed auth"], { cwd: repo, env: buildEnv(binDir) }); assert.notEqual(result.status, 0); assert.match(result.stderr, /authentication expired; run codex login/); }); test("review accepts the quoted raw argument style for built-in base-branch review", () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir); initGitRepo(repo); fs.mkdirSync(path.join(repo, "src")); fs.writeFileSync(path.join(repo, "src", "app.js"), "export const value = 1;\n"); run("git", ["add", "src/app.js"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); fs.writeFileSync(path.join(repo, "src", "app.js"), "export const value = 2;\n"); const result = run("node", [SCRIPT, "review", "--base main"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(result.status, 0); assert.match(result.stdout, /Reviewed changes against main/); assert.match(result.stdout, /No material issues found/); }); test("adversarial review renders structured findings over app-server turn/start", () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir); initGitRepo(repo); fs.mkdirSync(path.join(repo, "src")); fs.writeFileSync(path.join(repo, "src", "app.js"), "export const value = items[0];\n"); run("git", ["add", "src/app.js"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); fs.writeFileSync(path.join(repo, "src", "app.js"), "export const value = items[0].id;\n"); const result = run("node", [SCRIPT, "adversarial-review"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(result.status, 0); assert.match(result.stdout, /Missing empty-state guard/); }); test("adversarial review accepts the same base-branch targeting as review", () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir); initGitRepo(repo); fs.mkdirSync(path.join(repo, "src")); fs.writeFileSync(path.join(repo, "src", "app.js"), "export const value = items[0];\n"); run("git", ["add", "src/app.js"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); fs.writeFileSync(path.join(repo, "src", "app.js"), "export const value = items[0].id;\n"); const result = run("node", [SCRIPT, "adversarial-review", "--base", "main"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(result.status, 0, result.stderr); assert.match(result.stdout, /Branch review against main|against main/i); assert.match(result.stdout, /Missing empty-state guard/); }); test("review includes reasoning output when the app server returns it", () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir, "with-reasoning"); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); fs.writeFileSync(path.join(repo, "README.md"), "hello again\n"); const result = run("node", [SCRIPT, "review"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(result.status, 0, result.stderr); assert.match(result.stdout, /Reasoning:/); assert.match(result.stdout, /Reviewed the changed files and checked the likely regression paths first|Reviewed the changed files and checked the likely regression paths/i); }); test("review logs reasoning summaries and review output to the job log", () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir, "with-reasoning"); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); fs.writeFileSync(path.join(repo, "README.md"), "hello again\n"); const result = run("node", [SCRIPT, "review"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(result.status, 0, result.stderr); const stateDir = resolveStateDir(repo); const state = JSON.parse(fs.readFileSync(path.join(stateDir, "state.json"), "utf8")); const log = fs.readFileSync(state.jobs[0].logFile, "utf8"); assert.match(log, /Reasoning summary/); assert.match(log, /Reviewed the changed files and checked the likely regression paths/); assert.match(log, /Review output/); assert.match(log, /Reviewed uncommitted changes\./); }); test("task --resume-last resumes the latest persisted task thread", () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); const firstRun = run("node", [SCRIPT, "task", "initial task"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(firstRun.status, 0, firstRun.stderr); const result = run("node", [SCRIPT, "task", "--resume-last", "follow up"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(result.status, 0, result.stderr); assert.equal(result.stdout, "Resumed the prior run.\nFollow-up prompt accepted.\n"); }); test("task-resume-candidate returns the latest rescue thread from the current session", () => { const workspace = makeTempDir(); const stateDir = resolveStateDir(workspace); const jobsDir = path.join(stateDir, "jobs"); fs.mkdirSync(jobsDir, { recursive: true }); fs.writeFileSync( path.join(stateDir, "state.json"), `${JSON.stringify( { version: 1, config: { stopReviewGate: false }, jobs: [ { id: "task-current", status: "completed", title: "Codex Task", jobClass: "task", sessionId: "sess-current", threadId: "thr_current", summary: "Investigate the flaky test", updatedAt: "2026-03-24T20:00:00.000Z" }, { id: "task-other-session", status: "completed", title: "Codex Task", jobClass: "task", sessionId: "sess-other", threadId: "thr_other", summary: "Old rescue run", updatedAt: "2026-03-24T20:05:00.000Z" }, { id: "review-current", status: "completed", title: "Codex Review", jobClass: "review", sessionId: "sess-current", threadId: "thr_review", summary: "Review main...HEAD", updatedAt: "2026-03-24T20:10:00.000Z" } ] }, null, 2 )}\n`, "utf8" ); const result = run("node", [SCRIPT, "task-resume-candidate", "--json"], { cwd: workspace, env: { ...process.env, CODEX_COMPANION_SESSION_ID: "sess-current" } }); assert.equal(result.status, 0, result.stderr); const payload = JSON.parse(result.stdout); assert.equal(payload.available, true); assert.equal(payload.sessionId, "sess-current"); assert.equal(payload.candidate.id, "task-current"); assert.equal(payload.candidate.threadId, "thr_current"); }); test("task --resume-last does not resume a task from another Claude session", () => { const repo = makeTempDir(); const binDir = makeTempDir(); const statePath = path.join(binDir, "fake-codex-state.json"); installFakeCodex(binDir); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); const otherEnv = { ...buildEnv(binDir), CODEX_COMPANION_SESSION_ID: "sess-other" }; const currentEnv = { ...buildEnv(binDir), CODEX_COMPANION_SESSION_ID: "sess-current" }; const firstRun = run("node", [SCRIPT, "task", "initial task"], { cwd: repo, env: otherEnv }); assert.equal(firstRun.status, 0, firstRun.stderr); const candidate = run("node", [SCRIPT, "task-resume-candidate", "--json"], { cwd: repo, env: currentEnv }); assert.equal(candidate.status, 0, candidate.stderr); assert.equal(JSON.parse(candidate.stdout).available, false); const resume = run("node", [SCRIPT, "task", "--resume-last", "follow up"], { cwd: repo, env: currentEnv }); assert.equal(resume.status, 1); assert.match(resume.stderr, /No previous Codex task thread was found for this repository\./); const fakeState = JSON.parse(fs.readFileSync(statePath, "utf8")); assert.equal(fakeState.lastTurnStart.threadId, "thr_1"); assert.equal(fakeState.lastTurnStart.prompt, "initial task"); }); test("task --resume-last ignores running tasks from other Claude sessions", () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); const stateDir = resolveStateDir(repo); fs.mkdirSync(path.join(stateDir, "jobs"), { recursive: true }); fs.writeFileSync( path.join(stateDir, "state.json"), `${JSON.stringify( { version: 1, config: { stopReviewGate: false }, jobs: [ { id: "task-other-running", status: "running", title: "Codex Task", jobClass: "task", sessionId: "sess-other", threadId: "thr_other", summary: "Other session active task", updatedAt: "2026-03-24T20:05:00.000Z" } ] }, null, 2 )}\n`, "utf8" ); const env = { ...buildEnv(binDir), CODEX_COMPANION_SESSION_ID: "sess-current" }; const status = run("node", [SCRIPT, "status", "--json"], { cwd: repo, env }); assert.equal(status.status, 0, status.stderr); assert.deepEqual(JSON.parse(status.stdout).running, []); const resume = run("node", [SCRIPT, "task", "--resume-last", "follow up"], { cwd: repo, env }); assert.equal(resume.status, 1); assert.match(resume.stderr, /No previous Codex task thread was found for this repository\./); }); test("session start hook exports the Claude session id and plugin data dir for later commands", () => { const repo = makeTempDir(); const envFile = path.join(makeTempDir(), "claude-env.sh"); fs.writeFileSync(envFile, "", "utf8"); const pluginDataDir = makeTempDir(); const result = run("node", [SESSION_HOOK, "SessionStart"], { cwd: repo, env: { ...process.env, CLAUDE_ENV_FILE: envFile, CLAUDE_PLUGIN_DATA: pluginDataDir }, input: JSON.stringify({ hook_event_name: "SessionStart", session_id: "sess-current", cwd: repo }) }); assert.equal(result.status, 0, result.stderr); assert.equal( fs.readFileSync(envFile, "utf8"), `export CODEX_COMPANION_SESSION_ID='sess-current'\nexport CLAUDE_PLUGIN_DATA='${pluginDataDir}'\n` ); }); test("write task output focuses on the Codex result without generic follow-up hints", () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); const result = run("node", [SCRIPT, "task", "--write", "fix the failing test"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(result.status, 0, result.stderr); assert.equal(result.stdout, "Handled the requested task.\nTask prompt accepted.\n"); }); test("task --resume acts like --resume-last without leaking the flag into the prompt", () => { const repo = makeTempDir(); const binDir = makeTempDir(); const statePath = path.join(binDir, "fake-codex-state.json"); installFakeCodex(binDir); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); const firstRun = run("node", [SCRIPT, "task", "initial task"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(firstRun.status, 0, firstRun.stderr); const result = run("node", [SCRIPT, "task", "--resume", "follow up"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(result.status, 0, result.stderr); const fakeState = JSON.parse(fs.readFileSync(statePath, "utf8")); assert.equal(fakeState.lastTurnStart.threadId, "thr_1"); assert.equal(fakeState.lastTurnStart.prompt, "follow up"); }); test("task --fresh is treated as routing control and does not leak into the prompt", () => { const repo = makeTempDir(); const binDir = makeTempDir(); const statePath = path.join(binDir, "fake-codex-state.json"); installFakeCodex(binDir); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); const result = run("node", [SCRIPT, "task", "--fresh", "diagnose the flaky test"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(result.status, 0, result.stderr); const fakeState = JSON.parse(fs.readFileSync(statePath, "utf8")); assert.equal(fakeState.lastTurnStart.prompt, "diagnose the flaky test"); }); test("task forwards model selection and reasoning effort to app-server turn/start", () => { const repo = makeTempDir(); const binDir = makeTempDir(); const statePath = path.join(binDir, "fake-codex-state.json"); installFakeCodex(binDir); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); const result = run("node", [SCRIPT, "task", "--model", "spark", "--effort", "low", "diagnose the failing test"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(result.status, 0, result.stderr); const fakeState = JSON.parse(fs.readFileSync(statePath, "utf8")); assert.equal(fakeState.lastTurnStart.model, "gpt-5.3-codex-spark"); assert.equal(fakeState.lastTurnStart.effort, "low"); }); test("task logs reasoning summaries and assistant messages to the job log", () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir, "with-reasoning"); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); const result = run("node", [SCRIPT, "task", "investigate the failing test"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(result.status, 0, result.stderr); const stateDir = resolveStateDir(repo); const state = JSON.parse(fs.readFileSync(path.join(stateDir, "state.json"), "utf8")); const log = fs.readFileSync(state.jobs[0].logFile, "utf8"); assert.match(log, /Reasoning summary/); assert.match(log, /Inspected the prompt, gathered evidence, and checked the highest-risk paths first/); assert.match(log, /Assistant message/); assert.match(log, /Handled the requested task/); }); test("task logs subagent reasoning and messages with a subagent prefix", () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir, "with-subagent"); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); const result = run("node", [SCRIPT, "task", "challenge the current design"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(result.status, 0, result.stderr); const stateDir = resolveStateDir(repo); const state = JSON.parse(fs.readFileSync(path.join(stateDir, "state.json"), "utf8")); const log = fs.readFileSync(state.jobs[0].logFile, "utf8"); assert.match(log, /Starting subagent design-challenger via collaboration tool: wait\./); assert.match(log, /Subagent design-challenger reasoning:/); assert.match(log, /Questioned the retry strategy and the cache invalidation boundaries\./); assert.match(log, /Subagent design-challenger:/); assert.match( log, /The design assumes retries are harmless, but they can duplicate side effects without stronger idempotency guarantees\./ ); }); test("task waits for the main thread to complete before returning the final result", () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir, "with-subagent"); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); const result = run("node", [SCRIPT, "task", "challenge the current design"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(result.status, 0, result.stderr); assert.equal(result.stdout, "Handled the requested task.\nTask prompt accepted.\n"); }); test("task ignores later subagent messages when choosing the final returned output", () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir, "with-late-subagent-message"); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); const result = run("node", [SCRIPT, "task", "challenge the current design"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(result.status, 0, result.stderr); assert.equal(result.stdout, "Handled the requested task.\nTask prompt accepted.\n"); }); test("task can finish after subagent work even if the parent turn/completed event is missing", () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir, "with-subagent-no-main-turn-completed"); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); const result = run("node", [SCRIPT, "task", "challenge the current design"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(result.status, 0, result.stderr); assert.equal(result.stdout, "Handled the requested task.\nTask prompt accepted.\n"); }); test("task using the shared broker still completes when Codex spawns subagents", () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir, "with-subagent"); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); fs.writeFileSync(path.join(repo, "README.md"), "hello again\n"); const env = buildEnv(binDir); const review = run("node", [SCRIPT, "review"], { cwd: repo, env }); assert.equal(review.status, 0, review.stderr); if (!loadBrokerSession(repo)) { return; } const result = run("node", [SCRIPT, "task", "challenge the current design"], { cwd: repo, env }); assert.equal(result.status, 0, result.stderr); assert.equal(result.stdout, "Handled the requested task.\nTask prompt accepted.\n"); }); test("task --background enqueues a detached worker and exposes per-job status", async () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir, "slow-task"); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); const launched = run("node", [SCRIPT, "task", "--background", "--json", "investigate the failing test"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(launched.status, 0, launched.stderr); const launchPayload = JSON.parse(launched.stdout); assert.equal(launchPayload.status, "queued"); assert.match(launchPayload.jobId, /^task-/); const waitedStatus = run( "node", [SCRIPT, "status", launchPayload.jobId, "--wait", "--timeout-ms", "15000", "--json"], { cwd: repo, env: buildEnv(binDir) } ); assert.equal(waitedStatus.status, 0, waitedStatus.stderr); const waitedPayload = JSON.parse(waitedStatus.stdout); assert.equal(waitedPayload.job.id, launchPayload.jobId); assert.equal(waitedPayload.job.status, "completed"); const resultPayload = await waitFor(() => { const result = run("node", [SCRIPT, "result", launchPayload.jobId, "--json"], { cwd: repo, env: buildEnv(binDir) }); if (result.status !== 0) { return null; } return JSON.parse(result.stdout); }); assert.equal(resultPayload.job.id, launchPayload.jobId); assert.equal(resultPayload.job.status, "completed"); assert.match(resultPayload.storedJob.rendered, /Handled the requested task/); }); test("review rejects focus text because it is native-review only", () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); fs.writeFileSync(path.join(repo, "README.md"), "hello again\n"); const result = run("node", [SCRIPT, "review", "--scope working-tree focus on auth"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(result.status > 0, true); assert.match(result.stderr, /does not support custom focus text/i); assert.match(result.stderr, /\/codex:adversarial-review focus on auth/i); }); test("review rejects staged-only scope because it is native-review only", () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); fs.writeFileSync(path.join(repo, "README.md"), "hello again\n"); run("git", ["add", "README.md"], { cwd: repo }); const result = run("node", [SCRIPT, "review", "--scope", "staged"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(result.status > 0, true); assert.match(result.stderr, /Unsupported review scope "staged"/i); assert.match(result.stderr, /Use one of: auto, working-tree, branch, or pass --base /i); }); test("adversarial review rejects staged-only scope to match review target selection", () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); fs.writeFileSync(path.join(repo, "README.md"), "hello again\n"); run("git", ["add", "README.md"], { cwd: repo }); const result = run("node", [SCRIPT, "adversarial-review", "--scope", "staged"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(result.status > 0, true); assert.match(result.stderr, /Unsupported review scope "staged"/i); assert.match(result.stderr, /Use one of: auto, working-tree, branch, or pass --base /i); }); test("review accepts --background while still running as a tracked review job", () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); fs.writeFileSync(path.join(repo, "README.md"), "hello again\n"); const launched = run("node", [SCRIPT, "review", "--background", "--json"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(launched.status, 0, launched.stderr); const launchPayload = JSON.parse(launched.stdout); assert.equal(launchPayload.review, "Review"); assert.match(launchPayload.codex.stdout, /No material issues found/); const status = run("node", [SCRIPT, "status"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(status.status, 0, status.stderr); assert.match(status.stdout, /# Codex Status/); assert.match(status.stdout, /Codex Review/); assert.match(status.stdout, /completed/); }); test("status shows phases, hints, and the latest finished job", () => { const workspace = makeTempDir(); const stateDir = resolveStateDir(workspace); const jobsDir = path.join(stateDir, "jobs"); fs.mkdirSync(jobsDir, { recursive: true }); const logFile = path.join(jobsDir, "review-live.log"); fs.writeFileSync( logFile, [ "[2026-03-18T15:30:00.000Z] Starting Codex Review.", "[2026-03-18T15:30:01.000Z] Thread ready (thr_1).", "[2026-03-18T15:30:02.000Z] Turn started (turn_1).", "[2026-03-18T15:30:03.000Z] Reviewer started: current changes" ].join("\n"), "utf8" ); const finishedJobFile = path.join(jobsDir, "review-done.json"); fs.writeFileSync( finishedJobFile, JSON.stringify( { id: "review-done", status: "completed", title: "Codex Review", rendered: "# Codex Review\n\nReviewed uncommitted changes.\nNo material issues found.\n" }, null, 2 ), "utf8" ); fs.writeFileSync( path.join(stateDir, "state.json"), `${JSON.stringify( { version: 1, config: { stopReviewGate: false }, jobs: [ { id: "review-live", kind: "review", kindLabel: "review", status: "running", title: "Codex Review", jobClass: "review", phase: "reviewing", threadId: "thr_1", summary: "Review working tree diff", logFile, createdAt: "2026-03-18T15:30:00.000Z", updatedAt: "2026-03-18T15:30:03.000Z" }, { id: "review-done", status: "completed", title: "Codex Review", jobClass: "review", threadId: "thr_done", summary: "Review main...HEAD", createdAt: "2026-03-18T15:10:00.000Z", startedAt: "2026-03-18T15:10:05.000Z", completedAt: "2026-03-18T15:11:10.000Z", updatedAt: "2026-03-18T15:11:10.000Z" } ] }, null, 2 )}\n`, "utf8" ); const result = run("node", [SCRIPT, "status"], { cwd: workspace }); assert.equal(result.status, 0, result.stderr); assert.match(result.stdout, /Active jobs:/); assert.match(result.stdout, /\| Job \| Kind \| Status \| Phase \| Elapsed \| Codex Session ID \| Summary \| Actions \|/); assert.match(result.stdout, /\| review-live \| review \| running \| reviewing \| .* \| thr_1 \| Review working tree diff \|/); assert.match(result.stdout, /`\/codex:status review-live`
`\/codex:cancel review-live`/); assert.match(result.stdout, /Live details:/); assert.match(result.stdout, /Latest finished:/); assert.match(result.stdout, /Progress:/); assert.match(result.stdout, /Session runtime: direct startup/); assert.match(result.stdout, /Phase: reviewing/); assert.match(result.stdout, /Codex session ID: thr_1/); assert.match(result.stdout, /Resume in Codex: codex resume thr_1/); assert.match(result.stdout, /Thread ready \(thr_1\)\./); assert.match(result.stdout, /Reviewer started: current changes/); assert.match(result.stdout, /Duration: 1m 5s/); assert.match(result.stdout, /Codex session ID: thr_done/); assert.match(result.stdout, /Resume in Codex: codex resume thr_done/); }); test("status without a job id only shows jobs from the current Claude session", () => { const workspace = makeTempDir(); const stateDir = resolveStateDir(workspace); const jobsDir = path.join(stateDir, "jobs"); fs.mkdirSync(jobsDir, { recursive: true }); const currentLog = path.join(jobsDir, "review-current.log"); const otherLog = path.join(jobsDir, "review-other.log"); fs.writeFileSync(currentLog, "[2026-03-18T15:30:00.000Z] Reviewer started: current changes\n", "utf8"); fs.writeFileSync(otherLog, "[2026-03-18T15:31:00.000Z] Reviewer started: old changes\n", "utf8"); fs.writeFileSync( path.join(stateDir, "state.json"), `${JSON.stringify( { version: 1, config: { stopReviewGate: false }, jobs: [ { id: "review-current", kind: "review", kindLabel: "review", status: "running", title: "Codex Review", jobClass: "review", phase: "reviewing", sessionId: "sess-current", threadId: "thr_current", summary: "Current session review", logFile: currentLog, createdAt: "2026-03-18T15:30:00.000Z", updatedAt: "2026-03-18T15:30:00.000Z" }, { id: "review-other", kind: "review", kindLabel: "review", status: "completed", title: "Codex Review", jobClass: "review", sessionId: "sess-other", threadId: "thr_other", summary: "Previous session review", createdAt: "2026-03-18T15:20:00.000Z", startedAt: "2026-03-18T15:20:05.000Z", completedAt: "2026-03-18T15:21:00.000Z", updatedAt: "2026-03-18T15:21:00.000Z" } ] }, null, 2 )}\n`, "utf8" ); const result = run("node", [SCRIPT, "status"], { cwd: workspace, env: { ...process.env, CODEX_COMPANION_SESSION_ID: "sess-current" } }); assert.equal(result.status, 0, result.stderr); assert.deepEqual( [...new Set(result.stdout.match(/review-(?:current|other)/g) ?? [])], ["review-current"] ); }); test("status preserves adversarial review kind labels", () => { const workspace = makeTempDir(); const stateDir = resolveStateDir(workspace); const jobsDir = path.join(stateDir, "jobs"); fs.mkdirSync(jobsDir, { recursive: true }); const logFile = path.join(jobsDir, "review-adv.log"); fs.writeFileSync(logFile, "[2026-03-18T15:30:00.000Z] Reviewer started: adversarial review\n", "utf8"); fs.writeFileSync( path.join(stateDir, "state.json"), `${JSON.stringify( { version: 1, config: { stopReviewGate: false }, jobs: [ { id: "review-adv-live", kind: "adversarial-review", status: "running", title: "Codex Adversarial Review", jobClass: "review", phase: "reviewing", threadId: "thr_adv_live", summary: "Adversarial review current changes", logFile, createdAt: "2026-03-18T15:30:00.000Z", updatedAt: "2026-03-18T15:30:00.000Z" }, { id: "review-adv", kind: "adversarial-review", status: "completed", title: "Codex Adversarial Review", jobClass: "review", threadId: "thr_adv_done", summary: "Adversarial review working tree diff", createdAt: "2026-03-18T15:10:00.000Z", startedAt: "2026-03-18T15:10:05.000Z", completedAt: "2026-03-18T15:11:10.000Z", updatedAt: "2026-03-18T15:11:10.000Z" } ] }, null, 2 )}\n`, "utf8" ); const result = run("node", [SCRIPT, "status"], { cwd: workspace }); assert.equal(result.status, 0, result.stderr); assert.match(result.stdout, /\| review-adv-live \| adversarial-review \| running \| reviewing \|/); assert.match(result.stdout, /- review-adv \| completed \| adversarial-review \| Codex Adversarial Review/); assert.match(result.stdout, /Codex session ID: thr_adv_live/); assert.match(result.stdout, /Codex session ID: thr_adv_done/); }); test("status --wait times out cleanly when a job is still active", () => { const workspace = makeTempDir(); const stateDir = resolveStateDir(workspace); const jobsDir = path.join(stateDir, "jobs"); fs.mkdirSync(jobsDir, { recursive: true }); const logFile = path.join(jobsDir, "task-live.log"); fs.writeFileSync(logFile, "[2026-03-18T15:30:00.000Z] Starting Codex Task.\n", "utf8"); fs.writeFileSync( path.join(jobsDir, "task-live.json"), JSON.stringify( { id: "task-live", status: "running", title: "Codex Task", logFile }, null, 2 ), "utf8" ); fs.writeFileSync( path.join(stateDir, "state.json"), `${JSON.stringify( { version: 1, config: { stopReviewGate: false }, jobs: [ { id: "task-live", status: "running", title: "Codex Task", jobClass: "task", summary: "Investigate flaky test", logFile, createdAt: "2026-03-18T15:30:00.000Z", startedAt: "2026-03-18T15:30:01.000Z", updatedAt: "2026-03-18T15:30:02.000Z" } ] }, null, 2 )}\n`, "utf8" ); const result = run("node", [SCRIPT, "status", "task-live", "--wait", "--timeout-ms", "25", "--json"], { cwd: workspace }); assert.equal(result.status, 0, result.stderr); const payload = JSON.parse(result.stdout); assert.equal(payload.job.id, "task-live"); assert.equal(payload.job.status, "running"); assert.equal(payload.waitTimedOut, true); }); test("result returns the stored output for the latest finished job by default", () => { const workspace = makeTempDir(); const stateDir = resolveStateDir(workspace); const jobsDir = path.join(stateDir, "jobs"); fs.mkdirSync(jobsDir, { recursive: true }); fs.writeFileSync( path.join(jobsDir, "review-finished.json"), JSON.stringify( { id: "review-finished", status: "completed", title: "Codex Review", rendered: "# Codex Review\n\nReviewed uncommitted changes.\nNo material issues found.\n", result: { codex: { stdout: "Reviewed uncommitted changes.\nNo material issues found." } }, threadId: "thr_review_finished" }, null, 2 ), "utf8" ); fs.writeFileSync( path.join(stateDir, "state.json"), `${JSON.stringify( { version: 1, config: { stopReviewGate: false }, jobs: [ { id: "review-finished", status: "completed", title: "Codex Review", jobClass: "review", threadId: "thr_review_finished", summary: "Review working tree diff", createdAt: "2026-03-18T15:00:00.000Z", updatedAt: "2026-03-18T15:01:00.000Z" } ] }, null, 2 )}\n`, "utf8" ); const result = run("node", [SCRIPT, "result"], { cwd: workspace }); assert.equal(result.status, 0, result.stderr); assert.equal( result.stdout, "Reviewed uncommitted changes.\nNo material issues found.\n\nCodex session ID: thr_review_finished\nResume in Codex: codex resume thr_review_finished\n" ); }); test("result without a job id prefers the latest finished job from the current Claude session", () => { const workspace = makeTempDir(); const stateDir = resolveStateDir(workspace); const jobsDir = path.join(stateDir, "jobs"); fs.mkdirSync(jobsDir, { recursive: true }); fs.writeFileSync( path.join(jobsDir, "review-current.json"), JSON.stringify( { id: "review-current", status: "completed", title: "Codex Review", threadId: "thr_current", result: { codex: { stdout: "Current session output." } } }, null, 2 ), "utf8" ); fs.writeFileSync( path.join(jobsDir, "review-other.json"), JSON.stringify( { id: "review-other", status: "completed", title: "Codex Review", threadId: "thr_other", result: { codex: { stdout: "Old session output." } } }, null, 2 ), "utf8" ); fs.writeFileSync( path.join(stateDir, "state.json"), `${JSON.stringify( { version: 1, config: { stopReviewGate: false }, jobs: [ { id: "review-current", status: "completed", title: "Codex Review", jobClass: "review", sessionId: "sess-current", threadId: "thr_current", summary: "Current session review", createdAt: "2026-03-18T15:10:00.000Z", updatedAt: "2026-03-18T15:11:00.000Z" }, { id: "review-other", status: "completed", title: "Codex Review", jobClass: "review", sessionId: "sess-other", threadId: "thr_other", summary: "Old session review", createdAt: "2026-03-18T15:20:00.000Z", updatedAt: "2026-03-18T15:21:00.000Z" } ] }, null, 2 )}\n`, "utf8" ); const result = run("node", [SCRIPT, "result"], { cwd: workspace, env: { ...process.env, CODEX_COMPANION_SESSION_ID: "sess-current" } }); assert.equal(result.status, 0, result.stderr); assert.equal( result.stdout, "Current session output.\n\nCodex session ID: thr_current\nResume in Codex: codex resume thr_current\n" ); }); test("result for a finished write-capable task returns the raw Codex final response", () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); const taskRun = run("node", [SCRIPT, "task", "--write", "fix the flaky integration test"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(taskRun.status, 0, taskRun.stderr); const result = run("node", [SCRIPT, "result"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(result.status, 0, result.stderr); assert.match(result.stdout, /^Handled the requested task\.\nTask prompt accepted\.\n/); assert.match(result.stdout, /Codex session ID: thr_[a-z0-9]+/i); assert.match(result.stdout, /Resume in Codex: codex resume thr_[a-z0-9]+/i); }); test("cancel stops an active background job and marks it cancelled", async (t) => { const workspace = makeTempDir(); const stateDir = resolveStateDir(workspace); const jobsDir = path.join(stateDir, "jobs"); fs.mkdirSync(jobsDir, { recursive: true }); const sleeper = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"], { cwd: workspace, detached: true, stdio: "ignore" }); sleeper.unref(); t.after(() => { try { process.kill(-sleeper.pid, "SIGTERM"); } catch { try { process.kill(sleeper.pid, "SIGTERM"); } catch { // Ignore missing process. } } }); const logFile = path.join(jobsDir, "task-live.log"); const jobFile = path.join(jobsDir, "task-live.json"); fs.writeFileSync(logFile, "[2026-03-18T15:30:00.000Z] Starting Codex Task.\n", "utf8"); fs.writeFileSync( jobFile, JSON.stringify( { id: "task-live", status: "running", title: "Codex Task", logFile }, null, 2 ), "utf8" ); fs.writeFileSync( path.join(stateDir, "state.json"), `${JSON.stringify( { version: 1, config: { stopReviewGate: false }, jobs: [ { id: "task-live", status: "running", title: "Codex Task", jobClass: "task", summary: "Investigate flaky test", pid: sleeper.pid, logFile, createdAt: "2026-03-18T15:30:00.000Z", startedAt: "2026-03-18T15:30:01.000Z", updatedAt: "2026-03-18T15:30:02.000Z" } ] }, null, 2 )}\n`, "utf8" ); const cancelResult = run("node", [SCRIPT, "cancel", "task-live", "--json"], { cwd: workspace }); assert.equal(cancelResult.status, 0, cancelResult.stderr); assert.equal(JSON.parse(cancelResult.stdout).status, "cancelled"); await waitFor(() => { try { process.kill(sleeper.pid, 0); return false; } catch (error) { return error?.code === "ESRCH"; } }); const state = JSON.parse(fs.readFileSync(path.join(stateDir, "state.json"), "utf8")); const cancelled = state.jobs.find((job) => job.id === "task-live"); assert.equal(cancelled.status, "cancelled"); assert.equal(cancelled.pid, null); const stored = JSON.parse(fs.readFileSync(jobFile, "utf8")); assert.equal(stored.status, "cancelled"); assert.match(fs.readFileSync(logFile, "utf8"), /Cancelled by user/); }); test("cancel without a job id ignores active jobs from other Claude sessions", () => { const workspace = makeTempDir(); const stateDir = resolveStateDir(workspace); const jobsDir = path.join(stateDir, "jobs"); fs.mkdirSync(jobsDir, { recursive: true }); const logFile = path.join(jobsDir, "task-other.log"); fs.writeFileSync(logFile, "", "utf8"); fs.writeFileSync( path.join(stateDir, "state.json"), `${JSON.stringify( { version: 1, config: { stopReviewGate: false }, jobs: [ { id: "task-other", status: "running", title: "Codex Task", jobClass: "task", sessionId: "sess-other", summary: "Other session run", updatedAt: "2026-03-24T20:05:00.000Z", logFile } ] }, null, 2 )}\n`, "utf8" ); const env = { ...process.env, CODEX_COMPANION_SESSION_ID: "sess-current" }; const status = run("node", [SCRIPT, "status", "--json"], { cwd: workspace, env }); assert.equal(status.status, 0, status.stderr); assert.deepEqual(JSON.parse(status.stdout).running, []); const cancel = run("node", [SCRIPT, "cancel", "--json"], { cwd: workspace, env }); assert.equal(cancel.status, 1); assert.match(cancel.stderr, /No active Codex jobs to cancel for this session\./); const state = JSON.parse(fs.readFileSync(path.join(stateDir, "state.json"), "utf8")); assert.equal(state.jobs[0].status, "running"); }); test("cancel with a job id can still target an active job from another Claude session", () => { const workspace = makeTempDir(); const stateDir = resolveStateDir(workspace); const jobsDir = path.join(stateDir, "jobs"); fs.mkdirSync(jobsDir, { recursive: true }); const logFile = path.join(jobsDir, "task-other.log"); fs.writeFileSync(logFile, "", "utf8"); fs.writeFileSync( path.join(stateDir, "state.json"), `${JSON.stringify( { version: 1, config: { stopReviewGate: false }, jobs: [ { id: "task-other", status: "running", title: "Codex Task", jobClass: "task", sessionId: "sess-other", summary: "Other session run", updatedAt: "2026-03-24T20:05:00.000Z", logFile } ] }, null, 2 )}\n`, "utf8" ); const env = { ...process.env, CODEX_COMPANION_SESSION_ID: "sess-current" }; const cancel = run("node", [SCRIPT, "cancel", "task-other", "--json"], { cwd: workspace, env }); assert.equal(cancel.status, 0, cancel.stderr); assert.equal(JSON.parse(cancel.stdout).jobId, "task-other"); const state = JSON.parse(fs.readFileSync(path.join(stateDir, "state.json"), "utf8")); assert.equal(state.jobs[0].status, "cancelled"); }); test("cancel sends turn interrupt to the shared app-server before killing a brokered task", async () => { const repo = makeTempDir(); const binDir = makeTempDir(); const fakeStatePath = path.join(binDir, "fake-codex-state.json"); installFakeCodex(binDir, "interruptible-slow-task"); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); const env = buildEnv(binDir); const launched = run("node", [SCRIPT, "task", "--background", "--json", "investigate the flaky worker timeout"], { cwd: repo, env }); assert.equal(launched.status, 0, launched.stderr); const launchPayload = JSON.parse(launched.stdout); const jobId = launchPayload.jobId; assert.ok(jobId); const stateDir = resolveStateDir(repo); const runningJob = await waitFor(() => { const state = JSON.parse(fs.readFileSync(path.join(stateDir, "state.json"), "utf8")); const job = state.jobs.find((candidate) => candidate.id === jobId); if (job?.status === "running" && job.threadId && job.turnId) { return job; } return null; }, { timeoutMs: 15000 }); const cancelResult = run("node", [SCRIPT, "cancel", jobId, "--json"], { cwd: repo, env }); assert.equal(cancelResult.status, 0, cancelResult.stderr); const cancelPayload = JSON.parse(cancelResult.stdout); assert.equal(cancelPayload.status, "cancelled"); assert.equal(cancelPayload.turnInterruptAttempted, true); assert.equal(cancelPayload.turnInterrupted, true); await waitFor(() => { const fakeState = JSON.parse(fs.readFileSync(fakeStatePath, "utf8")); return fakeState.lastInterrupt ?? null; }); const fakeState = JSON.parse(fs.readFileSync(fakeStatePath, "utf8")); assert.deepEqual(fakeState.lastInterrupt, { threadId: runningJob.threadId, turnId: runningJob.turnId }); const cleanup = run("node", [SESSION_HOOK, "SessionEnd"], { cwd: repo, env, input: JSON.stringify({ hook_event_name: "SessionEnd", cwd: repo }) }); assert.equal(cleanup.status, 0, cleanup.stderr); }); test("session end fully cleans up jobs for the ending session", async (t) => { const repo = makeTempDir(); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); const stateDir = resolveStateDir(repo); const jobsDir = path.join(stateDir, "jobs"); fs.mkdirSync(jobsDir, { recursive: true }); const completedLog = path.join(jobsDir, "completed.log"); const runningLog = path.join(jobsDir, "running.log"); const otherSessionLog = path.join(jobsDir, "other.log"); const completedJobFile = path.join(jobsDir, "review-completed.json"); const runningJobFile = path.join(jobsDir, "review-running.json"); const otherJobFile = path.join(jobsDir, "review-other.json"); fs.writeFileSync(completedLog, "completed\n", "utf8"); fs.writeFileSync(runningLog, "running\n", "utf8"); fs.writeFileSync(otherSessionLog, "other\n", "utf8"); fs.writeFileSync(completedJobFile, JSON.stringify({ id: "review-completed" }, null, 2), "utf8"); fs.writeFileSync(otherJobFile, JSON.stringify({ id: "review-other" }, null, 2), "utf8"); const sleeper = spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"], { cwd: repo, detached: true, stdio: "ignore" }); sleeper.unref(); fs.writeFileSync(runningJobFile, JSON.stringify({ id: "review-running" }, null, 2), "utf8"); t.after(() => { try { process.kill(-sleeper.pid, "SIGTERM"); } catch { try { process.kill(sleeper.pid, "SIGTERM"); } catch { // Ignore missing process. } } }); fs.writeFileSync( path.join(stateDir, "state.json"), `${JSON.stringify( { version: 1, config: { stopReviewGate: false }, jobs: [ { id: "review-completed", status: "completed", title: "Codex Review", sessionId: "sess-current", logFile: completedLog, createdAt: "2026-03-18T15:30:00.000Z", updatedAt: "2026-03-18T15:31:00.000Z" }, { id: "review-running", status: "running", title: "Codex Review", sessionId: "sess-current", pid: sleeper.pid, logFile: runningLog, createdAt: "2026-03-18T15:32:00.000Z", updatedAt: "2026-03-18T15:33:00.000Z" }, { id: "review-other", status: "completed", title: "Codex Review", sessionId: "sess-other", logFile: otherSessionLog, createdAt: "2026-03-18T15:34:00.000Z", updatedAt: "2026-03-18T15:35:00.000Z" } ] }, null, 2 )}\n`, "utf8" ); const result = run("node", [SESSION_HOOK, "SessionEnd"], { cwd: repo, env: { ...process.env, CODEX_COMPANION_SESSION_ID: "sess-current" }, input: JSON.stringify({ hook_event_name: "SessionEnd", session_id: "sess-current", cwd: repo }) }); assert.equal(result.status, 0, result.stderr); assert.equal(fs.existsSync(otherSessionLog), true); assert.equal(fs.existsSync(otherJobFile), true); assert.deepEqual( fs.readdirSync(path.dirname(otherJobFile)).sort(), [path.basename(otherJobFile), path.basename(otherSessionLog)].sort() ); await waitFor(() => { try { process.kill(sleeper.pid, 0); return false; } catch (error) { return error?.code === "ESRCH"; } }); const state = JSON.parse(fs.readFileSync(path.join(stateDir, "state.json"), "utf8")); assert.deepEqual(state.jobs.map((job) => job.id), ["review-other"]); const otherJob = state.jobs[0]; assert.equal(otherJob.logFile, otherSessionLog); }); test("stop hook runs a stop-time review task and blocks on findings when the review gate is enabled", () => { const repo = makeTempDir(); const binDir = makeTempDir(); const fakeStatePath = path.join(binDir, "fake-codex-state.json"); installFakeCodex(binDir); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); const setup = run("node", [SCRIPT, "setup", "--enable-review-gate", "--json"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(setup.status, 0, setup.stderr); const setupPayload = JSON.parse(setup.stdout); assert.equal(setupPayload.reviewGateEnabled, true); const taskResult = run("node", [SCRIPT, "task", "--write", "fix the issue"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(taskResult.status, 0, taskResult.stderr); const blocked = run("node", [STOP_HOOK], { cwd: repo, env: buildEnv(binDir), input: JSON.stringify({ cwd: repo, session_id: "sess-stop-review", last_assistant_message: "I completed the refactor and updated the retry logic." }) }); assert.equal(blocked.status, 0, blocked.stderr); const blockedPayload = JSON.parse(blocked.stdout); assert.equal(blockedPayload.decision, "block"); assert.match(blockedPayload.reason, /Codex stop-time review found issues that still need fixes/i); assert.match(blockedPayload.reason, /Missing empty-state guard/i); const fakeState = JSON.parse(fs.readFileSync(fakeStatePath, "utf8")); assert.match(fakeState.lastTurnStart.prompt, //i); assert.match(fakeState.lastTurnStart.prompt, //i); assert.match(fakeState.lastTurnStart.prompt, /Only review the work from the previous Claude turn/i); assert.match(fakeState.lastTurnStart.prompt, /I completed the refactor and updated the retry logic\./); const status = run("node", [SCRIPT, "status"], { cwd: repo, env: { ...buildEnv(binDir), CODEX_COMPANION_SESSION_ID: "sess-stop-review" } }); assert.equal(status.status, 0, status.stderr); assert.match(status.stdout, /Codex Stop Gate Review/); }); test("stop hook logs running tasks to stderr without blocking when the review gate is disabled", () => { const repo = makeTempDir(); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); const stateDir = resolveStateDir(repo); const jobsDir = path.join(stateDir, "jobs"); fs.mkdirSync(jobsDir, { recursive: true }); const runningLog = path.join(jobsDir, "task-running.log"); fs.writeFileSync(runningLog, "running\n", "utf8"); fs.writeFileSync( path.join(stateDir, "state.json"), `${JSON.stringify( { version: 1, config: { stopReviewGate: false }, jobs: [ { id: "task-live", status: "running", title: "Codex Task", jobClass: "task", sessionId: "sess-current", logFile: runningLog, createdAt: "2026-03-18T15:32:00.000Z", updatedAt: "2026-03-18T15:33:00.000Z" } ] }, null, 2 )}\n`, "utf8" ); const blocked = run("node", [STOP_HOOK], { cwd: repo, env: { ...process.env, CODEX_COMPANION_SESSION_ID: "sess-current" }, input: JSON.stringify({ cwd: repo }) }); assert.equal(blocked.status, 0, blocked.stderr); assert.equal(blocked.stdout.trim(), ""); assert.match(blocked.stderr, /Codex task task-live is still running/i); assert.match(blocked.stderr, /\/codex:status/i); assert.match(blocked.stderr, /\/codex:cancel task-live/i); }); test("stop hook allows the stop when the review gate is enabled and the stop-time review task is clean", () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir, "adversarial-clean"); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); const setup = run("node", [SCRIPT, "setup", "--enable-review-gate", "--json"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(setup.status, 0, setup.stderr); const allowed = run("node", [STOP_HOOK], { cwd: repo, env: buildEnv(binDir), input: JSON.stringify({ cwd: repo, session_id: "sess-stop-clean" }) }); assert.equal(allowed.status, 0, allowed.stderr); assert.equal(allowed.stdout.trim(), ""); }); test("stop hook does not block when Codex is unavailable even if the review gate is enabled", () => { const repo = makeTempDir(); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); const setup = run(process.execPath, [SCRIPT, "setup", "--enable-review-gate", "--json"], { cwd: repo }); assert.equal(setup.status, 0, setup.stderr); const allowed = run(process.execPath, [STOP_HOOK], { cwd: repo, env: { ...process.env, PATH: "" }, input: JSON.stringify({ cwd: repo }) }); assert.equal(allowed.status, 0, allowed.stderr); assert.equal(allowed.stdout.trim(), ""); assert.match(allowed.stderr, /Codex is not set up for the review gate/i); assert.match(allowed.stderr, /Run \/codex:setup/i); }); test("stop hook runs the actual task when auth status looks stale", () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir, "refreshable-auth"); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); const setup = run("node", [SCRIPT, "setup", "--enable-review-gate", "--json"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(setup.status, 0, setup.stderr); const allowed = run("node", [STOP_HOOK], { cwd: repo, env: buildEnv(binDir), input: JSON.stringify({ cwd: repo }) }); assert.equal(allowed.status, 0, allowed.stderr); assert.doesNotMatch(allowed.stderr, /Codex is not set up for the review gate/i); const payload = JSON.parse(allowed.stdout); assert.equal(payload.decision, "block"); assert.match(payload.reason, /Missing empty-state guard/i); }); test("commands lazily start and reuse one shared app-server after first use", async () => { const repo = makeTempDir(); const binDir = makeTempDir(); const fakeStatePath = path.join(binDir, "fake-codex-state.json"); installFakeCodex(binDir); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); fs.writeFileSync(path.join(repo, "README.md"), "hello again\n"); const env = buildEnv(binDir); const review = run("node", [SCRIPT, "review"], { cwd: repo, env }); assert.equal(review.status, 0, review.stderr); const brokerSession = loadBrokerSession(repo); if (!brokerSession) { return; } const adversarial = run("node", [SCRIPT, "adversarial-review"], { cwd: repo, env }); assert.equal(adversarial.status, 0, adversarial.stderr); const fakeState = JSON.parse(fs.readFileSync(fakeStatePath, "utf8")); assert.equal(fakeState.appServerStarts, 1); const cleanup = run("node", [SESSION_HOOK, "SessionEnd"], { cwd: repo, env, input: JSON.stringify({ hook_event_name: "SessionEnd", cwd: repo }) }); assert.equal(cleanup.status, 0, cleanup.stderr); }); test("setup reuses an existing shared app-server without starting another one", () => { const repo = makeTempDir(); const binDir = makeTempDir(); const fakeStatePath = path.join(binDir, "fake-codex-state.json"); installFakeCodex(binDir); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); fs.writeFileSync(path.join(repo, "README.md"), "hello again\n"); const env = buildEnv(binDir); const review = run("node", [SCRIPT, "review"], { cwd: repo, env }); assert.equal(review.status, 0, review.stderr); const brokerSession = loadBrokerSession(repo); if (!brokerSession) { return; } const setup = run("node", [SCRIPT, "setup", "--json"], { cwd: repo, env }); assert.equal(setup.status, 0, setup.stderr); const fakeState = JSON.parse(fs.readFileSync(fakeStatePath, "utf8")); assert.equal(fakeState.appServerStarts, 1); const cleanup = run("node", [SESSION_HOOK, "SessionEnd"], { cwd: repo, env, input: JSON.stringify({ hook_event_name: "SessionEnd", cwd: repo }) }); assert.equal(cleanup.status, 0, cleanup.stderr); }); test("status reports shared session runtime when a lazy broker is active", () => { const repo = makeTempDir(); const binDir = makeTempDir(); installFakeCodex(binDir); initGitRepo(repo); fs.writeFileSync(path.join(repo, "README.md"), "hello\n"); run("git", ["add", "README.md"], { cwd: repo }); run("git", ["commit", "-m", "init"], { cwd: repo }); fs.writeFileSync(path.join(repo, "README.md"), "hello again\n"); const review = run("node", [SCRIPT, "review"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(review.status, 0, review.stderr); if (!loadBrokerSession(repo)) { return; } const result = run("node", [SCRIPT, "status"], { cwd: repo, env: buildEnv(binDir) }); assert.equal(result.status, 0, result.stderr); assert.match(result.stdout, /Session runtime: shared session/); });