codex-plugin-cc/tests/runtime.test.mjs
ZETA a1266348d6
fix: make test suite portable across platforms (#56)
* fix: make test suite portable across platforms

Replace hardcoded macOS path with fileURLToPath for cross-platform
compatibility. Add .cmd wrapper creation and platform-aware PATH
separator in test fixtures so the fake codex binary is discoverable
on Windows. Add shell and windowsHide options to the test helper
run() function to match production behavior.

Test results on Windows improve from 12/64 pass to 59/64 pass.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: skip shell for absolute-path commands to avoid Windows space-in-path breakage

When `process.execPath` resolves to a path with spaces (e.g.,
`C:\Program Files\nodejs\node.exe`), `shell: true` causes cmd.exe
to split the path at the space. Guard with `path.isAbsolute()` so
only bare command names (which need `.cmd` shim resolution) use the
shell.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Dominik Kundel <dkundel@openai.com>
2026-03-31 12:40:08 -07:00

1702 lines
55 KiB
JavaScript

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("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("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("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", "5000", "--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 <ref>/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 <ref>/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`<br>`\/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 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;
});
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, /<task>/i);
assert.match(fakeState.lastTurnStart.prompt, /<compact_output_contract>/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 does not block when Codex is not authenticated even if the review gate is enabled", () => {
const repo = makeTempDir();
const binDir = makeTempDir();
installFakeCodex(binDir, "logged-out");
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.equal(allowed.stdout.trim(), "");
assert.match(allowed.stderr, /Codex is not set up for the review gate/i);
assert.match(allowed.stderr, /not authenticated/i);
assert.match(allowed.stderr, /!codex login/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("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/);
});