import path from "node:path"; 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 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 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 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("") && 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") { 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 "thread/start": { 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 "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") } }); }, 400); 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); } export function buildEnv(binDir) { return { ...process.env, PATH: `${binDir}:${process.env.PATH}` }; }