1794 lines
68 KiB
JavaScript
1794 lines
68 KiB
JavaScript
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",
|
||
});
|
||
assert.deepEqual(metadata.threadActionSummary, {
|
||
actionCount: 11,
|
||
lifecycleActionCount: 5,
|
||
metadataActionCount: 2,
|
||
liveTurnActionCount: 2,
|
||
shellActionAvailable: true,
|
||
userInitiatedOnly: true,
|
||
labels: ["归档", "恢复", "分叉", "压缩", "回滚", "改名", "元数据", "活跃干预", "中断", "Shell", "取消订阅"],
|
||
});
|
||
assert.deepEqual(metadata.pluginGovernanceSummary, {
|
||
actionCount: 9,
|
||
lifecycleActionCount: 2,
|
||
shareActionCount: 4,
|
||
readActionCount: 3,
|
||
skillReadAvailable: true,
|
||
userInitiatedOnly: true,
|
||
labels: ["安装", "卸载", "读取", "Skill 读取", "共享保存", "共享拉取", "共享删除", "共享目标", "共享列表"],
|
||
});
|
||
assert.deepEqual(metadata.accountGovernanceSummary, {
|
||
actionCount: 6,
|
||
loginActionCount: 3,
|
||
sessionActionCount: 1,
|
||
tokenRefreshAvailable: true,
|
||
billingNudgeAvailable: true,
|
||
userInitiatedOnly: true,
|
||
labels: ["登录开始", "登录取消", "登录完成", "退出登录", "刷新令牌", "额度提醒"],
|
||
});
|
||
assert.deepEqual(metadata.configGovernanceSummary, {
|
||
actionCount: 5,
|
||
writeActionCount: 3,
|
||
reloadActionCount: 1,
|
||
readActionAvailable: true,
|
||
userInitiatedOnly: true,
|
||
labels: ["配置读取", "单项写入", "批量写入", "MCP 重载", "Skill 配置"],
|
||
});
|
||
assert.deepEqual(metadata.fileSystemGovernanceSummary, {
|
||
actionCount: 9,
|
||
readActionCount: 3,
|
||
writeActionCount: 3,
|
||
destructiveActionCount: 1,
|
||
watchActionCount: 2,
|
||
userInitiatedOnly: true,
|
||
labels: ["读取文件", "读取目录", "元数据", "写入文件", "创建目录", "复制", "删除", "监听", "取消监听"],
|
||
});
|
||
assert.deepEqual(metadata.commandSessionSummary, {
|
||
actionCount: 5,
|
||
controlActionCount: 3,
|
||
streamAvailable: true,
|
||
terminationAvailable: true,
|
||
sandboxedCommandAvailable: true,
|
||
userInitiatedOnly: true,
|
||
labels: ["执行命令", "写入 stdin", "调整 PTY", "终止命令", "输出流"],
|
||
});
|
||
assert.deepEqual(metadata.externalAgentGovernanceSummary, {
|
||
actionCount: 3,
|
||
importActionCount: 1,
|
||
notificationActionCount: 1,
|
||
detectActionAvailable: true,
|
||
userInitiatedOnly: true,
|
||
labels: ["迁移检测", "迁移导入", "导入完成"],
|
||
});
|
||
assert.deepEqual(metadata.marketplaceGovernanceSummary, {
|
||
actionCount: 3,
|
||
writeActionCount: 3,
|
||
upgradeAvailable: true,
|
||
userInitiatedOnly: true,
|
||
labels: ["添加市场", "移除市场", "升级市场"],
|
||
});
|
||
assert.deepEqual(metadata.experimentalFeatureGovernanceSummary, {
|
||
actionCount: 2,
|
||
writeActionCount: 1,
|
||
listAvailable: true,
|
||
userInitiatedOnly: true,
|
||
labels: ["实验列表", "启用设置"],
|
||
});
|
||
assert.deepEqual(metadata.reviewGovernanceSummary, {
|
||
actionCount: 1,
|
||
reviewStartAvailable: true,
|
||
userInitiatedOnly: true,
|
||
labels: ["启动审查"],
|
||
});
|
||
assert.deepEqual(metadata.windowsSandboxGovernanceSummary, {
|
||
actionCount: 3,
|
||
setupActionCount: 1,
|
||
readinessAvailable: true,
|
||
notificationAvailable: true,
|
||
userInitiatedOnly: true,
|
||
labels: ["准备检查", "启动设置", "设置完成"],
|
||
});
|
||
assert.deepEqual(metadata.fuzzyFileSearchSummary, {
|
||
eventCount: 2,
|
||
completedEventAvailable: true,
|
||
notificationOnly: true,
|
||
labels: ["搜索更新", "搜索完成"],
|
||
});
|
||
assert.deepEqual(metadata.mcpGovernanceSummary, {
|
||
actionCount: 5,
|
||
oauthActionCount: 2,
|
||
resourceActionCount: 1,
|
||
toolActionCount: 1,
|
||
elicitationAvailable: true,
|
||
userInitiatedOnly: true,
|
||
labels: ["OAuth 登录", "OAuth 完成", "资源读取", "工具调用", "交互请求"],
|
||
});
|
||
assert.deepEqual(metadata.userInteractionGovernanceSummary, {
|
||
actionCount: 1,
|
||
requestUserInputAvailable: true,
|
||
userInitiatedOnly: true,
|
||
labels: ["请求用户输入"],
|
||
});
|
||
assert.deepEqual(metadata.guardianGovernanceSummary, {
|
||
actionCount: 2,
|
||
approvalActionCount: 1,
|
||
permissionRequestEventAvailable: true,
|
||
userInitiatedOnly: true,
|
||
labels: ["Guardian 放行", "权限请求"],
|
||
});
|
||
assert.deepEqual(metadata.runtimeEventSummary, {
|
||
eventCount: 3,
|
||
processEventCount: 2,
|
||
rawResponseEventAvailable: true,
|
||
notificationOnly: true,
|
||
labels: ["进程输出", "进程退出", "原始响应完成"],
|
||
});
|
||
assert.deepEqual(metadata.extensionEventSummary, {
|
||
eventCount: 2,
|
||
skillChangeEventAvailable: true,
|
||
pluginInstallEventAvailable: true,
|
||
notificationOnly: true,
|
||
labels: ["Skill 变更", "插件安装"],
|
||
});
|
||
assert.deepEqual(metadata.threadLifecycleEventSummary, {
|
||
eventCount: 5,
|
||
archiveEventCount: 2,
|
||
nameEventAvailable: true,
|
||
closeEventAvailable: true,
|
||
notificationOnly: true,
|
||
labels: ["线程启动", "线程关闭", "已归档", "已恢复", "改名完成"],
|
||
});
|
||
assert.deepEqual(metadata.streamDeltaEventSummary, {
|
||
eventCount: 9,
|
||
reasoningDeltaEventCount: 3,
|
||
commandStreamEventCount: 2,
|
||
toolProgressEventAvailable: true,
|
||
fileChangeOutputEventAvailable: true,
|
||
notificationOnly: true,
|
||
labels: ["Agent 增量", "计划增量", "思考新增", "思考文本", "原始思考", "MCP 进度", "命令输出", "终端交互", "文件输出"],
|
||
});
|
||
|
||
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 summarizes stream deltas without leaking raw delta content", async () => {
|
||
const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_STREAM_DELTA_EVENTS;
|
||
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_STREAM_DELTA_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-stream-deltas",
|
||
taskType: "conversation_reply",
|
||
targetCodexThreadRef: "019d-app-server-thread",
|
||
targetCodexFolderRef: repoRoot,
|
||
executionPrompt: "检查 Codex 流式增量进度",
|
||
});
|
||
|
||
assert.equal(result.status, "completed");
|
||
assert.deepEqual(result.executionProgress.streamEvents, {
|
||
status: "completed",
|
||
agentDeltaCount: 1,
|
||
planDeltaCount: 1,
|
||
reasoningDeltaCount: 3,
|
||
toolProgressCount: 1,
|
||
commandOutputChunkCount: 1,
|
||
terminalInteractionCount: 1,
|
||
fileOutputChunkCount: 1,
|
||
});
|
||
const serialized = JSON.stringify(result.executionProgress);
|
||
assert.equal(serialized.includes("secret plan delta should not leak"), false);
|
||
assert.equal(serialized.includes("secret reasoning summary part should not leak"), false);
|
||
assert.equal(serialized.includes("secret reasoning summary delta should not leak"), false);
|
||
assert.equal(serialized.includes("secret raw reasoning delta should not leak"), false);
|
||
assert.equal(serialized.includes("secret mcp progress should not leak"), false);
|
||
assert.equal(serialized.includes("secret command output should not leak"), false);
|
||
assert.equal(serialized.includes("secret terminal input should not leak"), false);
|
||
assert.equal(serialized.includes("secret file output should not leak"), false);
|
||
} finally {
|
||
if (previous === undefined) {
|
||
delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_STREAM_DELTA_EVENTS;
|
||
} else {
|
||
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_EMIT_STREAM_DELTA_EVENTS = previous;
|
||
}
|
||
}
|
||
});
|
||
|
||
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 runner interrupts the active turn when the task is canceled while running", async () => {
|
||
const previous = process.env.BOSS_CODEX_APP_SERVER_FIXTURE_WAIT_FOR_INTERRUPT;
|
||
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_WAIT_FOR_INTERRUPT = "1";
|
||
try {
|
||
const interruptChecks = [];
|
||
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,
|
||
interruptPollIntervalMs: 10,
|
||
shouldInterruptActiveTurn: async (activeTurn) => {
|
||
interruptChecks.push(activeTurn);
|
||
return activeTurn.taskId === "task-turn-interrupt";
|
||
},
|
||
},
|
||
{
|
||
taskId: "task-turn-interrupt",
|
||
taskType: "conversation_reply",
|
||
targetCodexThreadRef: "active-thread-2",
|
||
targetCodexFolderRef: repoRoot,
|
||
executionPrompt: "继续执行一个可取消的长任务",
|
||
},
|
||
);
|
||
|
||
assert.equal(result.status, "interrupted");
|
||
assert.equal(result.threadId, "active-thread-2");
|
||
assert.equal(result.turnId, "turn-fixture");
|
||
assert.equal(result.turnControl, "interrupt");
|
||
assert.equal(result.replyBody, "已按用户要求中断当前 Codex turn。");
|
||
assert.equal(interruptChecks[0]?.taskId, "task-turn-interrupt");
|
||
assert.equal(interruptChecks[0]?.threadId, "active-thread-2");
|
||
assert.equal(interruptChecks[0]?.turnId, "turn-fixture");
|
||
} finally {
|
||
if (previous === undefined) {
|
||
delete process.env.BOSS_CODEX_APP_SERVER_FIXTURE_WAIT_FOR_INTERRUPT;
|
||
} else {
|
||
process.env.BOSS_CODEX_APP_SERVER_FIXTURE_WAIT_FOR_INTERRUPT = 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 rolls back a thread without starting a new turn or leaking history", async () => {
|
||
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-thread-rollback",
|
||
taskType: "conversation_reply",
|
||
intentCategory: "thread_rollback",
|
||
targetCodexThreadRef: "019d-app-server-thread",
|
||
targetCodexFolderRef: repoRoot,
|
||
rollbackNumTurns: 2,
|
||
executionPrompt: "回滚最近 2 轮 Codex 线程历史。",
|
||
});
|
||
|
||
assert.equal(result.status, "completed");
|
||
assert.equal(result.threadId, "019d-app-server-thread");
|
||
assert.equal(result.turnControl, "rollback");
|
||
assert.equal(result.rollback?.numTurns, 2);
|
||
assert.match(result.replyBody, /已回滚 Codex 线程最近 2 轮/);
|
||
assert.match(result.replyBody, /不会自动还原本地文件变更/);
|
||
assert.doesNotMatch(result.replyBody, /private rollback turn text/);
|
||
assert.equal(result.turnId, undefined);
|
||
});
|
||
|
||
test("codex app-server runner starts thread compaction without starting a normal turn or leaking history", async () => {
|
||
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-thread-compact",
|
||
taskType: "conversation_reply",
|
||
intentCategory: "thread_compact",
|
||
targetCodexThreadRef: "019d-app-server-thread",
|
||
targetCodexFolderRef: repoRoot,
|
||
executionPrompt: "压缩当前 Codex 线程上下文。",
|
||
});
|
||
|
||
assert.equal(result.status, "completed");
|
||
assert.equal(result.threadId, "019d-app-server-thread");
|
||
assert.equal(result.turnControl, "compact");
|
||
assert.equal(result.compaction?.status, "completed");
|
||
assert.match(result.replyBody, /已发起 Codex 线程上下文压缩/);
|
||
assert.doesNotMatch(JSON.stringify(result), /context-compaction-secret-should-not-leak/);
|
||
assert.doesNotMatch(JSON.stringify(result), /private compaction summary should not leak/);
|
||
assert.equal(result.turnId, undefined);
|
||
});
|
||
|
||
test("codex app-server runner archives and unarchives a thread without starting a normal turn", async () => {
|
||
const runnerConfig = getCodexAppServerRunnerConfig(process.env, {
|
||
codexAppServerEnabled: true,
|
||
codexAppServerCommand: process.execPath,
|
||
codexAppServerArgs: ["tests/fixtures/codex-app-server-runtime.mjs"],
|
||
codexAppServerWorkdir: repoRoot,
|
||
codexAppServerTimeoutMs: 5000,
|
||
});
|
||
|
||
const archiveResult = await executeCodexAppServerTask(runnerConfig, {
|
||
taskId: "task-thread-archive",
|
||
taskType: "conversation_reply",
|
||
intentCategory: "thread_archive",
|
||
targetCodexThreadRef: "019d-app-server-thread",
|
||
targetCodexFolderRef: repoRoot,
|
||
threadLifecycleAction: "archive",
|
||
executionPrompt: "归档当前 Codex 线程。",
|
||
});
|
||
const unarchiveResult = await executeCodexAppServerTask(runnerConfig, {
|
||
taskId: "task-thread-unarchive",
|
||
taskType: "conversation_reply",
|
||
intentCategory: "thread_unarchive",
|
||
targetCodexThreadRef: "019d-app-server-thread",
|
||
targetCodexFolderRef: repoRoot,
|
||
threadLifecycleAction: "unarchive",
|
||
executionPrompt: "恢复当前 Codex 线程。",
|
||
});
|
||
|
||
assert.equal(archiveResult.status, "completed");
|
||
assert.equal(archiveResult.threadId, "019d-app-server-thread");
|
||
assert.equal(archiveResult.turnControl, "archive");
|
||
assert.match(archiveResult.replyBody, /已归档 Codex 线程/);
|
||
assert.equal(archiveResult.turnId, undefined);
|
||
assert.equal(unarchiveResult.status, "completed");
|
||
assert.equal(unarchiveResult.threadId, "019d-app-server-thread");
|
||
assert.equal(unarchiveResult.turnControl, "unarchive");
|
||
assert.match(unarchiveResult.replyBody, /已恢复 Codex 线程/);
|
||
assert.equal(unarchiveResult.turnId, undefined);
|
||
|
||
const serialized = JSON.stringify({ archiveResult, unarchiveResult });
|
||
assert.doesNotMatch(serialized, /thread-archive-secret-should-not-leak/);
|
||
assert.doesNotMatch(serialized, /thread-unarchive-secret-should-not-leak/);
|
||
assert.doesNotMatch(serialized, /private unarchived thread name should not leak/);
|
||
});
|
||
|
||
test("codex app-server runner renames a thread without starting a normal turn", async () => {
|
||
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-thread-rename",
|
||
taskType: "conversation_reply",
|
||
intentCategory: "thread_rename",
|
||
targetCodexThreadRef: "019d-app-server-thread",
|
||
targetCodexFolderRef: repoRoot,
|
||
threadRenameName: "Boss 量产治理线程",
|
||
executionPrompt: "同步 Codex 线程名称。",
|
||
});
|
||
|
||
assert.equal(result.status, "completed");
|
||
assert.equal(result.threadId, "019d-app-server-thread");
|
||
assert.equal(result.turnControl, "rename");
|
||
assert.equal(result.threadRename?.name, "Boss 量产治理线程");
|
||
assert.match(result.replyBody, /已同步 Codex 线程名称/);
|
||
assert.equal(result.turnId, undefined);
|
||
assert.doesNotMatch(JSON.stringify(result), /thread-name-secret-should-not-leak/);
|
||
});
|
||
|
||
test("codex app-server runner syncs a thread goal without starting a normal turn", async () => {
|
||
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-thread-goal-sync",
|
||
taskType: "conversation_reply",
|
||
intentCategory: "thread_goal_sync",
|
||
targetCodexThreadRef: "019d-app-server-thread",
|
||
targetCodexFolderRef: repoRoot,
|
||
threadGoalObjective: "完成 App Server 线程目标双向同步。",
|
||
threadGoalStatus: "active",
|
||
threadGoalTokenBudget: 120000,
|
||
executionPrompt: "同步 Codex 线程目标。",
|
||
});
|
||
|
||
assert.equal(result.status, "completed");
|
||
assert.equal(result.threadId, "019d-app-server-thread");
|
||
assert.equal(result.turnControl, "goal_sync");
|
||
assert.equal(result.threadGoal?.objective, "完成 App Server 线程目标双向同步。");
|
||
assert.equal(result.threadGoal?.status, "active");
|
||
assert.equal(result.threadGoal?.tokenBudget, 120000);
|
||
assert.match(result.replyBody, /已同步 Codex 线程目标/);
|
||
assert.equal(result.turnId, undefined);
|
||
assert.doesNotMatch(JSON.stringify(result), /thread-goal-secret-should-not-leak/);
|
||
});
|
||
|
||
test("codex app-server runner syncs thread git metadata without starting a normal turn", async () => {
|
||
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-thread-metadata-sync",
|
||
taskType: "conversation_reply",
|
||
intentCategory: "thread_metadata_sync",
|
||
targetCodexThreadRef: "019d-app-server-thread",
|
||
targetCodexFolderRef: repoRoot,
|
||
threadMetadataGitInfo: {
|
||
sha: "0186ef7",
|
||
branch: "codex/wechat-native-ui-rollback",
|
||
originUrl: "https://git.hyzq.site/krisolo/boss.git",
|
||
},
|
||
executionPrompt: "同步 Codex 线程 Git 元数据。",
|
||
});
|
||
|
||
assert.equal(result.status, "completed");
|
||
assert.equal(result.threadId, "019d-app-server-thread");
|
||
assert.equal(result.turnControl, "metadata_sync");
|
||
assert.deepEqual(result.threadMetadata?.gitInfo, {
|
||
sha: "0186ef7",
|
||
branch: "codex/wechat-native-ui-rollback",
|
||
originUrl: "https://git.hyzq.site/krisolo/boss.git",
|
||
});
|
||
assert.match(result.replyBody, /已同步 Codex 线程 Git 元数据/);
|
||
assert.equal(result.turnId, undefined);
|
||
assert.doesNotMatch(JSON.stringify(result), /thread-metadata-secret-should-not-leak/);
|
||
});
|
||
|
||
test("codex app-server runner forks a thread without starting a normal turn", async () => {
|
||
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-thread-fork",
|
||
taskType: "conversation_reply",
|
||
intentCategory: "thread_fork",
|
||
targetCodexThreadRef: "019d-app-server-thread",
|
||
targetCodexFolderRef: repoRoot,
|
||
threadForkEphemeral: false,
|
||
executionPrompt: "分叉当前 Codex 线程。",
|
||
});
|
||
|
||
assert.equal(result.status, "completed");
|
||
assert.equal(result.threadId, "019d-app-server-thread");
|
||
assert.equal(result.turnControl, "fork");
|
||
assert.equal(result.threadFork?.sourceThreadId, "019d-app-server-thread");
|
||
assert.equal(result.threadFork?.forkedThreadId, "019d-app-server-thread-fork");
|
||
assert.equal(result.threadFork?.forkedThreadName, "Forked working thread");
|
||
assert.equal(result.threadFork?.ephemeral, false);
|
||
assert.equal(result.turnId, undefined);
|
||
assert.doesNotMatch(JSON.stringify(result), /thread-fork-secret-should-not-leak/);
|
||
});
|
||
|
||
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;
|
||
}
|
||
}
|
||
});
|