596 lines
21 KiB
JavaScript
596 lines
21 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 {
|
|
executeCodexAppServerTask,
|
|
getCodexAppServerRunnerConfig,
|
|
shouldUseCodexAppServerTaskRunner,
|
|
} from "../local-agent/codex-app-server-runner.mjs";
|
|
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
|
|
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 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;
|
|
}
|
|
}
|
|
});
|