#!/usr/bin/env bun import assert from "node:assert/strict"; import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import http from "node:http"; import https from "node:https"; import os from "node:os"; import path from "node:path"; import { handleCodeAgentChat, validateCodeAgentChatSchema } from "../internal/cloud/code-agent-chat.ts"; import { codexAppServerArgs, codexAppServerProviderBaseUrl, createCodexAppServerJsonLineClient, createCodexStdioSessionManager } from "../internal/cloud/codex-stdio-session.ts"; import { createAppServerTurnState } from "../internal/cloud/codex-stdio-session-turn-state.ts"; import { classifyCodexRunnerCapability, classifyCodeAgentChatReadiness, isHttpNon2xxStatus, summarizeCodeAgentPayload } from "./src/code-agent-response-contract.mjs"; const defaultLiveUrl = "http://74.48.78.17:17667/"; const defaultLiveMessage = "请用一句话回复 HWLAB Code Agent readiness。"; const defaultLiveTimeoutMs = 45000; const args = parseArgs(process.argv.slice(2)); if (args.mode === "help") { process.stdout.write(`${JSON.stringify(usage(), null, 2)}\n`); } else if (args.mode === "live") { const report = await runLiveReadinessSmoke(args); process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); process.exitCode = report.status === "pass" ? 0 : 2; } else { await runLocalContractSmoke(); } function logOk(name) { process.stdout.write(`[code-agent-chat-smoke] ok ${name}\n`); } function runnerTraceLabels(payload) { return Array.isArray(payload?.runnerTrace?.events) ? payload.runnerTrace.events.map((event) => event?.label ?? event).filter(Boolean) : []; } async function runJsonLineClientApprovalSmoke() { const fakeCodex = await createFakeJsonLineCodexCommand(); const events = []; const client = createCodexAppServerJsonLineClient({ command: fakeCodex.command, args: ["app-server", "--listen", "stdio://"], cwd: fakeCodex.root, env: { ...process.env, PATH: `${path.dirname(fakeCodex.command)}:${process.env.PATH ?? ""}` }, onNotification: (message) => events.push(message) }); try { await client.initialize(2000); const thread = await client.startThread({ model: "gpt-test", cwd: fakeCodex.root, sandbox: "workspace-write" }, 2000); assert.equal(thread.threadId, "thread_jsonline_test"); const turn = await client.startTurn({ threadId: thread.threadId, prompt: "trigger approval", model: "gpt-test", cwd: fakeCodex.root }, 2000); assert.equal(turn.turnId, "turn_jsonline_test"); assert.equal(turn.result.approvalDecision, "approved_for_session"); assert.equal(turn.result.spawnedFrom, "native"); assert.ok(events.some((event) => event.method === "client/request/handled" && event.params?.decision === "approved_for_session")); assert.ok(events.some((event) => event.method === "item/completed" && event.params?.item?.type === "commandExecution")); } finally { client.close(); await rm(fakeCodex.root, { recursive: true, force: true }); } logOk("codex app-server JSON-line client approves command requests and uses native binary directly"); } function runTurnStateVisibilitySmoke() { const events = []; const state = createAppServerTurnState({ traceRecorder: { append: (event) => { events.push(event); return event; }, appendAssistantDelta: (event) => { events.push({ ...event, label: "assistant:stream" }); return event; } }, session: { sessionId: "ses_visibility_smoke", status: "busy", turn: 1 } }); const command = "printf ".padEnd(3000, "c"); const stdout = "x".repeat(5000); state.handle({ method: "thread/started", params: { thread: { id: "thread_visibility_smoke" } } }); state.handle({ method: "turn/started", params: { turn: { id: "turn_visibility_smoke" } } }); state.handle({ method: "client/request/handled", params: { method: "item/commandExecution/requestApproval", decision: "approved", itemId: "cmd_visibility" } }); state.handle({ method: "item/completed", params: { item: { id: "cmd_visibility", type: "commandExecution", status: "completed", exitCode: 0, command, stdout } } }); const clientEvent = events.find((event) => event.label === "client/request:approved"); const commandEvent = events.find((event) => event.label === "item/commandExecution:completed"); assert.equal(clientEvent?.status, "completed"); assert.equal(commandEvent?.commandBytes, Buffer.byteLength(command, "utf8")); assert.equal(commandEvent?.outputBytes, Buffer.byteLength(stdout, "utf8")); assert.equal(commandEvent?.commandTruncated, true); assert.ok(commandEvent?.stdoutSummary?.length <= 2100); logOk("codex app-server turn trace bounds large command/output while preserving byte counts"); } async function runLocalContractSmoke() { const echoReadiness = classifyCodeAgentChatReadiness({ conversationId: "cnv_code-agent-chat-smoke-echo", sessionId: "cnv_code-agent-chat-smoke-echo", messageId: "msg_code-agent-chat-smoke-echo", status: "completed", createdAt: "2026-05-22T00:00:00.000Z", updatedAt: "2026-05-22T00:00:00.000Z", traceId: "trc_code-agent-chat-smoke-echo", provider: "echo-mock", model: "mock-model", backend: "hwlab-cloud-api/echo-mock", reply: { messageId: "msg_code-agent-chat-smoke-echo", role: "assistant", content: "mock reply", createdAt: "2026-05-22T00:00:00.000Z" } }, { realDevLive: true, httpStatus: 200 }); assert.equal(echoReadiness.status, "blocked"); assert.equal(echoReadiness.level, "BLOCKED/untrusted-completion"); assert.equal(echoReadiness.blocker, "untrusted-completion"); assert.equal(echoReadiness.devLiveReplyPass, false); assert.match(echoReadiness.reason, /echo\/mock\/stub/u); logOk("echo/mock completion is not real DEV-LIVE pass"); for (const [index, message] of [ "列出可以使用的skill", "pwd", "ls .", "rg --files .", "请用cat读取 package.json" ].entries()) { const blocked = await handleCodeAgentChat( { conversationId: `cnv_code-agent-chat-blocked-${index}`, traceId: `trc_code-agent-chat-blocked-${index}`, message }, { now: () => "2026-05-22T00:01:00.000Z", env: { PATH: "", HWLAB_CODE_AGENT_PROVIDER: "codex-cli", HWLAB_CODE_AGENT_MODEL: "gpt-test", HWLAB_CODE_AGENT_CODEX_COMMAND: "/tmp/hwlab-missing-codex" } } ); validateCodeAgentChatSchema(blocked); assert.equal(blocked.status, "failed"); assert.equal(blocked.provider, "codex-stdio"); assert.equal(blocked.backend, "hwlab-cloud-api/codex-app-server-stdio"); assert.equal(blocked.error.code, "codex_cli_binary_missing"); assert.equal(Object.hasOwn(blocked, "reply"), false); assert.deepEqual(blocked.toolCalls, []); assert.ok(blocked.runnerLimitations.includes("no-controlled-readonly-fallback")); assert.ok(blocked.runnerLimitations.includes("no-text-fallback")); assert.equal(blocked.availability.ready, false); assert.equal(blocked.availability.partialReady, false); assert.equal(blocked.runnerTrace.traceId, `trc_code-agent-chat-blocked-${index}`); assert.ok(runnerTraceLabels(blocked).includes("request:accepted")); assert.ok(runnerTraceLabels(blocked).includes("codex-stdio:blocked")); assert.equal(blocked.runnerTrace.lastEvent.errorCode, "codex_cli_binary_missing"); assert.equal(blocked.runnerTrace.lastEvent.waitingFor, "codex-stdio-readiness"); assert.equal(JSON.stringify(blocked).includes("sk-"), false); } logOk("skills/pwd/ls/rg/cat prompts fail with Codex stdio blocker instead of local fallback"); await runJsonLineClientApprovalSmoke(); await runTurnStateVisibilitySmoke(); const stdioWorkspaceRoot = await mkdtemp(path.join(os.tmpdir(), `hwlab-code-agent-stdio-smoke-${process.pid}-`)); const stdioWorkspace = path.join(stdioWorkspaceRoot, "workspace"); const stdioCodexHome = path.join(stdioWorkspaceRoot, "codex-home"); const stdioSkillsDir = path.join(stdioWorkspaceRoot, "skills"); await mkdir(stdioWorkspace, { recursive: true }); await mkdir(path.join(stdioSkillsDir, "aaa-stdio-smoke-skill"), { recursive: true }); await mkdir(stdioCodexHome, { recursive: true }); await writeFile(path.join(stdioWorkspace, "README.md"), "stdio smoke workspace\n"); await writeFile(path.join(stdioCodexHome, "config.toml"), "model = \"gpt-test\"\n"); await writeFile(path.join(stdioCodexHome, "auth.json"), "{\"auth\":\"redacted-test-placeholder\"}\n"); await writeFile(path.join(stdioSkillsDir, "aaa-stdio-smoke-skill", "SKILL.md"), [ "---", "name: aaa-stdio-smoke-skill", "description: Stdio smoke skill summary.", "---", "", "# Stdio Smoke" ].join("\n")); let fakeCodex = null; try { fakeCodex = await createFakeCodexCommand(); const rpcCalls = []; const stdioManager = createCodexStdioSessionManager({ idFactory: () => "ses_code_agent_chat_smoke_stdio", createRpcClient: async () => createFakeAppServerClient({ rpcCalls }) }); const stdioEnv = { PATH: process.env.PATH, OPENAI_API_KEY: "test-openai-key-material", CODEX_HOME: stdioCodexHome, HWLAB_CODE_AGENT_PROVIDER: "codex-stdio", HWLAB_CODE_AGENT_MODEL: "gpt-test", HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command, HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1", HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned", HWLAB_CODE_AGENT_CODEX_WORKSPACE: stdioWorkspace, HWLAB_CODE_AGENT_CODEX_SANDBOX: "workspace-write", HWLAB_CODE_AGENT_SKILLS_DIRS: stdioSkillsDir, HWLAB_CODE_AGENT_SKILLS_STRICT: "1", HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses" }; const stdioPwd = await handleCodeAgentChat( { conversationId: "cnv_code-agent-chat-stdio", traceId: "trc_code-agent-chat-stdio-pwd", message: "列出可以使用的skill" }, { now: () => "2026-05-22T00:02:10.000Z", codexStdioManager: stdioManager, env: stdioEnv, skillsDirs: [stdioSkillsDir], skillsDirsExact: true } ); validateCodeAgentChatSchema(stdioPwd); assert.equal(stdioPwd.status, "completed"); assert.equal(stdioPwd.provider, "codex-stdio"); assert.equal(stdioPwd.backend, "hwlab-cloud-api/codex-app-server-stdio"); assert.equal(stdioPwd.runner.kind, "codex-app-server-stdio-runner"); assert.equal(stdioPwd.sessionMode, "codex-app-server-stdio-long-lived"); assert.equal(stdioPwd.implementationType, "repo-owned-codex-app-server-stdio-session"); assert.equal(stdioPwd.capabilityLevel, "long-lived-codex-stdio-session"); assert.equal(stdioPwd.workspace, stdioWorkspace); assert.equal(stdioPwd.sandbox, "workspace-write"); assert.equal(stdioPwd.runner.codexStdio, true); assert.equal(stdioPwd.runner.writeCapable, true); assert.equal(stdioPwd.runner.durableSession, true); assert.equal(stdioPwd.session.longLivedSession, true); assert.equal(stdioPwd.session.codexStdio, true); assert.equal(stdioPwd.sessionReuse.reused, false); assert.equal(stdioPwd.sessionReuse.turn, 1); assert.equal(stdioPwd.codexStdioFeasibility.ready, true); assert.equal(stdioPwd.codexStdioFeasibility.runtimeContract.stdioProtocol.status, "wired"); assert.equal(stdioPwd.codexStdioFeasibility.runtimeContract.lifecycleSupervisor.status, "present"); assert.equal(stdioPwd.codexStdioFeasibility.runtimeContract.workspaceMount.status, "ready"); assert.equal(stdioPwd.codexStdioFeasibility.runtimeContract.codexHome.status, "ready"); assert.equal(stdioPwd.longLivedSessionGate.status, "pass"); assert.equal(stdioPwd.longLivedSessionGate.pass, true); assert.equal(stdioPwd.providerTrace.toolName, "codex-app-server.thread/start+turn/start"); assert.equal(stdioPwd.providerTrace.threadId, "thread_code_agent_chat_smoke_stdio"); assert.equal(Object.hasOwn(stdioPwd.providerTrace, "sidecarOnly"), false); const firstTurnCalls = rpcCalls.filter((call) => call.method !== "initialize"); assert.equal(firstTurnCalls[0].method, "thread/start"); assert.equal(firstTurnCalls[1].method, "turn/start"); assert.match(firstTurnCalls[1].args.prompt, /列出可以使用的skill/u); assert.ok(stdioPwd.toolCalls.some((call) => call.name === "codex-app-server.thread/start+turn/start" && call.status === "completed")); assert.ok(stdioPwd.toolCalls.some((call) => call.name === "skills.discover" && call.status === "completed")); assert.equal(stdioPwd.skills.status, "ready"); assert.ok(stdioPwd.skills.items.some((skill) => skill.name === "aaa-stdio-smoke-skill")); assert.equal(stdioPwd.runnerTrace.runnerKind, "codex-app-server-stdio-runner"); assert.equal(stdioPwd.runnerTrace.sessionMode, "codex-app-server-stdio-long-lived"); assert.ok(runnerTraceLabels(stdioPwd).includes("session:created")); assert.ok(runnerTraceLabels(stdioPwd).includes("prompt:sent")); assert.ok(runnerTraceLabels(stdioPwd).includes("tool:codex-app-server.thread/start+turn/start:started")); assert.ok(runnerTraceLabels(stdioPwd).includes("tool:codex-app-server.thread/start+turn/start:output_chunk")); assert.ok(runnerTraceLabels(stdioPwd).includes("tool:codex-app-server.thread/start+turn/start:completed")); assert.ok(stdioPwd.runnerTrace.assistantStreams.some((stream) => stream.label === "assistant:stream" && stream.chunkCount >= 1)); assert.ok(runnerTraceLabels(stdioPwd).includes("assistant:completed")); assert.equal(classifyCodexRunnerCapability(stdioPwd, { httpStatus: 200 }).capabilityPass, true); const stdioSecondTurn = await handleCodeAgentChat( { conversationId: "cnv_code-agent-chat-stdio", traceId: "trc_code-agent-chat-stdio-reuse", message: "继续复用同一个 session 并 ls ." }, { now: () => "2026-05-22T00:02:11.000Z", codexStdioManager: stdioManager, env: stdioEnv, skillsDirs: [stdioSkillsDir], skillsDirsExact: true } ); validateCodeAgentChatSchema(stdioSecondTurn); assert.equal(stdioSecondTurn.status, "completed"); assert.equal(stdioSecondTurn.provider, "codex-stdio"); assert.equal(stdioSecondTurn.sessionId, stdioPwd.sessionId); assert.equal(stdioSecondTurn.session.sessionId, stdioPwd.session.sessionId); assert.equal(stdioSecondTurn.session.threadId, "thread_code_agent_chat_smoke_stdio"); assert.equal(stdioSecondTurn.sessionReuse.reused, true); assert.equal(stdioSecondTurn.sessionReuse.turn, 2); assert.equal(stdioSecondTurn.providerTrace.toolName, "codex-app-server.thread/resume+turn/start"); assert.equal(Object.hasOwn(stdioSecondTurn.providerTrace, "sidecarOnly"), false); assert.ok(stdioSecondTurn.toolCalls.some((call) => call.name === "codex-app-server.thread/resume+turn/start" && call.status === "completed")); assert.ok(runnerTraceLabels(stdioSecondTurn).includes("session:reused")); assert.ok(runnerTraceLabels(stdioSecondTurn).includes("prompt:sent")); assert.ok(runnerTraceLabels(stdioSecondTurn).includes("tool:codex-app-server.thread/resume+turn/start:completed")); assert.equal(classifyCodexRunnerCapability(stdioSecondTurn, { httpStatus: 200 }).capabilityPass, true); assert.equal(JSON.stringify(stdioSecondTurn).includes("test-openai-key-material"), false); logOk("codex stdio runner passes long-lived workspace-write session capability"); } finally { await rm(stdioWorkspaceRoot, { recursive: true, force: true }); if (fakeCodex) { await rm(fakeCodex.root, { recursive: true, force: true }); } } const completedCodexPayload = { conversationId: "cnv_code-agent-chat-stdio-http", sessionId: "ses_code-agent-chat-stdio-http", messageId: "msg_code-agent-chat-stdio-http", status: "completed", createdAt: "2026-05-22T00:05:00.000Z", updatedAt: "2026-05-22T00:05:00.000Z", traceId: "trc_code-agent-chat-stdio-http", provider: "codex-stdio", model: "gpt-5.5", backend: "hwlab-cloud-api/codex-app-server-stdio", runner: { kind: "codex-app-server-stdio-runner", codexStdio: true, writeCapable: true, durableSession: true }, sessionMode: "codex-app-server-stdio-long-lived", implementationType: "repo-owned-codex-app-server-stdio-session", capabilityLevel: "long-lived-codex-stdio-session", reply: { messageId: "msg_code-agent-chat-stdio-http", role: "assistant", content: "real stdio reply", createdAt: "2026-05-22T00:05:00.000Z" } }; for (const status of [400, 401, 403, 429, 500, 502, 503, 504]) { const readiness = classifyCodeAgentChatReadiness(completedCodexPayload, { realDevLive: true, httpStatus: status }); assert.equal(readiness.status, "blocked", `HTTP ${status} must block`); assert.equal(readiness.blocker, "provider-upstream", `HTTP ${status} blocker`); assert.equal(readiness.providerStatus, status, `HTTP ${status} providerStatus`); assert.equal(readiness.devLiveReplyPass, false, `HTTP ${status} devLiveReplyPass`); assert.match(readiness.reason, /HTTP non-2xx|upstream response/u, `HTTP ${status} reason`); } logOk("non-2xx completion-looking payloads are blocked"); const failedProvider401Payload = { ...completedCodexPayload, status: "failed", reply: undefined, error: { code: "codex_stdio_failed", providerStatus: 401, layer: "runner", message: "provider returned HTTP 401" }, availability: { status: "codex-stdio-feasible", ready: true } }; const provider401Readiness = classifyCodeAgentChatReadiness(failedProvider401Payload, { realDevLive: true, httpStatus: 200 }); assert.equal(provider401Readiness.status, "blocked"); assert.equal(provider401Readiness.level, "BLOCKED/provider"); assert.equal(provider401Readiness.blocker, "provider-upstream"); assert.equal(provider401Readiness.providerStatus, 401); assert.equal(provider401Readiness.devLiveReplyPass, false); logOk("providerStatus=401 blocks even when readiness fields look feasible"); const providerEnv = { HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses", OPENAI_API_KEY: "test-openai-key-material" }; assert.equal(codexAppServerProviderBaseUrl(providerEnv), "http://127.0.0.1:49280"); const providerArgs = codexAppServerArgs(providerEnv).join(" "); assert.match(providerArgs, /model_providers\.OpenAI\.base_url="http:\/\/127\.0\.0\.1:49280"/u); assert.match(providerArgs, /model_providers\.OpenAI\.name="OpenAI"/u); assert.match(providerArgs, /model_providers\.OpenAI\.wire_api="responses"/u); assert.match(providerArgs, /model_providers\.OpenAI\.requires_openai_auth=true/u); assert.equal(providerArgs.includes("test-openai-key-material"), false); logOk("Codex app-server provider override keeps auth material out of argv"); process.stdout.write("[code-agent-chat-smoke] passed\n"); } async function runLiveReadinessSmoke(args) { const endpoint = agentChatEndpoint(args.url); const traceId = protocolId("trc_code-agent-chat-live"); const conversationId = protocolId("cnv_code-agent-chat-live"); let httpStatus = null; let payload = null; let transportError = null; try { const response = await postJson(endpoint, { headers: { "x-trace-id": traceId }, timeoutMs: args.timeoutMs, body: { conversationId, traceId, projectId: "prj_hwlab-cloud-workbench", message: args.message } }); httpStatus = response.status; payload = response.json; } catch (error) { transportError = error instanceof Error ? error.message : String(error); } const readiness = payload || isHttpNon2xxStatus(httpStatus) ? classifyCodeAgentChatReadiness(payload, { realDevLive: true, httpStatus }) : { status: "blocked", level: "BLOCKED", blocker: "transport", devLiveReplyPass: false, reason: "real DEV /v1/agent/chat did not return a JSON Code Agent payload" }; if (payload && readiness.status === "pass") { try { validateCodeAgentChatSchema(payload); } catch (error) { readiness.status = "blocked"; readiness.level = "BLOCKED"; readiness.blocker = "schema"; readiness.devLiveReplyPass = false; readiness.reason = error instanceof Error ? error.message : String(error); } } return { status: readiness.status, mode: "live-readiness", url: endpoint.href, httpStatus, readiness, response: summarizePayload(payload, { httpStatus, traceId }), ...(transportError ? { transportError } : {}), safety: { prodTouched: false, servicesRestarted: false, readsSecretValues: false, printsAssistantReply: false, statement: "This smoke posts only to the configured DEV Code Agent chat endpoint and prints reply presence, not reply content or secret values." } }; } function summarizePayload(payload, options = {}) { const responseSummary = summarizeCodeAgentPayload(payload, options); const assistantReply = responseSummary?.hasReply === true && typeof payload?.reply?.content === "string" ? payload.reply.content.trim() : ""; return { status: responseSummary?.status ?? null, provider: responseSummary?.provider ?? null, model: responseSummary?.model ?? null, backend: responseSummary?.backend ?? null, runner: responseSummary?.runner ?? null, workspace: responseSummary?.workspace ?? null, sandbox: responseSummary?.sandbox ?? null, capabilityLevel: responseSummary?.capabilityLevel ?? null, sessionMode: responseSummary?.sessionMode ?? null, sessionReuse: responseSummary?.sessionReuse ?? null, implementationType: responseSummary?.implementationType ?? null, runnerLimitations: responseSummary?.runnerLimitations ?? [], toolCalls: responseSummary?.toolCalls ?? [], skills: responseSummary?.skills ?? null, runnerTrace: responseSummary?.runnerTrace ?? null, codexStdioFeasibility: responseSummary?.codexStdioFeasibility ?? null, session: responseSummary?.session ?? null, longLivedSessionGate: responseSummary?.longLivedSessionGate ?? null, assistantReplyNonEmpty: responseSummary?.hasReply === true, assistantReplyLength: assistantReply.length, error: responseSummary?.error ?? null }; } function parseArgs(argv) { const parsed = { mode: "local", url: defaultLiveUrl, message: defaultLiveMessage, timeoutMs: defaultLiveTimeoutMs }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === "--live") { parsed.mode = "live"; } else if (arg === "--url") { index += 1; if (!argv[index]) throw new Error("--url requires a value"); parsed.url = argv[index]; } else if (arg === "--message") { index += 1; if (!argv[index]) throw new Error("--message requires a value"); parsed.message = argv[index]; } else if (arg === "--timeout-ms") { index += 1; if (!argv[index]) throw new Error("--timeout-ms requires a value"); const timeoutMs = Number.parseInt(argv[index], 10); if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) { throw new Error("--timeout-ms must be a positive integer"); } parsed.timeoutMs = timeoutMs; } else if (arg === "--help") { return { mode: "help" }; } else { throw new Error(`unknown argument ${arg}`); } } return parsed; } function usage() { return { status: "usage", command: "node scripts/code-agent-chat-smoke.mjs [--live --url http://74.48.78.17:17667/ --timeout-ms 45000]", notes: [ "Default mode is local schema/readiness contract only and cannot pass #143 DEV-LIVE.", "--live posts a minimal chat prompt to the current G14 DEV public endpoint and passes only on completed plus non-empty assistant reply.", "Live chat first token can exceed 10s on healthy Codex stdio cold turns; default timeout is 45000ms and can be overridden with --timeout-ms.", "provider_unavailable with missing OPENAI_API_KEY or missing/forbidden HWLAB_CODE_AGENT_OPENAI_BASE_URL is reported as BLOCKED/provider-config." ] }; } async function createFakeCodexCommand() { const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-fake-codex-")); const packageRoot = path.join(root, "node_modules", "@openai", "codex"); const command = path.join(packageRoot, "bin", "codex.js"); const native = path.join(root, "node_modules", "@openai", "codex-linux-x64", "vendor", "x86_64-unknown-linux-musl", "codex", "codex"); await mkdir(path.dirname(command), { recursive: true }); await mkdir(path.dirname(native), { recursive: true }); await writeFile(path.join(packageRoot, "package.json"), JSON.stringify({ name: "@openai/codex", version: "0.128.0" }), "utf8"); await writeFile(command, "#!/usr/bin/env node\nconsole.log('codex 0.128.0');\n", "utf8"); await writeFile(native, "#!/bin/sh\nexit 0\n", "utf8"); await chmod(command, 0o755); await chmod(native, 0o755); return { root, command }; } async function createFakeJsonLineCodexCommand() { const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-fake-jsonline-codex-")); const packageRoot = path.join(root, "node_modules", "@openai", "codex"); const command = path.join(packageRoot, "bin", "codex.js"); const native = path.join(root, "node_modules", "@openai", "codex-linux-x64", "vendor", "x86_64-unknown-linux-musl", "codex", "codex"); await mkdir(path.dirname(command), { recursive: true }); await mkdir(path.dirname(native), { recursive: true }); await writeFile(path.join(packageRoot, "package.json"), JSON.stringify({ name: "@openai/codex", version: "0.128.0" }), "utf8"); await writeFile(command, "#!/usr/bin/env node\nconsole.log('node-wrapper-should-not-run');\n", "utf8"); await writeFile(native, [ "#!/usr/bin/env node", "const readline = require('node:readline');", "let turnRequestId = null;", "let approvalDecision = null;", "const rl = readline.createInterface({ input: process.stdin });", "function send(payload) { process.stdout.write(JSON.stringify(payload) + '\\n'); }", "rl.on('line', (line) => {", " if (!line.trim()) return;", " const payload = JSON.parse(line);", " if (payload.id === 'approval-test' && !payload.method) {", " approvalDecision = payload.result && payload.result.decision;", " send({ method: 'item/completed', params: { item: { id: 'cmd_jsonline_test', type: 'commandExecution', status: 'completed', exitCode: 0, command: 'echo approval', stdout: 'approval ok' } } });", " send({ id: turnRequestId, result: { turn: { id: 'turn_jsonline_test' }, approvalDecision, spawnedFrom: 'native' } });", " return;", " }", " if (payload.method === 'initialize') { send({ id: payload.id, result: { initialized: true } }); return; }", " if (payload.method === 'initialized') return;", " if (payload.method === 'thread/start') {", " send({ method: 'thread/started', params: { thread: { id: 'thread_jsonline_test' } } });", " send({ id: payload.id, result: { thread: { id: 'thread_jsonline_test' } } });", " return;", " }", " if (payload.method === 'turn/start') {", " turnRequestId = payload.id;", " send({ method: 'turn/started', params: { turn: { id: 'turn_jsonline_test' } } });", " send({ id: 'approval-test', method: 'item/commandExecution/requestApproval', params: { itemId: 'cmd_jsonline_test', availableDecisions: ['approved_for_session', 'decline'] } });", " return;", " }", " send({ id: payload.id, error: { code: -32601, message: 'unknown method' } });", "});", "setTimeout(() => {}, 30000);" ].join("\n"), "utf8"); await chmod(command, 0o755); await chmod(native, 0o755); return { root, command }; } function createFakeAppServerClient({ rpcCalls }) { let notificationHandler = null; let turn = 0; return { async initialize() { rpcCalls.push({ method: "initialize" }); return { initialized: true }; }, setNotificationHandler(handler) { notificationHandler = typeof handler === "function" ? handler : null; }, async startThread(args) { rpcCalls.push({ method: "thread/start", args }); notificationHandler?.({ method: "thread/started", params: { thread: { id: "thread_code_agent_chat_smoke_stdio" } } }); return { threadId: "thread_code_agent_chat_smoke_stdio" }; }, async resumeThread(args) { rpcCalls.push({ method: "thread/resume", args }); notificationHandler?.({ method: "thread/started", params: { thread: { id: args.threadId } } }); return { threadId: args.threadId }; }, async startTurn(args) { rpcCalls.push({ method: "turn/start", args }); turn += 1; const turnId = `turn_code_agent_chat_smoke_${turn}`; const text = `codex-app-server fixture reply for ${args.threadId ?? "new-thread"}`; notificationHandler?.({ method: "turn/started", params: { turn: { id: turnId } } }); notificationHandler?.({ method: "item/agentMessage/delta", params: { itemId: `item_${turn}`, delta: text } }); notificationHandler?.({ method: "item/completed", params: { item: { id: `item_${turn}`, type: "agentMessage", text } } }); notificationHandler?.({ method: "turn/completed", params: { turn: { id: turnId, status: "completed" } } }); return { turnId }; }, close() {} }; } function agentChatEndpoint(value) { const url = new URL(value); if (url.pathname.endsWith("/v1/agent/chat")) return url; return new URL("/v1/agent/chat", url); } function postJson(url, { headers = {}, body, timeoutMs = 10000 }) { const client = url.protocol === "https:" ? https : http; const payload = JSON.stringify(body); return new Promise((resolve, reject) => { const request = client.request( url, { method: "POST", headers: { "content-type": "application/json", "content-length": Buffer.byteLength(payload), ...headers }, insecureHTTPParser: true, timeout: timeoutMs }, (response) => { const chunks = []; response.on("data", (chunk) => chunks.push(chunk)); response.on("end", () => { const text = Buffer.concat(chunks).toString("utf8"); resolve({ status: response.statusCode ?? null, body: text, json: parseJsonOrNull(text) }); }); } ); request.on("timeout", () => request.destroy(new Error(`request timed out after ${timeoutMs}ms`))); request.on("error", reject); request.end(payload); }); } function parseJsonOrNull(value) { try { return JSON.parse(value); } catch { return null; } } function protocolId(prefix) { return `${prefix}_${Date.now().toString(36)}`; }