chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:39:44 +08:00
commit 93aa9c11ee
64 changed files with 11530 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
import test from "node:test";
import assert from "node:assert/strict";
import { createBrokerEndpoint, parseBrokerEndpoint } from "../plugins/codex/scripts/lib/broker-endpoint.mjs";
test("createBrokerEndpoint uses Unix sockets on non-Windows platforms", () => {
const endpoint = createBrokerEndpoint("/tmp/cxc-12345", "darwin");
assert.equal(endpoint, "unix:/tmp/cxc-12345/broker.sock");
assert.deepEqual(parseBrokerEndpoint(endpoint), {
kind: "unix",
path: "/tmp/cxc-12345/broker.sock"
});
});
test("createBrokerEndpoint uses named pipes on Windows", () => {
const endpoint = createBrokerEndpoint("C:\\\\Temp\\\\cxc-12345", "win32");
assert.equal(endpoint, "pipe:\\\\.\\pipe\\cxc-12345-codex-app-server");
assert.deepEqual(parseBrokerEndpoint(endpoint), {
kind: "pipe",
path: "\\\\.\\pipe\\cxc-12345-codex-app-server"
});
});
+88
View File
@@ -0,0 +1,88 @@
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
import assert from "node:assert/strict";
import { fileURLToPath } from "node:url";
import { makeTempDir, run } from "./helpers.mjs";
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const SCRIPT = path.join(ROOT, "scripts", "bump-version.mjs");
function writeJson(filePath, json) {
fs.mkdirSync(path.dirname(filePath), { recursive: true });
fs.writeFileSync(filePath, `${JSON.stringify(json, null, 2)}\n`);
}
function readJson(filePath) {
return JSON.parse(fs.readFileSync(filePath, "utf8"));
}
function makeVersionFixture() {
const root = makeTempDir();
writeJson(path.join(root, "package.json"), {
name: "@openai/codex-plugin-cc",
version: "1.0.2"
});
writeJson(path.join(root, "package-lock.json"), {
name: "@openai/codex-plugin-cc",
version: "1.0.2",
lockfileVersion: 3,
packages: {
"": {
name: "@openai/codex-plugin-cc",
version: "1.0.2"
}
}
});
writeJson(path.join(root, "plugins", "codex", ".claude-plugin", "plugin.json"), {
name: "codex",
version: "1.0.2"
});
writeJson(path.join(root, ".claude-plugin", "marketplace.json"), {
metadata: {
version: "1.0.2"
},
plugins: [
{
name: "codex",
version: "1.0.2"
}
]
});
return root;
}
test("bump-version updates every release manifest", () => {
const root = makeVersionFixture();
const result = run("node", [SCRIPT, "--root", root, "1.2.3"], {
cwd: ROOT
});
assert.equal(result.status, 0, result.stderr);
assert.equal(readJson(path.join(root, "package.json")).version, "1.2.3");
assert.equal(readJson(path.join(root, "package-lock.json")).version, "1.2.3");
assert.equal(readJson(path.join(root, "package-lock.json")).packages[""].version, "1.2.3");
assert.equal(readJson(path.join(root, "plugins", "codex", ".claude-plugin", "plugin.json")).version, "1.2.3");
assert.equal(readJson(path.join(root, ".claude-plugin", "marketplace.json")).metadata.version, "1.2.3");
assert.equal(readJson(path.join(root, ".claude-plugin", "marketplace.json")).plugins[0].version, "1.2.3");
});
test("bump-version check mode reports stale metadata", () => {
const root = makeVersionFixture();
writeJson(path.join(root, "package.json"), {
name: "@openai/codex-plugin-cc",
version: "1.0.3"
});
const result = run("node", [SCRIPT, "--root", root, "--check"], {
cwd: ROOT
});
assert.notEqual(result.status, 0);
assert.match(result.stderr, /plugins\/codex\/\.claude-plugin\/plugin\.json version/);
assert.match(result.stderr, /\.claude-plugin\/marketplace\.json metadata\.version/);
});
+225
View File
@@ -0,0 +1,225 @@
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
import assert from "node:assert/strict";
import { fileURLToPath } from "node:url";
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const PLUGIN_ROOT = path.join(ROOT, "plugins", "codex");
function read(relativePath) {
return fs.readFileSync(path.join(PLUGIN_ROOT, relativePath), "utf8");
}
test("review command uses AskUserQuestion and background Bash while staying review-only", () => {
const source = read("commands/review.md");
assert.match(source, /AskUserQuestion/);
assert.match(source, /\bBash\(/);
assert.match(source, /Do not fix issues/i);
assert.match(source, /review-only/i);
assert.match(source, /return Codex's output verbatim to the user/i);
assert.match(source, /```bash/);
assert.match(source, /```typescript/);
assert.match(source, /review "\$ARGUMENTS"/);
assert.match(source, /\[--scope auto\|working-tree\|branch\]/);
assert.match(source, /run_in_background:\s*true/);
assert.match(source, /command:\s*`node "\$\{CLAUDE_PLUGIN_ROOT\}\/scripts\/codex-companion\.mjs" review "\$ARGUMENTS"`/);
assert.match(source, /description:\s*"Codex review"/);
assert.match(source, /Do not call `BashOutput`/);
assert.match(source, /Return the command stdout verbatim, exactly as-is/i);
assert.match(source, /git status --short --untracked-files=all/);
assert.match(source, /git diff --shortstat/);
assert.match(source, /Treat untracked files or directories as reviewable work/i);
assert.match(source, /Recommend waiting only when the review is clearly tiny, roughly 1-2 files total/i);
assert.match(source, /In every other case, including unclear size, recommend background/i);
assert.match(source, /The companion script parses `--wait` and `--background`/i);
assert.match(source, /Claude Code's `Bash\(..., run_in_background: true\)` is what actually detaches the run/i);
assert.match(source, /When in doubt, run the review/i);
assert.match(source, /\(Recommended\)/);
assert.match(source, /does not support staged-only review, unstaged-only review, or extra focus text/i);
});
test("adversarial review command uses AskUserQuestion and background Bash while staying review-only", () => {
const source = read("commands/adversarial-review.md");
assert.match(source, /AskUserQuestion/);
assert.match(source, /\bBash\(/);
assert.match(source, /Do not fix issues/i);
assert.match(source, /review-only/i);
assert.match(source, /return Codex's output verbatim to the user/i);
assert.match(source, /```bash/);
assert.match(source, /```typescript/);
assert.match(source, /adversarial-review "\$ARGUMENTS"/);
assert.match(source, /\[--scope auto\|working-tree\|branch\] \[focus \.\.\.\]/);
assert.match(source, /run_in_background:\s*true/);
assert.match(source, /command:\s*`node "\$\{CLAUDE_PLUGIN_ROOT\}\/scripts\/codex-companion\.mjs" adversarial-review "\$ARGUMENTS"`/);
assert.match(source, /description:\s*"Codex adversarial review"/);
assert.match(source, /Do not call `BashOutput`/);
assert.match(source, /Return the command stdout verbatim, exactly as-is/i);
assert.match(source, /git status --short --untracked-files=all/);
assert.match(source, /git diff --shortstat/);
assert.match(source, /Treat untracked files or directories as reviewable work/i);
assert.match(source, /Recommend waiting only when the scoped review is clearly tiny, roughly 1-2 files total/i);
assert.match(source, /In every other case, including unclear size, recommend background/i);
assert.match(source, /The companion script parses `--wait` and `--background`/i);
assert.match(source, /Claude Code's `Bash\(..., run_in_background: true\)` is what actually detaches the run/i);
assert.match(source, /When in doubt, run the review/i);
assert.match(source, /\(Recommended\)/);
assert.match(source, /uses the same review target selection as `\/codex:review`/i);
assert.match(source, /supports working-tree review, branch review, and `--base <ref>`/i);
assert.match(source, /does not support `--scope staged` or `--scope unstaged`/i);
assert.match(source, /can still take extra focus text after the flags/i);
});
test("continue is not exposed as a user-facing command", () => {
const commandFiles = fs.readdirSync(path.join(PLUGIN_ROOT, "commands")).sort();
assert.deepEqual(commandFiles, [
"adversarial-review.md",
"cancel.md",
"rescue.md",
"result.md",
"review.md",
"setup.md",
"status.md",
"transfer.md"
]);
});
test("rescue command absorbs continue semantics", () => {
const rescue = read("commands/rescue.md");
const agent = read("agents/codex-rescue.md");
const readme = fs.readFileSync(path.join(ROOT, "README.md"), "utf8");
const runtimeSkill = read("skills/codex-cli-runtime/SKILL.md");
assert.match(rescue, /The final user-visible response must be Codex's output verbatim/i);
assert.match(rescue, /allowed-tools:\s*Bash\(node:\*\),\s*AskUserQuestion,\s*Agent/);
// Regression for #234: `Skill(codex:rescue)` from the main agent recursed
// because rescue.md named the routing with ambiguous prose ("Route this
// request to the `codex:codex-rescue` subagent") while running under
// `context: fork` — forked general-purpose subagents do not expose the
// `Agent` tool, so the fork fell back to `Skill` and re-entered this
// command. Pin the explicit transport and the inline (no-fork) execution.
assert.match(rescue, /subagent_type: "codex:codex-rescue"/);
assert.match(rescue, /do not call `Skill\(codex:codex-rescue\)`/i);
assert.doesNotMatch(rescue, /^context:\s*fork\b/m);
assert.match(rescue, /--background\|--wait/);
assert.match(rescue, /--resume\|--fresh/);
assert.match(rescue, /--model <model\|spark>/);
assert.match(rescue, /--effort <none\|minimal\|low\|medium\|high\|xhigh>/);
assert.match(rescue, /task-resume-candidate --json/);
assert.match(rescue, /AskUserQuestion/);
assert.match(rescue, /Continue current Codex thread/);
assert.match(rescue, /Start a new Codex thread/);
assert.match(rescue, /run the `codex:codex-rescue` subagent in the background/i);
assert.match(rescue, /default to foreground/i);
assert.match(rescue, /Do not forward them to `task`/i);
assert.match(rescue, /`--model` and `--effort` are runtime-selection flags/i);
assert.match(rescue, /Leave `--effort` unset unless the user explicitly asks for a specific reasoning effort/i);
assert.match(rescue, /If they ask for `spark`, map it to `gpt-5\.3-codex-spark`/i);
assert.match(rescue, /If the request includes `--resume`, do not ask whether to continue/i);
assert.match(rescue, /If the request includes `--fresh`, do not ask whether to continue/i);
assert.match(rescue, /If the user chooses continue, add `--resume`/i);
assert.match(rescue, /If the user chooses a new thread, add `--fresh`/i);
assert.match(rescue, /thin forwarder only/i);
assert.match(rescue, /Return the Codex companion stdout verbatim to the user/i);
assert.match(rescue, /Do not paraphrase, summarize, rewrite, or add commentary before or after it/i);
assert.match(rescue, /return that command's stdout as-is/i);
assert.match(rescue, /Leave `--resume` and `--fresh` in the forwarded request/i);
assert.match(agent, /--resume/);
assert.match(agent, /--fresh/);
assert.match(agent, /thin forwarding wrapper/i);
assert.match(agent, /prefer foreground for a small, clearly bounded rescue request/i);
assert.match(agent, /If the user did not explicitly choose `--background` or `--wait` and the task looks complicated, open-ended, multi-step, or likely to keep Codex running for a long time, prefer background execution/i);
assert.match(agent, /Use exactly one `Bash` call/i);
assert.match(agent, /Do not inspect the repository, read files, grep, monitor progress, poll status, fetch results, cancel jobs, summarize output, or do any follow-up work of your own/i);
assert.match(agent, /Do not call `review`, `adversarial-review`, `status`, `result`, or `cancel`/i);
assert.match(agent, /Leave `--effort` unset unless the user explicitly requests a specific reasoning effort/i);
assert.match(agent, /Leave model unset by default/i);
assert.match(agent, /If the user asks for `spark`, map that to `--model gpt-5\.3-codex-spark`/i);
assert.match(agent, /If the user asks for a concrete model name such as `gpt-5\.4-mini`, pass it through with `--model`/i);
assert.match(agent, /Return the stdout of the `codex-companion` command exactly as-is/i);
assert.match(agent, /If the Bash call fails or Codex cannot be invoked, return nothing/i);
assert.match(agent, /gpt-5-4-prompting/);
assert.match(agent, /only to tighten the user's request into a better Codex prompt/i);
assert.match(agent, /Do not use that skill to inspect the repository, reason through the problem yourself, draft a solution, or do any independent work/i);
assert.match(runtimeSkill, /only job is to invoke `task` once and return that stdout unchanged/i);
assert.match(runtimeSkill, /Do not call `setup`, `review`, `adversarial-review`, `status`, `result`, or `cancel`/i);
assert.match(runtimeSkill, /use the `gpt-5-4-prompting` skill to rewrite the user's request into a tighter Codex prompt/i);
assert.match(runtimeSkill, /That prompt drafting is the only Claude-side work allowed/i);
assert.match(runtimeSkill, /Leave `--effort` unset unless the user explicitly requests a specific effort/i);
assert.match(runtimeSkill, /Leave model unset by default/i);
assert.match(runtimeSkill, /Map `spark` to `--model gpt-5\.3-codex-spark`/i);
assert.match(runtimeSkill, /If the forwarded request includes `--background` or `--wait`, treat that as Claude-side execution control only/i);
assert.match(runtimeSkill, /Strip it before calling `task`/i);
assert.match(runtimeSkill, /`--effort`: accepted values are `none`, `minimal`, `low`, `medium`, `high`, `xhigh`/i);
assert.match(runtimeSkill, /Do not inspect the repository, read files, grep, monitor progress, poll status, fetch results, cancel jobs, summarize output, or do any follow-up work of your own/i);
assert.match(runtimeSkill, /If the Bash call fails or Codex cannot be invoked, return nothing/i);
assert.match(readme, /`codex:codex-rescue` subagent/i);
assert.match(readme, /if you do not pass `--model` or `--effort`, Codex chooses its own defaults/i);
assert.match(readme, /--model gpt-5\.4-mini --effort medium/i);
assert.match(readme, /`spark`, the plugin maps that to `gpt-5\.3-codex-spark`/i);
assert.match(readme, /continue a previous Codex task/i);
assert.match(readme, /### `\/codex:setup`/);
assert.match(readme, /### `\/codex:review`/);
assert.match(readme, /### `\/codex:adversarial-review`/);
assert.match(readme, /uses the same review target selection as `\/codex:review`/i);
assert.match(readme, /--base main challenge whether this was the right caching and retry design/);
assert.match(readme, /### `\/codex:rescue`/);
assert.match(readme, /### `\/codex:transfer`/);
assert.match(readme, /### `\/codex:status`/);
assert.match(readme, /### `\/codex:result`/);
assert.match(readme, /### `\/codex:cancel`/);
});
test("transfer, result, and cancel commands are exposed as deterministic runtime entrypoints", () => {
const transfer = read("commands/transfer.md");
const result = read("commands/result.md");
const cancel = read("commands/cancel.md");
const resultHandling = read("skills/codex-result-handling/SKILL.md");
assert.match(transfer, /disable-model-invocation:\s*true/);
assert.match(transfer, /codex-companion\.mjs" transfer "\$ARGUMENTS"/);
assert.match(transfer, /codex resume <session-id>/);
assert.match(result, /disable-model-invocation:\s*true/);
assert.match(result, /codex-companion\.mjs" result "\$ARGUMENTS"/);
assert.match(cancel, /disable-model-invocation:\s*true/);
assert.match(cancel, /codex-companion\.mjs" cancel "\$ARGUMENTS"/);
assert.match(resultHandling, /do not turn a failed or incomplete Codex run into a Claude-side implementation attempt/i);
assert.match(resultHandling, /if Codex was never successfully invoked, do not generate a substitute answer at all/i);
});
test("internal docs use task terminology for rescue runs", () => {
const runtimeSkill = read("skills/codex-cli-runtime/SKILL.md");
const promptingSkill = read("skills/gpt-5-4-prompting/SKILL.md");
const promptRecipes = read("skills/gpt-5-4-prompting/references/codex-prompt-recipes.md");
assert.match(runtimeSkill, /codex-companion\.mjs" task "<raw arguments>"/);
assert.match(runtimeSkill, /Use `task` for every rescue request/i);
assert.match(runtimeSkill, /task --resume-last/i);
assert.match(promptingSkill, /Use `task` when the task is diagnosis/i);
assert.match(promptRecipes, /Codex task prompts/i);
assert.match(promptRecipes, /Use these as starting templates for Codex task prompts/i);
assert.match(promptRecipes, /## Diagnosis/);
assert.match(promptRecipes, /## Narrow Fix/);
});
test("hooks keep session-end cleanup and stop gating enabled", () => {
const source = read("hooks/hooks.json");
assert.match(source, /SessionStart/);
assert.match(source, /SessionEnd/);
assert.match(source, /stop-review-gate-hook\.mjs/);
assert.match(source, /session-lifecycle-hook\.mjs/);
});
test("setup command can offer Codex install and still points users to codex login", () => {
const setup = read("commands/setup.md");
const readme = fs.readFileSync(path.join(ROOT, "README.md"), "utf8");
assert.match(setup, /argument-hint:\s*'\[--enable-review-gate\|--disable-review-gate\]'/);
assert.match(setup, /AskUserQuestion/);
assert.match(setup, /npm install -g @openai\/codex/);
assert.match(setup, /codex-companion\.mjs" setup --json \$ARGUMENTS/);
assert.match(readme, /!codex login/);
assert.match(readme, /offer to install Codex for you/i);
assert.match(readme, /\/codex:setup --enable-review-gate/);
assert.match(readme, /\/codex:setup --disable-review-gate/);
});
+658
View File
@@ -0,0 +1,658 @@
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import { writeExecutable } from "./helpers.mjs";
export function installFakeCodex(binDir, behavior = "review-ok") {
const statePath = path.join(binDir, "fake-codex-state.json");
const scriptPath = path.join(binDir, "codex");
const source = `#!/usr/bin/env node
const fs = require("node:fs");
const crypto = require("node:crypto");
const path = require("node:path");
const readline = require("node:readline");
const STATE_PATH = ${JSON.stringify(statePath)};
const BEHAVIOR = ${JSON.stringify(behavior)};
const interruptibleTurns = new Map();
function loadState() {
if (!fs.existsSync(STATE_PATH)) {
return { nextThreadId: 1, nextTurnId: 1, appServerStarts: 0, threads: [], capabilities: null, lastInterrupt: null };
}
return JSON.parse(fs.readFileSync(STATE_PATH, "utf8"));
}
function saveState(state) {
fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
}
function requiresExperimental(field, message, state) {
if (!(field in (message.params || {}))) {
return false;
}
return !state.capabilities || state.capabilities.experimentalApi !== true;
}
function now() {
return Math.floor(Date.now() / 1000);
}
function buildThread(thread) {
return {
id: thread.id,
preview: thread.preview || "",
ephemeral: Boolean(thread.ephemeral),
modelProvider: "openai",
createdAt: thread.createdAt,
updatedAt: thread.updatedAt,
status: { type: "idle" },
path: null,
cwd: thread.cwd,
cliVersion: "fake-codex",
source: "appServer",
agentNickname: null,
agentRole: null,
gitInfo: null,
name: thread.name || null,
turns: []
};
}
function buildTurn(id, status = "inProgress", error = null) {
return { id, status, items: [], error };
}
function buildAccountReadResult() {
switch (BEHAVIOR) {
case "logged-out":
case "refreshable-auth":
case "auth-run-fails":
return { account: null, requiresOpenaiAuth: true };
case "provider-no-auth":
case "env-key-provider":
return { account: null, requiresOpenaiAuth: false };
case "api-key-account-only":
return { account: { type: "apiKey" }, requiresOpenaiAuth: true };
default:
return {
account: { type: "chatgpt", email: "test@example.com", planType: "plus" },
requiresOpenaiAuth: true
};
}
}
function buildConfigReadResult() {
switch (BEHAVIOR) {
case "provider-no-auth":
return {
config: { model_provider: "ollama" },
origins: {}
};
case "env-key-provider":
return {
config: {
model_provider: "openai-custom",
model_providers: {
"openai-custom": {
name: "OpenAI custom",
env_key: "OPENAI_API_KEY",
requires_openai_auth: false
}
}
},
origins: {}
};
default:
return {
config: { model_provider: "openai" },
origins: {}
};
}
}
function send(message) {
process.stdout.write(JSON.stringify(message) + "\\n");
}
function nextThread(state, cwd, ephemeral) {
const thread = {
id: "thr_" + state.nextThreadId++,
cwd: cwd || process.cwd(),
name: null,
preview: "",
ephemeral: Boolean(ephemeral),
createdAt: now(),
updatedAt: now()
};
state.threads.unshift(thread);
saveState(state);
return thread;
}
function ensureThread(state, threadId) {
const thread = state.threads.find((candidate) => candidate.id === threadId);
if (!thread) {
throw new Error("unknown thread " + threadId);
}
return thread;
}
function nextTurnId(state) {
const turnId = "turn_" + state.nextTurnId++;
saveState(state);
return turnId;
}
function importLedgerPath() {
return path.join(process.env.CODEX_HOME || path.join(process.env.HOME, ".codex"), "external_agent_session_imports.json");
}
function loadImportLedger() {
const ledgerPath = importLedgerPath();
return fs.existsSync(ledgerPath) ? JSON.parse(fs.readFileSync(ledgerPath, "utf8")) : { records: [] };
}
function saveImportLedger(ledger) {
const ledgerPath = importLedgerPath();
fs.mkdirSync(path.dirname(ledgerPath), { recursive: true });
fs.writeFileSync(ledgerPath, JSON.stringify(ledger, null, 2));
}
function emitTurnCompleted(threadId, turnId, item) {
const items = Array.isArray(item) ? item : [item];
send({ method: "turn/started", params: { threadId, turn: buildTurn(turnId) } });
for (const entry of items) {
if (entry && entry.started) {
send({ method: "item/started", params: { threadId, turnId, item: entry.started } });
}
if (entry && entry.completed) {
send({ method: "item/completed", params: { threadId, turnId, item: entry.completed } });
}
}
send({ method: "turn/completed", params: { threadId, turn: buildTurn(turnId, "completed") } });
}
function emitTurnCompletedLater(threadId, turnId, item, delayMs) {
setTimeout(() => {
emitTurnCompleted(threadId, turnId, item);
}, delayMs);
}
function nativeReviewText(target) {
if (target.type === "baseBranch") {
return "Reviewed changes against " + target.branch + ".\\nNo material issues found.";
}
if (target.type === "custom") {
return "Reviewed custom target.\\nNo material issues found.";
}
return "Reviewed uncommitted changes.\\nNo material issues found.";
}
function structuredReviewPayload(prompt) {
if (prompt.includes("adversarial software review")) {
if (BEHAVIOR === "adversarial-clean") {
return JSON.stringify({
verdict: "approve",
summary: "No material issues found.",
findings: [],
next_steps: []
});
}
return JSON.stringify({
verdict: "needs-attention",
summary: "One adversarial concern surfaced.",
findings: [
{
severity: "high",
title: "Missing empty-state guard",
body: "The change assumes data is always present.",
file: "src/app.js",
line_start: 4,
line_end: 6,
confidence: 0.87,
recommendation: "Handle empty collections before indexing."
}
],
next_steps: ["Add an empty-state test."]
});
}
if (BEHAVIOR === "invalid-json") {
return "not valid json";
}
return JSON.stringify({
verdict: "approve",
summary: "No material issues found.",
findings: [],
next_steps: []
});
}
function taskPayload(prompt, resume) {
if (prompt.includes("<task>") && prompt.includes("Only review the work from the previous Claude turn.")) {
if (BEHAVIOR === "adversarial-clean") {
return "ALLOW: No blocking issues found in the previous turn.";
}
return "BLOCK: Missing empty-state guard in src/app.js:4-6.";
}
if (resume || prompt.includes("Continue from the current thread state") || prompt.includes("follow up")) {
return "Resumed the prior run.\\nFollow-up prompt accepted.";
}
return "Handled the requested task.\\nTask prompt accepted.";
}
const args = process.argv.slice(2);
if (args[0] === "--version") {
console.log("codex-cli test");
process.exit(0);
}
if (args[0] === "app-server" && args[1] === "--help") {
console.log("fake app-server help");
process.exit(0);
}
if (args[0] === "login" && args[1] === "status") {
if (BEHAVIOR === "logged-out" || BEHAVIOR === "refreshable-auth" || BEHAVIOR === "auth-run-fails" || BEHAVIOR === "provider-no-auth" || BEHAVIOR === "env-key-provider" || BEHAVIOR === "api-key-account-only") {
console.error("not authenticated");
process.exit(1);
}
console.log("logged in");
process.exit(0);
}
if (args[0] === "login") {
process.exit(0);
}
if (args[0] !== "app-server") {
process.exit(1);
}
const bootState = loadState();
bootState.appServerStarts = (bootState.appServerStarts || 0) + 1;
saveState(bootState);
const rl = readline.createInterface({ input: process.stdin });
rl.on("line", (line) => {
if (!line.trim()) {
return;
}
const message = JSON.parse(line);
const state = loadState();
try {
switch (message.method) {
case "initialize":
state.capabilities = message.params.capabilities || null;
saveState(state);
send({ id: message.id, result: { userAgent: "fake-codex-app-server" } });
break;
case "initialized":
break;
case "account/read":
send({ id: message.id, result: buildAccountReadResult() });
break;
case "config/read":
if (BEHAVIOR === "config-read-fails") {
throw new Error("config/read failed for cwd");
}
send({ id: message.id, result: buildConfigReadResult() });
break;
case "thread/start": {
if (BEHAVIOR === "auth-run-fails") {
throw new Error("authentication expired; run codex login");
}
if (requiresExperimental("persistExtendedHistory", message, state) || requiresExperimental("persistFullHistory", message, state)) {
throw new Error("thread/start.persistFullHistory requires experimentalApi capability");
}
const thread = nextThread(state, message.params.cwd, message.params.ephemeral);
send({ id: message.id, result: { thread: buildThread(thread), model: message.params.model || "gpt-5.4", modelProvider: "openai", serviceTier: null, cwd: thread.cwd, approvalPolicy: "never", sandbox: { type: "readOnly", access: { type: "fullAccess" }, networkAccess: false }, reasoningEffort: null } });
send({ method: "thread/started", params: { thread: { id: thread.id } } });
break;
}
case "thread/name/set": {
const thread = ensureThread(state, message.params.threadId);
thread.name = message.params.name;
thread.updatedAt = now();
saveState(state);
send({ id: message.id, result: {} });
break;
}
case "thread/list": {
let threads = state.threads.slice();
if (message.params.cwd) {
threads = threads.filter((thread) => thread.cwd === message.params.cwd);
}
if (message.params.searchTerm) {
threads = threads.filter((thread) => (thread.name || "").includes(message.params.searchTerm));
}
threads.sort((left, right) => right.updatedAt - left.updatedAt);
send({ id: message.id, result: { data: threads.map(buildThread), nextCursor: null } });
break;
}
case "thread/resume": {
if (requiresExperimental("persistExtendedHistory", message, state) || requiresExperimental("persistFullHistory", message, state)) {
throw new Error("thread/resume.persistFullHistory requires experimentalApi capability");
}
const thread = ensureThread(state, message.params.threadId);
thread.updatedAt = now();
saveState(state);
send({ id: message.id, result: { thread: buildThread(thread), model: message.params.model || "gpt-5.4", modelProvider: "openai", serviceTier: null, cwd: thread.cwd, approvalPolicy: "never", sandbox: { type: "readOnly", access: { type: "fullAccess" }, networkAccess: false }, reasoningEffort: null } });
break;
}
case "externalAgentConfig/import": {
if (BEHAVIOR === "external-import-unsupported") {
send({ id: message.id, error: { code: -32601, message: "Unsupported method: externalAgentConfig/import" } });
break;
}
if (BEHAVIOR === "external-import-fails") {
send({ id: message.id, result: {} });
send({ method: "externalAgentConfig/import/completed", params: {} });
break;
}
const sessions = (message.params.migrationItems || [])
.flatMap((item) => item.details && Array.isArray(item.details.sessions) ? item.details.sessions : []);
const session = sessions[0];
if (!session) {
throw new Error("missing external session migration");
}
const sourcePath = fs.realpathSync(session.path);
const contents = fs.readFileSync(sourcePath, "utf8");
const contentSha256 = crypto.createHash("sha256").update(contents).digest("hex");
const ledger = loadImportLedger();
let record = ledger.records.find(
(candidate) => candidate.source_path === sourcePath && candidate.content_sha256 === contentSha256
);
let thread;
if (record) {
thread = ensureThread(state, record.imported_thread_id);
} else {
const records = contents.split(/\\r?\\n/).filter(Boolean).map((line) => JSON.parse(line));
const title = records.find((entry) => entry.type === "custom-title")?.customTitle || null;
const messages = records
.filter((entry) => entry.type === "user" || entry.type === "assistant")
.map((entry) => ({ role: entry.type, text: entry.message?.content || "" }));
thread = nextThread(state, session.cwd, false);
thread.name = title;
thread.preview = messages.find((entry) => entry.role === "user")?.text || "";
thread.visibleMessages = messages;
state.lastExternalAgentImport = { sourcePath, threadId: thread.id, messages };
record = {
source_path: sourcePath,
content_sha256: contentSha256,
imported_thread_id: thread.id,
imported_at: now(),
source_modified_at: null
};
ledger.records.push(record);
saveState(state);
saveImportLedger(ledger);
}
send({ id: message.id, result: {} });
send({ method: "externalAgentConfig/import/completed", params: {} });
break;
}
case "review/start": {
const thread = ensureThread(state, message.params.threadId);
let reviewThread = thread;
if (message.params.delivery === "detached") {
reviewThread = nextThread(state, thread.cwd, true);
send({ method: "thread/started", params: { thread: { id: reviewThread.id } } });
}
const turnId = nextTurnId(state);
send({ id: message.id, result: { turn: buildTurn(turnId), reviewThreadId: reviewThread.id } });
emitTurnCompleted(reviewThread.id, turnId, [
{
started: { type: "enteredReviewMode", id: turnId, review: "current changes" }
},
...(BEHAVIOR === "with-reasoning"
? [
{
completed: {
type: "reasoning",
id: "reasoning_" + turnId,
summary: [{ text: "Reviewed the changed files and checked the likely regression paths." }],
content: []
}
}
]
: []),
{
completed: { type: "exitedReviewMode", id: turnId, review: nativeReviewText(message.params.target) }
}
]);
break;
}
case "turn/start": {
const thread = ensureThread(state, message.params.threadId);
const prompt = (message.params.input || [])
.filter((item) => item.type === "text")
.map((item) => item.text)
.join("\\n");
const turnId = nextTurnId(state);
thread.updatedAt = now();
state.lastTurnStart = {
threadId: message.params.threadId,
turnId,
model: message.params.model ?? null,
effort: message.params.effort ?? null,
prompt
};
saveState(state);
send({ id: message.id, result: { turn: buildTurn(turnId) } });
const payload = message.params.outputSchema && message.params.outputSchema.properties && message.params.outputSchema.properties.verdict
? structuredReviewPayload(prompt)
: taskPayload(prompt, thread.name && thread.name.startsWith("Codex Companion Task") && prompt.includes("Continue from the current thread state"));
if (
BEHAVIOR === "with-subagent" ||
BEHAVIOR === "with-late-subagent-message" ||
BEHAVIOR === "with-subagent-no-main-turn-completed"
) {
const subThread = nextThread(state, thread.cwd, true);
const subThreadRecord = ensureThread(state, subThread.id);
subThreadRecord.name = "design-challenger";
saveState(state);
const subTurnId = nextTurnId(state);
send({ method: "thread/started", params: { thread: { ...buildThread(subThreadRecord), name: "design-challenger", agentNickname: "design-challenger" } } });
send({ method: "turn/started", params: { threadId: thread.id, turn: buildTurn(turnId) } });
send({
method: "item/started",
params: {
threadId: thread.id,
turnId,
item: {
type: "collabAgentToolCall",
id: "collab_" + turnId,
tool: "wait",
status: "inProgress",
senderThreadId: thread.id,
receiverThreadIds: [subThread.id],
prompt: "Challenge the implementation approach",
model: null,
reasoningEffort: null,
agentsStates: {
[subThread.id]: { status: "inProgress", message: "Investigating design tradeoffs" }
}
}
}
});
if (BEHAVIOR === "with-late-subagent-message") {
send({
method: "item/completed",
params: {
threadId: thread.id,
turnId,
item: { type: "agentMessage", id: "msg_" + turnId, text: payload, phase: "final_answer" }
}
});
}
send({ method: "turn/started", params: { threadId: subThread.id, turn: buildTurn(subTurnId) } });
send({
method: "item/completed",
params: {
threadId: subThread.id,
turnId: subTurnId,
item: {
type: "reasoning",
id: "reasoning_" + subTurnId,
summary: [{ text: "Questioned the retry strategy and the cache invalidation boundaries." }],
content: []
}
}
});
send({
method: "item/completed",
params: {
threadId: subThread.id,
turnId: subTurnId,
item: {
type: "agentMessage",
id: "msg_" + subTurnId,
text: "The design assumes retries are harmless, but they can duplicate side effects without stronger idempotency guarantees.",
phase: "analysis"
}
}
});
send({ method: "turn/completed", params: { threadId: subThread.id, turn: buildTurn(subTurnId, "completed") } });
send({
method: "item/completed",
params: {
threadId: thread.id,
turnId,
item: {
type: "collabAgentToolCall",
id: "collab_" + turnId,
tool: "wait",
status: "completed",
senderThreadId: thread.id,
receiverThreadIds: [subThread.id],
prompt: "Challenge the implementation approach",
model: null,
reasoningEffort: null,
agentsStates: {
[subThread.id]: { status: "completed", message: "Finished" }
}
}
}
});
if (BEHAVIOR !== "with-late-subagent-message") {
send({
method: "item/completed",
params: {
threadId: thread.id,
turnId,
item: { type: "agentMessage", id: "msg_" + turnId, text: payload, phase: "final_answer" }
}
});
}
if (BEHAVIOR !== "with-subagent-no-main-turn-completed") {
send({ method: "turn/completed", params: { threadId: thread.id, turn: buildTurn(turnId, "completed") } });
}
break;
}
const items = [
...(BEHAVIOR === "with-reasoning"
? [
{
completed: {
type: "reasoning",
id: "reasoning_" + turnId,
summary: [{ text: "Inspected the prompt, gathered evidence, and checked the highest-risk paths first." }],
content: []
}
}
]
: []),
{
completed: { type: "agentMessage", id: "msg_" + turnId, text: payload, phase: "final_answer" }
}
];
if (BEHAVIOR === "interruptible-slow-task") {
send({ method: "turn/started", params: { threadId: thread.id, turn: buildTurn(turnId) } });
const timer = setTimeout(() => {
if (!interruptibleTurns.has(turnId)) {
return;
}
interruptibleTurns.delete(turnId);
for (const entry of items) {
if (entry && entry.completed) {
send({ method: "item/completed", params: { threadId: thread.id, turnId, item: entry.completed } });
}
}
send({ method: "turn/completed", params: { threadId: thread.id, turn: buildTurn(turnId, "completed") } });
}, 5000);
interruptibleTurns.set(turnId, { threadId: thread.id, timer });
} else if (BEHAVIOR === "slow-task") {
emitTurnCompletedLater(thread.id, turnId, items, 400);
} else {
emitTurnCompleted(thread.id, turnId, items);
}
break;
}
case "turn/interrupt": {
state.lastInterrupt = {
threadId: message.params.threadId,
turnId: message.params.turnId
};
saveState(state);
const pending = interruptibleTurns.get(message.params.turnId);
if (pending) {
clearTimeout(pending.timer);
interruptibleTurns.delete(message.params.turnId);
send({
method: "turn/completed",
params: {
threadId: pending.threadId,
turn: buildTurn(message.params.turnId, "interrupted")
}
});
}
send({ id: message.id, result: {} });
break;
}
default:
send({ id: message.id, error: { code: -32601, message: "Unsupported method: " + message.method } });
break;
}
} catch (error) {
send({ id: message.id, error: { code: -32000, message: error.message } });
}
});
`;
writeExecutable(scriptPath, source);
// On Windows, npm global binaries are invoked via .cmd wrappers.
// Create a codex.cmd so the fake binary is discoverable by spawn with shell: true.
if (process.platform === "win32") {
const cmdWrapper = `@echo off\r\nnode "%~dp0codex" %*\r\n`;
fs.writeFileSync(path.join(binDir, "codex.cmd"), cmdWrapper, { encoding: "utf8" });
}
}
export function buildEnv(binDir) {
const sep = process.platform === "win32" ? ";" : ":";
return {
...process.env,
PATH: `${binDir}${sep}${process.env.PATH}`
};
}
+212
View File
@@ -0,0 +1,212 @@
import fs from "node:fs";
import path from "node:path";
import test from "node:test";
import assert from "node:assert/strict";
import { collectReviewContext, resolveReviewTarget } from "../plugins/codex/scripts/lib/git.mjs";
import { initGitRepo, makeTempDir, run } from "./helpers.mjs";
test("resolveReviewTarget prefers working tree when repo is dirty", () => {
const cwd = makeTempDir();
initGitRepo(cwd);
fs.writeFileSync(path.join(cwd, "app.js"), "console.log('v1');\n");
run("git", ["add", "app.js"], { cwd });
run("git", ["commit", "-m", "init"], { cwd });
fs.writeFileSync(path.join(cwd, "app.js"), "console.log('v2');\n");
const target = resolveReviewTarget(cwd, {});
assert.equal(target.mode, "working-tree");
});
test("resolveReviewTarget falls back to branch diff when repo is clean", () => {
const cwd = makeTempDir();
initGitRepo(cwd);
fs.writeFileSync(path.join(cwd, "app.js"), "console.log('v1');\n");
run("git", ["add", "app.js"], { cwd });
run("git", ["commit", "-m", "init"], { cwd });
run("git", ["checkout", "-b", "feature/test"], { cwd });
fs.writeFileSync(path.join(cwd, "app.js"), "console.log('v2');\n");
run("git", ["add", "app.js"], { cwd });
run("git", ["commit", "-m", "change"], { cwd });
const target = resolveReviewTarget(cwd, {});
const context = collectReviewContext(cwd, target);
assert.equal(target.mode, "branch");
assert.match(target.label, /main/);
assert.match(context.content, /Branch Diff/);
});
test("default branch names with special characters are passed to git literally", () => {
const cwd = makeTempDir();
const branchName = "main&branch-helper&x";
const helperOutputPath = path.join(cwd, "branch-helper-output");
initGitRepo(cwd);
fs.writeFileSync(path.join(cwd, "branch-helper.cmd"), "@echo branch-helper>branch-helper-output\r\n");
fs.writeFileSync(path.join(cwd, "app.js"), "console.log('base');\n");
run("git", ["add", "app.js", "branch-helper.cmd"], { cwd });
run("git", ["commit", "-m", "base"], { cwd });
run("git", ["branch", "-m", branchName], { cwd, shell: false });
run("git", ["update-ref", `refs/remotes/origin/${branchName}`, branchName], { cwd, shell: false });
run("git", ["symbolic-ref", "refs/remotes/origin/HEAD", `refs/remotes/origin/${branchName}`], {
cwd,
shell: false
});
run("git", ["checkout", "-b", "feature/test"], { cwd });
fs.writeFileSync(path.join(cwd, "app.js"), "console.log('feature');\n");
run("git", ["add", "app.js"], { cwd });
run("git", ["commit", "-m", "feature"], { cwd });
const target = resolveReviewTarget(cwd, {});
const context = collectReviewContext(cwd, target);
assert.equal(target.mode, "branch");
assert.equal(target.baseRef, branchName);
assert.match(context.content, /Branch Diff/);
assert.equal(fs.existsSync(helperOutputPath), false);
});
test("resolveReviewTarget honors explicit base overrides", () => {
const cwd = makeTempDir();
initGitRepo(cwd);
fs.writeFileSync(path.join(cwd, "app.js"), "console.log('v1');\n");
run("git", ["add", "app.js"], { cwd });
run("git", ["commit", "-m", "init"], { cwd });
run("git", ["checkout", "-b", "feature/test"], { cwd });
fs.writeFileSync(path.join(cwd, "app.js"), "console.log('v2');\n");
run("git", ["add", "app.js"], { cwd });
run("git", ["commit", "-m", "change"], { cwd });
const target = resolveReviewTarget(cwd, { base: "main" });
assert.equal(target.mode, "branch");
assert.equal(target.baseRef, "main");
});
test("resolveReviewTarget requires an explicit base when no default branch can be inferred", () => {
const cwd = makeTempDir();
initGitRepo(cwd);
fs.writeFileSync(path.join(cwd, "app.js"), "console.log('v1');\n");
run("git", ["add", "app.js"], { cwd });
run("git", ["commit", "-m", "init"], { cwd });
run("git", ["branch", "-m", "feature-only"], { cwd });
assert.throws(
() => resolveReviewTarget(cwd, {}),
/Unable to detect the repository default branch\. Pass --base <ref> or use --scope working-tree\./
);
});
test("collectReviewContext keeps inline diffs for tiny adversarial reviews", () => {
const cwd = makeTempDir();
initGitRepo(cwd);
fs.writeFileSync(path.join(cwd, "app.js"), "console.log('v1');\n");
run("git", ["add", "app.js"], { cwd });
run("git", ["commit", "-m", "init"], { cwd });
fs.writeFileSync(path.join(cwd, "app.js"), "console.log('INLINE_MARKER');\n");
const target = resolveReviewTarget(cwd, {});
const context = collectReviewContext(cwd, target);
assert.equal(context.inputMode, "inline-diff");
assert.equal(context.fileCount, 1);
assert.match(context.collectionGuidance, /primary evidence/i);
assert.match(context.content, /INLINE_MARKER/);
});
test("collectReviewContext skips untracked directories in working tree review", () => {
const cwd = makeTempDir();
initGitRepo(cwd);
fs.writeFileSync(path.join(cwd, "app.js"), "console.log('v1');\n");
run("git", ["add", "app.js"], { cwd });
run("git", ["commit", "-m", "init"], { cwd });
const nestedRepoDir = path.join(cwd, ".claude", "worktrees", "agent-test");
fs.mkdirSync(nestedRepoDir, { recursive: true });
initGitRepo(nestedRepoDir);
const target = resolveReviewTarget(cwd, { scope: "working-tree" });
const context = collectReviewContext(cwd, target);
assert.match(context.content, /### \.claude\/worktrees\/agent-test\/\n\(skipped: directory\)/);
});
test("collectReviewContext skips broken untracked symlinks instead of crashing", () => {
const cwd = makeTempDir();
initGitRepo(cwd);
fs.writeFileSync(path.join(cwd, "app.js"), "console.log('v1');\n");
run("git", ["add", "app.js"], { cwd });
run("git", ["commit", "-m", "init"], { cwd });
fs.symlinkSync("missing-target", path.join(cwd, "broken-link"));
const target = resolveReviewTarget(cwd, {});
const context = collectReviewContext(cwd, target);
assert.equal(target.mode, "working-tree");
assert.match(context.content, /### broken-link/);
assert.match(context.content, /skipped: broken symlink or unreadable file/i);
});
test("collectReviewContext falls back to lightweight context for larger adversarial reviews", () => {
const cwd = makeTempDir();
initGitRepo(cwd);
for (const name of ["a.js", "b.js", "c.js"]) {
fs.writeFileSync(path.join(cwd, name), `export const value = "${name}-v1";\n`);
}
run("git", ["add", "a.js", "b.js", "c.js"], { cwd });
run("git", ["commit", "-m", "init"], { cwd });
fs.writeFileSync(path.join(cwd, "a.js"), 'export const value = "SELF_COLLECT_MARKER_A";\n');
fs.writeFileSync(path.join(cwd, "b.js"), 'export const value = "SELF_COLLECT_MARKER_B";\n');
fs.writeFileSync(path.join(cwd, "c.js"), 'export const value = "SELF_COLLECT_MARKER_C";\n');
const target = resolveReviewTarget(cwd, {});
const context = collectReviewContext(cwd, target);
assert.equal(context.inputMode, "self-collect");
assert.equal(context.fileCount, 3);
assert.match(context.collectionGuidance, /lightweight summary/i);
assert.match(context.collectionGuidance, /read-only git commands/i);
assert.doesNotMatch(context.content, /SELF_COLLECT_MARKER_[ABC]/);
assert.match(context.content, /## Changed Files/);
});
test("collectReviewContext falls back to lightweight context for oversized single-file diffs", () => {
const cwd = makeTempDir();
initGitRepo(cwd);
fs.writeFileSync(path.join(cwd, "app.js"), "export const value = 'v1';\n");
run("git", ["add", "app.js"], { cwd });
run("git", ["commit", "-m", "init"], { cwd });
fs.writeFileSync(path.join(cwd, "app.js"), `export const value = '${"x".repeat(512)}';\n`);
const target = resolveReviewTarget(cwd, {});
const context = collectReviewContext(cwd, target, { maxInlineDiffBytes: 128 });
assert.equal(context.fileCount, 1);
assert.equal(context.inputMode, "self-collect");
assert.ok(context.diffBytes > 128);
assert.doesNotMatch(context.content, /xxx/);
assert.match(context.content, /## Changed Files/);
});
test("collectReviewContext keeps untracked file content in lightweight working tree context", () => {
const cwd = makeTempDir();
initGitRepo(cwd);
for (const name of ["a.js", "b.js"]) {
fs.writeFileSync(path.join(cwd, name), `export const value = "${name}-v1";\n`);
}
run("git", ["add", "a.js", "b.js"], { cwd });
run("git", ["commit", "-m", "init"], { cwd });
fs.writeFileSync(path.join(cwd, "a.js"), 'export const value = "TRACKED_MARKER_A";\n');
fs.writeFileSync(path.join(cwd, "b.js"), 'export const value = "TRACKED_MARKER_B";\n');
fs.writeFileSync(path.join(cwd, "new-risk.js"), 'export const value = "UNTRACKED_RISK_MARKER";\n');
const target = resolveReviewTarget(cwd, {});
const context = collectReviewContext(cwd, target);
assert.equal(context.inputMode, "self-collect");
assert.equal(context.fileCount, 3);
assert.doesNotMatch(context.content, /TRACKED_MARKER_[AB]/);
assert.match(context.content, /## Untracked Files/);
assert.match(context.content, /UNTRACKED_RISK_MARKER/);
});
+32
View File
@@ -0,0 +1,32 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import process from "node:process";
import { spawnSync } from "node:child_process";
export function makeTempDir(prefix = "codex-plugin-test-") {
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
}
export function writeExecutable(filePath, source) {
fs.writeFileSync(filePath, source, { encoding: "utf8", mode: 0o755 });
}
export function run(command, args, options = {}) {
return spawnSync(command, args, {
cwd: options.cwd,
env: options.env,
encoding: "utf8",
input: options.input,
shell: options.shell ?? (process.platform === "win32" && !path.isAbsolute(command)),
windowsHide: true
});
}
export function initGitRepo(cwd) {
run("git", ["init", "-b", "main"], { cwd });
run("git", ["config", "user.name", "Codex Plugin Tests"], { cwd });
run("git", ["config", "user.email", "tests@example.com"], { cwd });
run("git", ["config", "commit.gpgsign", "false"], { cwd });
run("git", ["config", "tag.gpgsign", "false"], { cwd });
}
+55
View File
@@ -0,0 +1,55 @@
import test from "node:test";
import assert from "node:assert/strict";
import { terminateProcessTree } from "../plugins/codex/scripts/lib/process.mjs";
test("terminateProcessTree uses taskkill on Windows", () => {
let captured = null;
const outcome = terminateProcessTree(1234, {
platform: "win32",
runCommandImpl(command, args) {
captured = { command, args };
return {
command,
args,
status: 0,
signal: null,
stdout: "",
stderr: "",
error: null
};
},
killImpl() {
throw new Error("kill fallback should not run");
}
});
assert.deepEqual(captured, {
command: "taskkill",
args: ["/PID", "1234", "/T", "/F"]
});
assert.equal(outcome.delivered, true);
assert.equal(outcome.method, "taskkill");
});
test("terminateProcessTree treats missing Windows processes as already stopped", () => {
const outcome = terminateProcessTree(1234, {
platform: "win32",
runCommandImpl(command, args) {
return {
command,
args,
status: 128,
signal: null,
stdout: "ERROR: The process \"1234\" not found.",
stderr: "",
error: null
};
}
});
assert.equal(outcome.attempted, true);
assert.equal(outcome.method, "taskkill");
assert.equal(outcome.result.status, 128);
assert.match(outcome.result.stdout, /not found/i);
});
+59
View File
@@ -0,0 +1,59 @@
import test from "node:test";
import assert from "node:assert/strict";
import { renderReviewResult, renderStoredJobResult } from "../plugins/codex/scripts/lib/render.mjs";
test("renderReviewResult degrades gracefully when JSON is missing required review fields", () => {
const output = renderReviewResult(
{
parsed: {
verdict: "approve",
summary: "Looks fine."
},
rawOutput: JSON.stringify({
verdict: "approve",
summary: "Looks fine."
}),
parseError: null
},
{
reviewLabel: "Adversarial Review",
targetLabel: "working tree diff"
}
);
assert.match(output, /Codex returned JSON with an unexpected review shape\./);
assert.match(output, /Missing array `findings`\./);
assert.match(output, /Raw final message:/);
});
test("renderStoredJobResult prefers rendered output for structured review jobs", () => {
const output = renderStoredJobResult(
{
id: "review-123",
status: "completed",
title: "Codex Adversarial Review",
jobClass: "review",
threadId: "thr_123"
},
{
threadId: "thr_123",
rendered: "# Codex Adversarial Review\n\nTarget: working tree diff\nVerdict: needs-attention\n",
result: {
result: {
verdict: "needs-attention",
summary: "One issue.",
findings: [],
next_steps: []
},
rawOutput:
'{"verdict":"needs-attention","summary":"One issue.","findings":[],"next_steps":[]}'
}
}
);
assert.match(output, /^# Codex Adversarial Review/);
assert.doesNotMatch(output, /^\{/);
assert.match(output, /Codex session ID: thr_123/);
assert.match(output, /Resume in Codex: codex resume thr_123/);
});
File diff suppressed because it is too large Load Diff
+105
View File
@@ -0,0 +1,105 @@
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import test from "node:test";
import assert from "node:assert/strict";
import { makeTempDir } from "./helpers.mjs";
import { resolveJobFile, resolveJobLogFile, resolveStateDir, resolveStateFile, saveState } from "../plugins/codex/scripts/lib/state.mjs";
test("resolveStateDir uses a temp-backed per-workspace directory", () => {
const workspace = makeTempDir();
const stateDir = resolveStateDir(workspace);
assert.equal(stateDir.startsWith(os.tmpdir()), true);
assert.match(path.basename(stateDir), /.+-[a-f0-9]{16}$/);
assert.match(stateDir, new RegExp(`^${os.tmpdir().replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`));
});
test("resolveStateDir uses CLAUDE_PLUGIN_DATA when it is provided", () => {
const workspace = makeTempDir();
const pluginDataDir = makeTempDir();
const previousPluginDataDir = process.env.CLAUDE_PLUGIN_DATA;
process.env.CLAUDE_PLUGIN_DATA = pluginDataDir;
try {
const stateDir = resolveStateDir(workspace);
assert.equal(stateDir.startsWith(path.join(pluginDataDir, "state")), true);
assert.match(path.basename(stateDir), /.+-[a-f0-9]{16}$/);
assert.match(
stateDir,
new RegExp(`^${path.join(pluginDataDir, "state").replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`)
);
} finally {
if (previousPluginDataDir == null) {
delete process.env.CLAUDE_PLUGIN_DATA;
} else {
process.env.CLAUDE_PLUGIN_DATA = previousPluginDataDir;
}
}
});
test("saveState prunes dropped job artifacts when indexed jobs exceed the cap", () => {
const workspace = makeTempDir();
const stateFile = resolveStateFile(workspace);
fs.mkdirSync(path.dirname(stateFile), { recursive: true });
const jobs = Array.from({ length: 51 }, (_, index) => {
const jobId = `job-${index}`;
const updatedAt = new Date(Date.UTC(2026, 0, 1, 0, index, 0)).toISOString();
const logFile = resolveJobLogFile(workspace, jobId);
const jobFile = resolveJobFile(workspace, jobId);
fs.writeFileSync(logFile, `log ${jobId}\n`, "utf8");
fs.writeFileSync(jobFile, JSON.stringify({ id: jobId, status: "completed" }, null, 2), "utf8");
return {
id: jobId,
status: "completed",
logFile,
updatedAt,
createdAt: updatedAt
};
});
fs.writeFileSync(
stateFile,
`${JSON.stringify(
{
version: 1,
config: { stopReviewGate: false },
jobs
},
null,
2
)}\n`,
"utf8"
);
saveState(workspace, {
version: 1,
config: { stopReviewGate: false },
jobs
});
const prunedJobFile = resolveJobFile(workspace, "job-0");
const prunedLogFile = resolveJobLogFile(workspace, "job-0");
const retainedJobFile = resolveJobFile(workspace, "job-50");
const retainedLogFile = resolveJobLogFile(workspace, "job-50");
const jobsDir = path.dirname(prunedJobFile);
assert.equal(fs.existsSync(retainedJobFile), true);
assert.equal(fs.existsSync(retainedLogFile), true);
const savedState = JSON.parse(fs.readFileSync(stateFile, "utf8"));
assert.equal(savedState.jobs.length, 50);
assert.deepEqual(
savedState.jobs.map((job) => job.id),
Array.from({ length: 50 }, (_, index) => `job-${50 - index}`)
);
assert.deepEqual(
fs.readdirSync(jobsDir).sort(),
Array.from({ length: 50 }, (_, index) => `job-${index + 1}`)
.flatMap((jobId) => [`${jobId}.json`, `${jobId}.log`])
.sort()
);
});