Files
boss/tests/local-agent-codex-app-server-runner.test.mjs

1320 lines
49 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import test from "node:test";
import assert from "node:assert/strict";
import crypto from "node:crypto";
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import http from "node:http";
import os from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import {
discoverCodexAppServerCapabilities,
executeCodexAppServerTask,
getCodexAppServerRunnerConfig,
shouldUseCodexAppServerTaskRunner,
} from "../local-agent/codex-app-server-runner.mjs";
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
test("codex app-server discovery includes governance and MCP summaries without leaking internals", async () => {
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
codexAppServerEnabled: true,
codexAppServerCommand: process.execPath,
codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"],
codexAppServerWorkdir: repoRoot,
codexAppServerTimeoutMs: 5000,
codexAppServerDiscoveryLimit: 20,
codexAppServerSkillExtraRoots: [
"/Users/kris/enterprise/boss-shared-skills",
"/Users/kris/enterprise/team-skills",
],
});
const metadata = await discoverCodexAppServerCapabilities(runnerConfig);
assert.equal(metadata.experimentalFeatures[0].name, "multi_agent");
assert.equal(metadata.experimentalFeatures[0].stage, "stable");
assert.equal(metadata.experimentalFeatures[0].enabled, true);
assert.equal(metadata.collaborationModes[1].id, "plan");
assert.equal(metadata.permissionProfiles[0].id, ":workspace");
assert.equal(metadata.mcpServers[0].name, "github");
assert.equal(metadata.mcpServers[0].toolCount, 2);
assert.equal(metadata.mcpServers[0].authStatus, "oAuth");
assert.deepEqual(metadata.accountSummary, {
signedIn: true,
authMode: "chatgpt",
planType: "pro",
requiresOpenaiAuth: true,
});
assert.equal(metadata.rateLimitSummary.bucketCount, 2);
assert.equal(metadata.rateLimitSummary.maxUsedPercent, 42);
assert.equal(metadata.rateLimitSummary.reached, false);
assert.deepEqual(metadata.appConfigSummary, {
appCount: 2,
enabledAppCount: 1,
defaultEnabled: true,
destructiveEnabled: false,
openWorldEnabled: false,
});
assert.deepEqual(metadata.configRequirements, {
managed: true,
requirementCount: 2,
warningCount: 1,
});
assert.deepEqual(metadata.externalAgentMigration, {
itemCount: 3,
homeItemCount: 1,
projectItemCount: 2,
itemTypes: ["AGENTS_MD", "MCP_SERVER_CONFIG", "SKILLS"],
});
assert.deepEqual(metadata.skillExtraRootsSummary, {
configured: true,
status: "applied",
rootCount: 2,
rootLabels: ["boss-shared-skills", "team-skills"],
});
assert.deepEqual(metadata.hookSummary, {
workspaceCount: 1,
hookCount: 2,
enabledHookCount: 1,
managedHookCount: 1,
trustedHookCount: 1,
modifiedHookCount: 1,
untrustedHookCount: 0,
warningCount: 1,
errorCount: 1,
eventNames: ["PreToolUse", "SessionStart"],
handlerTypes: ["command", "prompt"],
});
assert.equal(metadata.threadSummary.threadCount, 3);
assert.equal(metadata.threadSummary.loadedThreadCount, 2);
assert.equal(metadata.threadSummary.activeThreadCount, 1);
assert.equal(metadata.threadSummary.archivedThreadCount, 1);
assert.equal(metadata.threadSummary.latestUpdatedAt, "2026-06-03T08:20:00.000Z");
assert.deepEqual(metadata.threadSummary.sourceKinds, ["app", "cli"]);
assert.deepEqual(metadata.threadSummary.visibleThreads[0], {
id: "thr-active",
name: "Boss App Server rollout",
sourceKind: "app",
status: "active",
archived: false,
loaded: true,
updatedAt: "2026-06-03T08:20:00.000Z",
});
assert.equal(metadata.threadTurnSummary.threadCount, 2);
assert.equal(metadata.threadTurnSummary.totalTurnCount, 3);
assert.equal(metadata.threadTurnSummary.runningTurnCount, 1);
assert.equal(metadata.threadTurnSummary.completedTurnCount, 2);
assert.equal(metadata.threadTurnSummary.latestUpdatedAt, "2026-06-03T08:21:00.000Z");
assert.deepEqual(metadata.threadTurnSummary.threads[0], {
threadId: "thr-active",
turnCount: 2,
runningTurnCount: 1,
completedTurnCount: 1,
latestTurnStatus: "running",
latestTurnUpdatedAt: "2026-06-03T08:21:00.000Z",
});
const serialized = JSON.stringify(metadata);
assert.equal(serialized.includes("sk-secret-should-not-leak"), false);
assert.equal(serialized.includes("/Users/kris"), false);
assert.equal(serialized.includes("id_ed25519"), false);
assert.equal(serialized.includes("filesystem"), false);
assert.equal(serialized.includes("resources"), false);
assert.equal(serialized.includes("private-user@example.com"), false);
assert.equal(serialized.includes("CLAUDE.md"), false);
assert.equal(serialized.includes("AGENTS.md"), false);
assert.equal(serialized.includes("/Users/kris/enterprise"), false);
assert.equal(serialized.includes("secret user text should not leak"), false);
assert.equal(serialized.includes("Old private thread"), false);
assert.equal(serialized.includes("private active turn text should not leak"), false);
assert.equal(serialized.includes("private item content should not leak"), false);
assert.equal(serialized.includes("private idle turn text should not leak"), false);
assert.equal(serialized.includes("private-hook.toml"), false);
assert.equal(serialized.includes("private-user-hook.toml"), false);
assert.equal(serialized.includes("broken-hook.toml"), false);
assert.equal(serialized.includes("private hook status"), false);
assert.equal(serialized.includes("private hook warning"), false);
assert.equal(serialized.includes("private hook error"), false);
assert.equal(serialized.includes("session-start-private-key"), false);
assert.equal(serialized.includes("sk-secret-should-not-leak"), false);
});
function encodeWsTextFrame(value) {
const payload = Buffer.from(value);
if (payload.length < 126) {
return Buffer.concat([Buffer.from([0x81, payload.length]), payload]);
}
if (payload.length <= 0xffff) {
const header = Buffer.alloc(4);
header[0] = 0x81;
header[1] = 126;
header.writeUInt16BE(payload.length, 2);
return Buffer.concat([header, payload]);
}
const header = Buffer.alloc(10);
header[0] = 0x81;
header[1] = 127;
header.writeBigUInt64BE(BigInt(payload.length), 2);
return Buffer.concat([header, payload]);
}
function decodeWsFrames(buffer, socket, onText) {
let offset = 0;
while (buffer.length - offset >= 2) {
const first = buffer[offset];
const second = buffer[offset + 1];
const opcode = first & 0x0f;
const masked = (second & 0x80) !== 0;
let payloadLength = second & 0x7f;
let headerLength = 2;
if (payloadLength === 126) {
if (buffer.length - offset < 4) break;
payloadLength = buffer.readUInt16BE(offset + 2);
headerLength = 4;
} else if (payloadLength === 127) {
if (buffer.length - offset < 10) break;
payloadLength = Number(buffer.readBigUInt64BE(offset + 2));
headerLength = 10;
}
const maskLength = masked ? 4 : 0;
const frameLength = headerLength + maskLength + payloadLength;
if (buffer.length - offset < frameLength) break;
let payload = buffer.subarray(
offset + headerLength + maskLength,
offset + frameLength,
);
if (masked) {
const mask = buffer.subarray(offset + headerLength, offset + headerLength + 4);
const unmaskedPayload = Buffer.alloc(payload.length);
for (let index = 0; index < payload.length; index += 1) {
unmaskedPayload[index] = payload[index] ^ mask[index % 4];
}
payload = unmaskedPayload;
}
if (opcode === 0x1) {
onText(payload.toString("utf8"));
} else if (opcode === 0x8) {
socket.end();
} else if (opcode === 0x9) {
socket.write(Buffer.concat([Buffer.from([0x8a, payload.length]), payload]));
}
offset += frameLength;
}
return buffer.subarray(offset);
}
async function createCodexAppServerWebSocketFixture(options = {}) {
const server = http.createServer();
const sockets = new Set();
let lastAuthorization = "";
server.on("upgrade", (request, socket) => {
sockets.add(socket);
socket.on("close", () => {
sockets.delete(socket);
});
lastAuthorization = String(request.headers.authorization || "");
const key = request.headers["sec-websocket-key"];
const accept = crypto
.createHash("sha1")
.update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`)
.digest("base64");
socket.write(
[
"HTTP/1.1 101 Switching Protocols",
"Upgrade: websocket",
"Connection: Upgrade",
`Sec-WebSocket-Accept: ${accept}`,
"",
"",
].join("\r\n"),
);
let buffered = Buffer.alloc(0);
const send = (message) => {
socket.write(encodeWsTextFrame(JSON.stringify(message)));
};
socket.on("data", (chunk) => {
buffered = decodeWsFrames(Buffer.concat([buffered, chunk]), socket, (line) => {
const message = JSON.parse(line);
if (message.method === "initialize") {
send({
id: message.id,
result: {
userAgent: "boss-test-codex-ws-app-server",
platformFamily: "mac",
platformOs: "darwin",
},
});
return;
}
if (message.method === "initialized") {
return;
}
if (message.method === "thread/resume") {
send({
id: message.id,
result: {
thread: {
id: message.params?.threadId,
name: "ws fixture thread",
},
},
});
return;
}
if (message.method === "turn/start") {
const text = message.params?.input?.find?.((item) => item?.type === "text")?.text ?? "";
send({
id: message.id,
result: {
turn: {
id: "ws-turn-fixture",
threadId: message.params?.threadId,
},
},
});
send({
method: "turn/plan/updated",
params: {
threadId: message.params?.threadId,
turnId: "ws-turn-fixture",
plan: [{ text: "通过 WebSocket 接入 Codex App Server", status: "completed" }],
},
});
send({
method: "item/agentMessage/delta",
params: {
threadId: message.params?.threadId,
turnId: "ws-turn-fixture",
delta: `WS_APP_SERVER_REPLY:${text}`,
},
});
send({
method: "turn/completed",
params: {
threadId: message.params?.threadId,
turn: { id: "ws-turn-fixture", status: "completed" },
},
});
return;
}
send({
id: message.id,
error: {
code: -32601,
message: `unknown method ${message.method}`,
},
});
});
});
});
let socketPath;
if (options.unixSocketPath) {
socketPath = options.unixSocketPath;
await new Promise((resolveServer) => server.listen(socketPath, resolveServer));
} else {
await new Promise((resolveServer) => server.listen(0, "127.0.0.1", resolveServer));
}
const address = server.address();
return {
url: socketPath ? `unix://${socketPath}` : `ws://127.0.0.1:${address.port}`,
getLastAuthorization: () => lastAuthorization,
close: () =>
new Promise((resolveServer) => {
for (const socket of sockets) {
socket.destroy();
}
server.close(resolveServer);
}),
};
}
test("codex app-server runner resumes a thread and collects streamed agent text", async () => {
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
codexAppServerEnabled: true,
codexAppServerCommand: process.execPath,
codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"],
codexAppServerWorkdir: repoRoot,
codexAppServerTimeoutMs: 5000,
masterAgentModel: "gpt-5.4",
});
const task = {
taskId: "task-app-server-1",
taskType: "conversation_reply",
targetCodexThreadRef: "019d-app-server-thread",
targetCodexFolderRef: repoRoot,
executionPrompt: "继续开发并给出结果",
};
assert.equal(shouldUseCodexAppServerTaskRunner(runnerConfig, task), true);
const result = await executeCodexAppServerTask(runnerConfig, task);
assert.equal(result.status, "completed");
assert.equal(result.threadId, "019d-app-server-thread");
assert.equal(result.cwd, repoRoot);
assert.equal(result.replyBody, "APP_SERVER_REPLY:继续开发并给出结果");
assert.equal(result.transport, "stdio");
});
test("codex app-server runner converts protocol progress events into Boss execution progress", async () => {
const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_PROGRESS;
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_PROGRESS = "1";
try {
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
codexAppServerEnabled: true,
codexAppServerCommand: process.execPath,
codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"],
codexAppServerWorkdir: repoRoot,
codexAppServerTimeoutMs: 5000,
masterAgentModel: "gpt-5.4",
});
const result = await executeCodexAppServerTask(runnerConfig, {
taskId: "task-app-server-progress",
taskType: "dispatch_execution",
targetCodexThreadRef: "019d-app-server-thread",
targetCodexFolderRef: repoRoot,
executionPrompt: "实现并回归",
});
assert.equal(result.status, "completed");
assert.equal(result.executionProgress.steps[0].text, "读取 Codex 官方 app-server 协议");
assert.equal(result.executionProgress.steps[0].status, "done");
assert.equal(result.executionProgress.steps[1].text, "执行 targeted/full test");
assert.equal(result.executionProgress.steps[1].status, "running");
assert.equal(result.executionProgress.branch.changedFiles, 3);
assert.equal(result.executionProgress.branch.additions, 181);
assert.equal(result.executionProgress.branch.deletions, 52);
assert.deepEqual(result.executionProgress.artifacts, [
{
id: "artifact-1",
label: "codex-app-server-protocol-0.135.0.json",
kind: "file",
},
]);
assert.deepEqual(result.executionProgress.agents, [
{
name: "Mendel",
role: "explorer",
status: "running",
},
]);
} finally {
if (previous === undefined) {
delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_PROGRESS;
} else {
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_PROGRESS = previous;
}
}
});
test("codex app-server runner maps guardian approval and file-change events without leaking secrets", async () => {
const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_GUARDIAN_EVENTS;
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_GUARDIAN_EVENTS = "1";
try {
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
codexAppServerEnabled: true,
codexAppServerCommand: process.execPath,
codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"],
codexAppServerWorkdir: repoRoot,
codexAppServerTimeoutMs: 5000,
masterAgentModel: "gpt-5.4",
});
const result = await executeCodexAppServerTask(runnerConfig, {
taskId: "task-app-server-guardian",
taskType: "dispatch_execution",
targetCodexThreadRef: "019d-app-server-thread",
targetCodexFolderRef: repoRoot,
executionPrompt: "需要审批和文件变更摘要",
});
assert.equal(result.status, "completed");
assert.equal(result.replyBody, "APP_SERVER_REPLY:需要审批和文件变更摘要");
assert.deepEqual(result.executionProgress.approvals, [
{
id: "cmd-approval-1",
kind: "command",
label: "命令执行审批",
status: "resolved",
detail: "需要确认命令执行",
},
{
id: "review-1",
kind: "auto_review",
label: "自动审批复核",
status: "approved",
riskLevel: "medium",
},
]);
assert.deepEqual(result.executionProgress.warnings, [
{
id: "guardian-warning-1",
message: "检测到需要用户确认的命令执行。",
severity: "warning",
},
]);
assert.deepEqual(result.executionProgress.fileChanges, [
{
id: "file-change-item-1-1",
path: "src/app/page.tsx",
kind: "update",
status: "updated",
},
{
id: "file-change-item-1-2",
path: "docs/architecture/codex_server_progress_card_cn.md",
kind: "add",
status: "updated",
},
]);
assert.equal(result.executionProgress.branch.changedFiles, 2);
assert.equal(JSON.stringify(result.executionProgress).includes("sk-secret-should-not-leak"), false);
assert.equal(JSON.stringify(result.executionProgress).includes("internal prompt"), false);
} finally {
if (previous === undefined) {
delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_GUARDIAN_EVENTS;
} else {
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_GUARDIAN_EVENTS = previous;
}
}
});
test("codex app-server runner maps thread status and realtime events without leaking transport payloads", async () => {
const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_REALTIME_EVENTS;
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_REALTIME_EVENTS = "1";
try {
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
codexAppServerEnabled: true,
codexAppServerCommand: process.execPath,
codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"],
codexAppServerWorkdir: repoRoot,
codexAppServerTimeoutMs: 5000,
masterAgentModel: "gpt-5.4",
});
const result = await executeCodexAppServerTask(runnerConfig, {
taskId: "task-app-server-realtime",
taskType: "conversation_reply",
targetCodexThreadRef: "019d-app-server-thread",
targetCodexFolderRef: repoRoot,
executionPrompt: "开启实时协作并回写状态",
});
assert.equal(result.status, "completed");
assert.deepEqual(result.executionProgress.threadStatus, {
type: "active",
activeFlags: ["waitingOnApproval", "waitingOnUserInput"],
waitingOnApproval: true,
waitingOnUserInput: true,
});
assert.deepEqual(result.executionProgress.realtime, {
status: "closed",
sessionId: "rt-session-1",
version: "v2",
transcriptRole: "assistant",
transcriptPreview: "正在分析 Codex App Server 实时事件。",
audioChunkCount: 1,
itemCount: 1,
closeReason: "completed",
});
const serialized = JSON.stringify(result.executionProgress);
assert.equal(serialized.includes("audio-secret-payload"), false);
assert.equal(serialized.includes("v=0 secret"), false);
assert.equal(serialized.includes("raw realtime item should not be persisted"), false);
} finally {
if (previous === undefined) {
delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_REALTIME_EVENTS;
} else {
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_REALTIME_EVENTS = previous;
}
}
});
test("codex app-server runner maps runtime status events without leaking internal identifiers", async () => {
const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_RUNTIME_STATUS;
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_RUNTIME_STATUS = "1";
try {
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
codexAppServerEnabled: true,
codexAppServerCommand: process.execPath,
codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"],
codexAppServerWorkdir: repoRoot,
codexAppServerTimeoutMs: 5000,
masterAgentModel: "gpt-5.4",
});
const result = await executeCodexAppServerTask(runnerConfig, {
taskId: "task-app-server-runtime-status",
taskType: "conversation_reply",
targetCodexThreadRef: "019d-app-server-thread",
targetCodexFolderRef: repoRoot,
executionPrompt: "检查模型和运行状态",
});
assert.equal(result.status, "completed");
assert.deepEqual(result.executionProgress.modelRoute, {
fromModel: "gpt-5.4-mini",
toModel: "gpt-5.4",
reason: "highRiskCyberActivity",
});
assert.deepEqual(result.executionProgress.tokenUsage, {
totalTokens: 3000,
inputTokens: 2200,
cachedInputTokens: 300,
outputTokens: 650,
reasoningOutputTokens: 150,
modelContextWindow: 200000,
contextPercent: 2,
});
assert.deepEqual(result.executionProgress.mcpServers, [
{
name: "github",
status: "failed",
error: "token=[redacted] failed to start",
},
]);
assert.deepEqual(result.executionProgress.remoteControl, {
status: "connected",
serverName: "Mac Studio",
environmentId: "env-prod",
});
assert.deepEqual(result.executionProgress.windowsSandbox, {
status: "failed",
setupMode: "elevated",
error: "token=[redacted] failed in [path]",
});
const serialized = JSON.stringify(result.executionProgress);
assert.equal(serialized.includes("install-secret-should-not-leak"), false);
assert.equal(serialized.includes("sk-secret-should-not-leak"), false);
assert.equal(serialized.includes("C:\\Users\\kris\\secret"), false);
} finally {
if (previous === undefined) {
delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_RUNTIME_STATUS;
} else {
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_RUNTIME_STATUS = previous;
}
}
});
test("codex app-server runner maps thread goal, settings, and compaction events without leaking local paths", async () => {
const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_THREAD_CONFIG_EVENTS;
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_THREAD_CONFIG_EVENTS = "1";
try {
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
codexAppServerEnabled: true,
codexAppServerCommand: process.execPath,
codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"],
codexAppServerWorkdir: repoRoot,
codexAppServerTimeoutMs: 5000,
masterAgentModel: "gpt-5.4",
});
const result = await executeCodexAppServerTask(runnerConfig, {
taskId: "task-app-server-thread-config",
taskType: "conversation_reply",
targetCodexThreadRef: "019d-app-server-thread",
targetCodexFolderRef: repoRoot,
executionPrompt: "同步线程目标和设置",
});
assert.equal(result.status, "completed");
assert.deepEqual(result.executionProgress.threadGoal, {
objective: "完成 App Server 线程目标同步",
status: "active",
tokenBudget: 120000,
tokensUsed: 4800,
timeUsedSeconds: 600,
});
assert.deepEqual(result.executionProgress.threadSettings, {
model: "gpt-5.5",
modelProvider: "openai",
approvalPolicy: "on-request",
approvalsReviewer: "user",
sandboxPolicy: "workspaceWrite",
permissionProfile: ":workspace",
serviceTier: "fast",
effort: "low",
summary: "concise",
collaborationMode: "plan",
personality: "pragmatic",
});
assert.deepEqual(result.executionProgress.compaction, {
status: "completed",
message: "上下文已压缩",
});
const serialized = JSON.stringify(result.executionProgress);
assert.equal(serialized.includes("/Users/kris"), false);
assert.equal(serialized.includes("internal prompt"), false);
assert.equal(serialized.includes("secret-instructions"), false);
} finally {
if (previous === undefined) {
delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_THREAD_CONFIG_EVENTS;
} else {
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_THREAD_CONFIG_EVENTS = previous;
}
}
});
test("codex app-server runner maps account, quota, verification, and notices without leaking config paths", async () => {
const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_ACCOUNT_NOTICE_EVENTS;
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_ACCOUNT_NOTICE_EVENTS = "1";
try {
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
codexAppServerEnabled: true,
codexAppServerCommand: process.execPath,
codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"],
codexAppServerWorkdir: repoRoot,
codexAppServerTimeoutMs: 5000,
masterAgentModel: "gpt-5.4",
});
const result = await executeCodexAppServerTask(runnerConfig, {
taskId: "task-app-server-account-notices",
taskType: "conversation_reply",
targetCodexThreadRef: "019d-app-server-thread",
targetCodexFolderRef: repoRoot,
executionPrompt: "同步账号与告警状态",
});
assert.equal(result.status, "completed");
assert.deepEqual(result.executionProgress.accountStatus, {
authMode: "chatgpt",
planType: "team",
limitId: "codex",
limitName: "Codex",
usedPercent: 88,
windowDurationMins: 180,
resetsAt: 1770003600,
creditsBalance: "120.5",
hasCredits: true,
unlimitedCredits: false,
});
assert.deepEqual(result.executionProgress.modelVerification, {
verifications: ["trustedAccessForCyber"],
});
assert.deepEqual(result.executionProgress.warnings, [
{
id: "codex-warning-1",
message: "模型切换提醒 token=[redacted]",
severity: "warning",
},
{
id: "config-warning-2",
message: "项目配置已忽略openai_base_url 不能放在项目配置里",
severity: "warning",
},
{
id: "deprecation-notice-3",
message: "on-failure 已废弃:请改用 on-request",
severity: "info",
},
]);
const serialized = JSON.stringify(result.executionProgress);
assert.equal(serialized.includes("/Users/kris"), false);
assert.equal(serialized.includes("sk-secret-should-not-leak"), false);
} finally {
if (previous === undefined) {
delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_ACCOUNT_NOTICE_EVENTS;
} else {
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_ACCOUNT_NOTICE_EVENTS = previous;
}
}
});
test("codex app-server runner maps collab tool calls and context compaction without leaking thread internals", async () => {
const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_COLLAB_EVENTS;
const previousV2 = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_COLLAB_EVENTS_V2;
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_COLLAB_EVENTS = "1";
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_COLLAB_EVENTS_V2 = "1";
try {
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
codexAppServerEnabled: true,
codexAppServerCommand: process.execPath,
codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"],
codexAppServerWorkdir: repoRoot,
codexAppServerTimeoutMs: 5000,
masterAgentModel: "gpt-5.4",
});
const result = await executeCodexAppServerTask(runnerConfig, {
taskId: "task-app-server-collab-events",
taskType: "conversation_reply",
targetCodexThreadRef: "019d-app-server-thread",
targetCodexFolderRef: repoRoot,
executionPrompt: "让目标线程协作推进",
});
assert.equal(result.status, "completed");
assert.deepEqual(result.executionProgress.threadCollaboration, {
tool: "send_input",
status: "completed",
target: "已有线程",
agentStatus: "completed · errored",
receiverCount: 2,
});
assert.deepEqual(result.executionProgress.compaction, {
status: "completed",
message: "上下文已压缩",
});
const serialized = JSON.stringify(result.executionProgress);
assert.equal(serialized.includes("thread-source-secret-should-not-leak"), false);
assert.equal(serialized.includes("thread-target-secret-should-not-leak"), false);
assert.equal(serialized.includes("thread-target-2-secret-should-not-leak"), false);
assert.equal(serialized.includes("internal prompt"), false);
assert.equal(serialized.includes("private agent status message"), false);
assert.equal(serialized.includes("private agent completed message"), false);
assert.equal(serialized.includes("private agent error message"), false);
assert.equal(serialized.includes("sk-secret-should-not-leak"), false);
assert.equal(serialized.includes("context-compaction-secret-should-not-leak"), false);
} finally {
if (previous === undefined) {
delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_COLLAB_EVENTS;
} else {
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_COLLAB_EVENTS = previous;
}
if (previousV2 === undefined) {
delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_COLLAB_EVENTS_V2;
} else {
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_COLLAB_EVENTS_V2 = previousV2;
}
}
});
test("codex app-server runner maps tool, search, image, review, and command activities without leaking payloads", async () => {
const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_TOOL_ACTIVITY_EVENTS;
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_TOOL_ACTIVITY_EVENTS = "1";
try {
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
codexAppServerEnabled: true,
codexAppServerCommand: process.execPath,
codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"],
codexAppServerWorkdir: repoRoot,
codexAppServerTimeoutMs: 5000,
masterAgentModel: "gpt-5.4",
});
const result = await executeCodexAppServerTask(runnerConfig, {
taskId: "task-app-server-tool-activity-events",
taskType: "conversation_reply",
targetCodexThreadRef: "019d-app-server-thread",
targetCodexFolderRef: repoRoot,
executionPrompt: "检查 Codex 工具活动",
});
assert.equal(result.status, "completed");
assert.deepEqual(result.executionProgress.toolActivities, [
{
kind: "mcp",
name: "github/pull_request/list",
status: "failed",
detail: "token=[redacted] failed",
},
{
kind: "dynamic",
name: "browser.open",
status: "completed",
detail: "成功 · 1234ms",
},
{
kind: "web_search",
name: "openPage",
status: "running",
detail: "Codex App Server ThreadItem",
},
{
kind: "image_view",
name: "imageView",
status: "completed",
detail: "private-screenshot.png",
},
{
kind: "review",
name: "reviewMode",
status: "exited",
detail: "completed",
},
{
kind: "command",
name: "commandExecution",
status: "completed",
detail: "exit 0 · 2345ms",
},
]);
const serialized = JSON.stringify(result.executionProgress);
assert.equal(serialized.includes("sk-secret-should-not-leak"), false);
assert.equal(serialized.includes("/Users/kris"), false);
assert.equal(serialized.includes("internal tool result"), false);
assert.equal(serialized.includes("internal review prompt"), false);
assert.equal(serialized.includes("id_rsa"), false);
assert.equal(serialized.includes("https://example.com/private"), false);
} finally {
if (previous === undefined) {
delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_TOOL_ACTIVITY_EVENTS;
} else {
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_TOOL_ACTIVITY_EVENTS = previous;
}
}
});
test("codex app-server runner maps image generation as a safe activity and artifact", async () => {
const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_IMAGE_GENERATION_EVENTS;
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_IMAGE_GENERATION_EVENTS = "1";
try {
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
codexAppServerEnabled: true,
codexAppServerCommand: process.execPath,
codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"],
codexAppServerWorkdir: repoRoot,
codexAppServerTimeoutMs: 5000,
masterAgentModel: "gpt-5.4",
});
const result = await executeCodexAppServerTask(runnerConfig, {
taskId: "task-app-server-image-generation-events",
taskType: "conversation_reply",
targetCodexThreadRef: "019d-app-server-thread",
targetCodexFolderRef: repoRoot,
executionPrompt: "检查 Codex imageGeneration item",
});
assert.equal(result.status, "completed");
assert.deepEqual(result.executionProgress.toolActivities, [
{
kind: "image_generation",
name: "imageGeneration",
status: "completed",
detail: "generated-secret-image.png",
},
]);
assert.deepEqual(result.executionProgress.artifacts, [
{
id: "artifact-1",
label: "generated-secret-image.png",
kind: "image",
},
]);
const serialized = JSON.stringify(result.executionProgress);
assert.equal(serialized.includes("image-generation-secret-should-not-leak"), false);
assert.equal(serialized.includes("raw-secret-should-not-leak"), false);
assert.equal(serialized.includes("sk-secret-should-not-leak"), false);
assert.equal(serialized.includes("/Users/kris"), false);
assert.equal(serialized.includes("internal prompt"), false);
} finally {
if (previous === undefined) {
delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_IMAGE_GENERATION_EVENTS;
} else {
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_IMAGE_GENERATION_EVENTS = previous;
}
}
});
test("codex app-server runner maps hook lifecycle events without leaking hook internals", async () => {
const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_HOOK_EVENTS;
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_HOOK_EVENTS = "1";
try {
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
codexAppServerEnabled: true,
codexAppServerCommand: process.execPath,
codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"],
codexAppServerWorkdir: repoRoot,
codexAppServerTimeoutMs: 5000,
masterAgentModel: "gpt-5.4",
});
const result = await executeCodexAppServerTask(runnerConfig, {
taskId: "task-app-server-hook-events",
taskType: "conversation_reply",
targetCodexThreadRef: "019d-app-server-thread",
targetCodexFolderRef: repoRoot,
executionPrompt: "检查 Codex hook 生命周期",
});
assert.equal(result.status, "completed");
assert.deepEqual(result.executionProgress.toolActivities, [
{
kind: "hook",
name: "postToolUse/command",
status: "completed",
detail: "project · async · 42ms",
},
]);
const serialized = JSON.stringify(result.executionProgress);
assert.equal(serialized.includes("hook-secret-should-not-leak"), false);
assert.equal(serialized.includes("sk-secret-should-not-leak"), false);
assert.equal(serialized.includes("/Users/kris"), false);
assert.equal(serialized.includes("private-hook.toml"), false);
assert.equal(serialized.includes("internal hook output"), false);
assert.equal(serialized.includes("private hook result"), false);
} finally {
if (previous === undefined) {
delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_HOOK_EVENTS;
} else {
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_HOOK_EVENTS = previous;
}
}
});
test("codex app-server runner maps item plan and reasoning summary without leaking raw reasoning", async () => {
const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_REASONING_PLAN_EVENTS;
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_REASONING_PLAN_EVENTS = "1";
try {
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
codexAppServerEnabled: true,
codexAppServerCommand: process.execPath,
codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"],
codexAppServerWorkdir: repoRoot,
codexAppServerTimeoutMs: 5000,
masterAgentModel: "gpt-5.4",
});
const result = await executeCodexAppServerTask(runnerConfig, {
taskId: "task-app-server-reasoning-plan-events",
taskType: "conversation_reply",
targetCodexThreadRef: "019d-app-server-thread",
targetCodexFolderRef: repoRoot,
executionPrompt: "检查 Codex plan 和 reasoning item",
});
assert.equal(result.status, "completed");
assert.deepEqual(result.executionProgress.steps, [
{
id: "plan-item-1",
text: "回读官方 App Server item 协议",
status: "pending",
},
{
id: "plan-item-2",
text: "补充 reasoning summary 安全映射",
status: "pending",
},
]);
assert.deepEqual(result.executionProgress.reasoningSummary, {
status: "completed",
summary: "确认只展示官方 summary不展示 raw content。",
});
const serialized = JSON.stringify(result.executionProgress);
assert.equal(serialized.includes("raw hidden reasoning"), false);
assert.equal(serialized.includes("raw hidden chain of thought"), false);
assert.equal(serialized.includes("sk-secret-should-not-leak"), false);
assert.equal(serialized.includes("reasoning-secret-should-not-leak"), false);
} finally {
if (previous === undefined) {
delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_REASONING_PLAN_EVENTS;
} else {
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_REASONING_PLAN_EVENTS = previous;
}
}
});
test("codex app-server runner bridges source thread context into target thread through inject_items", async () => {
const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_INTER_THREAD;
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_INTER_THREAD = "1";
try {
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
codexAppServerEnabled: true,
codexAppServerCommand: process.execPath,
codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"],
codexAppServerWorkdir: repoRoot,
codexAppServerTimeoutMs: 5000,
});
const result = await executeCodexAppServerTask(runnerConfig, {
taskId: "task-inter-thread",
taskType: "conversation_reply",
intentCategory: "thread_collaboration",
sourceCodexThreadRef: "source-thread-1",
sourceThreadDisplayName: "源线程",
targetCodexThreadRef: "target-thread-2",
targetThreadDisplayName: "目标线程",
targetCodexFolderRef: repoRoot,
executionPrompt: "请基于源线程结论继续实现",
});
assert.equal(result.status, "completed");
assert.equal(result.threadId, "target-thread-2");
assert.equal(result.interThreadBroker.sourceThreadId, "source-thread-1");
assert.equal(result.interThreadBroker.targetThreadId, "target-thread-2");
assert.equal(result.interThreadBroker.injectedItemCount, 1);
assert.match(result.replyBody, /INTER_THREAD_INJECTED/);
assert.match(result.replyBody, /源线程最近结论:优先使用 app-server 协议/);
} finally {
if (previous === undefined) {
delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_INTER_THREAD;
} else {
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_INTER_THREAD = previous;
}
}
});
test("codex app-server runner steers an active turn when a target turn id is present", async () => {
const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_STEER;
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_STEER = "1";
try {
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
codexAppServerEnabled: true,
codexAppServerCommand: process.execPath,
codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"],
codexAppServerWorkdir: repoRoot,
codexAppServerTimeoutMs: 5000,
});
const result = await executeCodexAppServerTask(runnerConfig, {
taskId: "task-turn-steer",
taskType: "conversation_reply",
targetCodexThreadRef: "active-thread-1",
targetCodexTurnId: "active-turn-1",
targetCodexFolderRef: repoRoot,
executionPrompt: "先暂停写入,改为只做 diff 检查",
});
assert.equal(result.status, "completed");
assert.equal(result.threadId, "active-thread-1");
assert.equal(result.turnId, "active-turn-1");
assert.equal(result.turnControl, "steer");
assert.equal(result.replyBody, "STEERED:先暂停写入,改为只做 diff 检查");
} finally {
if (previous === undefined) {
delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_STEER;
} else {
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_STEER = previous;
}
}
});
test("codex app-server config exposes ws transport without mutating stdio defaults", () => {
const previousTransport = process.env.BOSS_CODEX_APP_SERVER_TRANSPORT;
const previousUrl = process.env.BOSS_CODEX_APP_SERVER_URL;
process.env.BOSS_CODEX_APP_SERVER_TRANSPORT = "ws";
process.env.BOSS_CODEX_APP_SERVER_URL = "ws://127.0.0.1:4500";
try {
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
codexAppServerEnabled: true,
});
assert.equal(runnerConfig.transport, "ws");
assert.equal(runnerConfig.url, "ws://127.0.0.1:4500");
assert.equal(runnerConfig.args[0], "app-server");
} finally {
if (previousTransport === undefined) {
delete process.env.BOSS_CODEX_APP_SERVER_TRANSPORT;
} else {
process.env.BOSS_CODEX_APP_SERVER_TRANSPORT = previousTransport;
}
if (previousUrl === undefined) {
delete process.env.BOSS_CODEX_APP_SERVER_URL;
} else {
process.env.BOSS_CODEX_APP_SERVER_URL = previousUrl;
}
}
});
test("codex app-server runner connects to a ws app-server endpoint without spawning stdio", async () => {
const fixture = await createCodexAppServerWebSocketFixture();
try {
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
codexAppServerEnabled: true,
codexAppServerTransport: "ws",
codexAppServerUrl: fixture.url,
codexAppServerCommand: "definitely-not-a-real-codex-app-server",
codexAppServerWorkdir: repoRoot,
codexAppServerTimeoutMs: 5000,
masterAgentModel: "gpt-5.4",
});
const result = await executeCodexAppServerTask(runnerConfig, {
taskId: "task-ws-app-server",
taskType: "conversation_reply",
targetCodexThreadRef: "ws-thread-1",
targetCodexFolderRef: repoRoot,
executionPrompt: "用 ws 路径回复",
});
assert.equal(result.status, "completed");
assert.equal(result.transport, "ws");
assert.equal(result.threadId, "ws-thread-1");
assert.equal(result.turnId, "ws-turn-fixture");
assert.equal(result.replyBody, "WS_APP_SERVER_REPLY:用 ws 路径回复");
assert.equal(result.executionProgress.steps[0].text, "通过 WebSocket 接入 Codex App Server");
} finally {
await fixture.close();
}
});
test("codex app-server runner sends bearer auth during ws handshake", async () => {
const fixture = await createCodexAppServerWebSocketFixture();
try {
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
codexAppServerEnabled: true,
codexAppServerTransport: "ws",
codexAppServerUrl: fixture.url,
codexAppServerAuthToken: "boss-ws-token",
codexAppServerCommand: "definitely-not-a-real-codex-app-server",
codexAppServerWorkdir: repoRoot,
codexAppServerTimeoutMs: 5000,
});
const result = await executeCodexAppServerTask(runnerConfig, {
taskId: "task-ws-auth",
taskType: "conversation_reply",
targetCodexThreadRef: "ws-auth-thread",
targetCodexFolderRef: repoRoot,
executionPrompt: "验证 ws 鉴权",
});
assert.equal(result.status, "completed");
assert.equal(result.replyBody, "WS_APP_SERVER_REPLY:验证 ws 鉴权");
assert.equal(fixture.getLastAuthorization(), "Bearer boss-ws-token");
} finally {
await fixture.close();
}
});
test("codex app-server runner can load bearer auth from a local token file", async () => {
const runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-codex-app-server-auth-"));
const tokenPath = path.join(runtimeRoot, "ws-token.txt");
await writeFile(tokenPath, "boss-token-from-file\n", "utf8");
const fixture = await createCodexAppServerWebSocketFixture();
try {
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
codexAppServerEnabled: true,
codexAppServerTransport: "ws",
codexAppServerUrl: fixture.url,
codexAppServerAuthTokenFile: tokenPath,
codexAppServerCommand: "definitely-not-a-real-codex-app-server",
codexAppServerWorkdir: repoRoot,
codexAppServerTimeoutMs: 5000,
});
const result = await executeCodexAppServerTask(runnerConfig, {
taskId: "task-ws-auth-file",
taskType: "conversation_reply",
targetCodexThreadRef: "ws-auth-file-thread",
targetCodexFolderRef: repoRoot,
executionPrompt: "验证 token file",
});
assert.equal(result.status, "completed");
assert.equal(fixture.getLastAuthorization(), "Bearer boss-token-from-file");
} finally {
await fixture.close();
await rm(runtimeRoot, { recursive: true, force: true });
}
});
test("codex app-server runner connects to a unix socket app-server endpoint", async () => {
const runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-codex-app-server-unix-"));
const fixture = await createCodexAppServerWebSocketFixture({
unixSocketPath: path.join(runtimeRoot, "codex-app-server.sock"),
});
try {
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
codexAppServerEnabled: true,
codexAppServerTransport: "unix",
codexAppServerUrl: fixture.url,
codexAppServerCommand: "definitely-not-a-real-codex-app-server",
codexAppServerWorkdir: repoRoot,
codexAppServerTimeoutMs: 5000,
});
const result = await executeCodexAppServerTask(runnerConfig, {
taskId: "task-unix-app-server",
taskType: "conversation_reply",
targetCodexThreadRef: "unix-thread-1",
targetCodexFolderRef: repoRoot,
executionPrompt: "用 unix socket 回复",
});
assert.equal(result.status, "completed");
assert.equal(result.transport, "unix");
assert.equal(result.threadId, "unix-thread-1");
assert.equal(result.replyBody, "WS_APP_SERVER_REPLY:用 unix socket 回复");
} finally {
await fixture.close();
await rm(runtimeRoot, { recursive: true, force: true });
}
});
test("codex app-server runner retries transient overloaded JSON-RPC requests", async () => {
const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_OVERLOAD_ON_TURN_START;
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_OVERLOAD_ON_TURN_START = "1";
try {
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
codexAppServerEnabled: true,
codexAppServerCommand: process.execPath,
codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"],
codexAppServerWorkdir: repoRoot,
codexAppServerTimeoutMs: 5000,
});
const result = await executeCodexAppServerTask(runnerConfig, {
taskId: "task-app-server-overloaded",
taskType: "conversation_reply",
targetCodexThreadRef: "019d-app-server-thread",
targetCodexFolderRef: repoRoot,
executionPrompt: "拥塞后重试",
});
assert.equal(result.status, "completed");
assert.equal(result.replyBody, "APP_SERVER_REPLY:拥塞后重试");
} finally {
if (previous === undefined) {
delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_OVERLOAD_ON_TURN_START;
} else {
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_OVERLOAD_ON_TURN_START = previous;
}
}
});
test("codex app-server runner stays disabled unless feature flag is explicit", () => {
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
codexAppServerCommand: process.execPath,
codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"],
});
assert.equal(
shouldUseCodexAppServerTaskRunner(runnerConfig, {
taskType: "conversation_reply",
targetCodexThreadRef: "thread-disabled",
executionPrompt: "不会执行",
}),
false,
);
});
test("codex app-server runner fails fast when the server exits before turn completion", async () => {
const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EXIT_AFTER_TURN_START;
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EXIT_AFTER_TURN_START = "1";
try {
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
codexAppServerEnabled: true,
codexAppServerCommand: process.execPath,
codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"],
codexAppServerWorkdir: repoRoot,
codexAppServerTimeoutMs: 5000,
});
const result = await executeCodexAppServerTask(runnerConfig, {
taskId: "task-app-server-exit",
taskType: "conversation_reply",
targetCodexThreadRef: "019d-app-server-thread",
targetCodexFolderRef: repoRoot,
executionPrompt: "不要等到超时",
});
assert.equal(result.status, "failed");
assert.equal(result.canFallbackToCli, false);
assert.match(result.errorMessage, /CODEX_APP_SERVER_EXITED:0/);
} finally {
if (previous === undefined) {
delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EXIT_AFTER_TURN_START;
} else {
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EXIT_AFTER_TURN_START = previous;
}
}
});