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; } } });