1971 lines
63 KiB
JavaScript
1971 lines
63 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("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 <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;
|
|
}, { 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, /<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 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/);
|
|
});
|