diff --git a/scripts/lib/docker-e2e-scenarios.mjs b/scripts/lib/docker-e2e-scenarios.mjs index db57e4c5a877..884690747d63 100644 --- a/scripts/lib/docker-e2e-scenarios.mjs +++ b/scripts/lib/docker-e2e-scenarios.mjs @@ -15,6 +15,7 @@ const rootManagedVpsUpgradeCommand = "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:root-managed-vps-upgrade"; const updateRestartAuthCommand = "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:update-restart-auth"; +const CODEX_HARNESS_API_KEY_ENV = "OPENCLAW_LIVE_CODEX_HARNESS_AUTH=api-key"; const LIVE_RETRY_PATTERNS = [ /529\b/i, @@ -513,13 +514,17 @@ export const tailLanes = [ "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:openai-web-search-minimal", { stateScenario: "empty", timeoutMs: 8 * 60 * 1000 }, ), - liveLane("live-codex-harness", liveDockerScriptCommand("test-live-codex-harness-docker.sh"), { - cacheKey: "codex-harness", - provider: "codex-cli", - resources: ["npm"], - timeoutMs: LIVE_ACP_TIMEOUT_MS, - weight: 3, - }), + liveLane( + "live-codex-harness", + liveDockerScriptCommand("test-live-codex-harness-docker.sh", CODEX_HARNESS_API_KEY_ENV), + { + cacheKey: "codex-harness", + provider: "openai", + resources: ["npm"], + timeoutMs: LIVE_ACP_TIMEOUT_MS, + weight: 3, + }, + ), liveLane( "live-codex-media-path", liveDockerScriptCommand( @@ -549,11 +554,11 @@ export const tailLanes = [ "live-codex-bind", liveDockerScriptCommand( "test-live-codex-harness-docker.sh", - "OPENCLAW_LIVE_CODEX_BIND=1 OPENCLAW_LIVE_CODEX_TEST_FILES=src/gateway/gateway-codex-bind.live.test.ts", + `${CODEX_HARNESS_API_KEY_ENV} OPENCLAW_LIVE_CODEX_BIND=1 OPENCLAW_LIVE_CODEX_TEST_FILES=src/gateway/gateway-codex-bind.live.test.ts`, ), { cacheKey: "codex-harness", - provider: "codex-cli", + provider: "openai", resources: ["npm"], timeoutMs: LIVE_ACP_TIMEOUT_MS, weight: 3, diff --git a/src/gateway/gateway-codex-bind.live.test.ts b/src/gateway/gateway-codex-bind.live.test.ts index 032fd3b7f9ab..055aa224cbde 100644 --- a/src/gateway/gateway-codex-bind.live.test.ts +++ b/src/gateway/gateway-codex-bind.live.test.ts @@ -31,8 +31,14 @@ import { startGatewayServer } from "./server.js"; const LIVE = isLiveTestEnabled(); const CODEX_BIND_LIVE = isTruthyEnvValue(process.env.OPENCLAW_LIVE_CODEX_BIND); const describeLive = LIVE && CODEX_BIND_LIVE ? describe : describe.skip; -const CODEX_BIND_TIMEOUT_MS = 10 * 60_000; -const CODEX_BIND_REQUEST_TIMEOUT_MS = 180_000; +const CODEX_BIND_TIMEOUT_MS = resolveLiveTimeoutMs( + process.env.OPENCLAW_LIVE_CODEX_BIND_TIMEOUT_MS, + 900_000, +); +const CODEX_BIND_REQUEST_TIMEOUT_MS = resolveLiveTimeoutMs( + process.env.OPENCLAW_LIVE_CODEX_BIND_REQUEST_TIMEOUT_MS, + 300_000, +); const DEFAULT_CODEX_BIND_MODEL = "gpt-5.5"; type CapturedOutboundReply = { @@ -42,6 +48,15 @@ type CapturedOutboundReply = { to: string; }; +function resolveLiveTimeoutMs(raw: string | undefined, fallback: number): number { + const parsed = raw ? Number(raw) : Number.NaN; + return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback; +} + +function logCodexBindStep(message: string): void { + console.info(`[live-codex-bind] ${message}`); +} + function createSlackCurrentConversationBindingRegistry(outboundReplies: CapturedOutboundReply[]) { return createTestRegistry([ { @@ -164,14 +179,24 @@ function restoreEnvVar(name: string, value: string | undefined): void { process.env[name] = value; } -async function waitForAgentRunOk(client: GatewayClient, runId: string): Promise { - const result: { status?: string } = await client.request( - "agent.wait", - { runId, timeoutMs: CODEX_BIND_REQUEST_TIMEOUT_MS }, - { timeoutMs: CODEX_BIND_REQUEST_TIMEOUT_MS + 5_000 }, - ); +async function waitForAgentRunOk( + client: GatewayClient, + runId: string, + context: string, +): Promise { + let result: { status?: string }; + try { + result = await client.request( + "agent.wait", + { runId, timeoutMs: CODEX_BIND_REQUEST_TIMEOUT_MS }, + { timeoutMs: CODEX_BIND_REQUEST_TIMEOUT_MS + 5_000 }, + ); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + throw new Error(`${context}: agent.wait error for ${runId}: ${message}`, { cause: error }); + } if (result?.status !== "ok") { - throw new Error(`agent.wait failed for ${runId}: status=${String(result?.status)}`); + throw new Error(`${context}: agent.wait failed for ${runId}: status=${String(result?.status)}`); } } @@ -179,6 +204,7 @@ async function sendChatAndWait(params: { client: GatewayClient; sessionKey: string; idempotencyKey: string; + context: string; message: string; originatingChannel: string; originatingTo: string; @@ -201,9 +227,13 @@ async function sendChatAndWait(params: { attachments: params.attachments, }); if (started?.status !== "started" || typeof started.runId !== "string") { - throw new Error(`chat.send did not start correctly: ${JSON.stringify(started)}`); + throw new Error( + `${params.context}: chat.send did not start correctly: ${JSON.stringify(started)}`, + ); } - await waitForAgentRunOk(params.client, started.runId); + logCodexBindStep(`${params.context} started (${started.runId})`); + await waitForAgentRunOk(params.client, started.runId, params.context); + logCodexBindStep(`${params.context} completed`); } async function waitForAssistantText(params: { @@ -344,8 +374,10 @@ async function writeGatewayConfig(params: { agents: { defaults: { workspace: params.workspace, - agentRuntime: { id: "codex" }, model: { primary: `${modelProvider}/${params.model}` }, + models: { + [`${modelProvider}/${params.model}`]: { agentRuntime: { id: "codex" } }, + }, skipBootstrap: true, heartbeat: { every: "0m" }, sandbox: { mode: "off" }, @@ -462,6 +494,7 @@ describeLive("gateway live (native Codex conversation binding)", () => { client, sessionKey, idempotencyKey: `idem-codex-bind-${randomUUID()}`, + context: "bind command", message: `/codex bind --cwd ${workspace} --model ${bindModel}${ bindProvider ? ` --provider ${bindProvider}` : "" }`, @@ -481,6 +514,7 @@ describeLive("gateway live (native Codex conversation binding)", () => { accountId, conversationId, }); + logCodexBindStep(`binding resolved to ${boundSessionKey}`); let commandReplyCount = bindReply.outboundTexts.length; const sendCodexCommand = async (message: string, contains: string, timeoutMs = 60_000) => { @@ -488,6 +522,7 @@ describeLive("gateway live (native Codex conversation binding)", () => { client, sessionKey, idempotencyKey: `idem-codex-command-${randomUUID()}`, + context: message, message, originatingChannel: "slack", originatingTo: conversationId, @@ -530,6 +565,7 @@ describeLive("gateway live (native Codex conversation binding)", () => { client, sessionKey, idempotencyKey: `idem-codex-bound-text-${randomUUID()}`, + context: "bound text turn", message: `Reply with exactly this token and nothing else: ${textToken}`, originatingChannel: "slack", originatingTo: conversationId, @@ -547,6 +583,7 @@ describeLive("gateway live (native Codex conversation binding)", () => { client, sessionKey, idempotencyKey: `idem-codex-bound-image-${randomUUID()}`, + context: "bound image turn", message: "What animal is drawn in the attached image? Reply with only the lowercase animal name.", originatingChannel: "slack", @@ -578,7 +615,7 @@ describeLive("gateway live (native Codex conversation binding)", () => { clearRuntimeConfigSnapshot(); await client.stopAndWait({ timeoutMs: 2_000 }).catch(() => {}); await server.close(); - await fs.rm(tempRoot, { recursive: true, force: true }); + await fs.rm(tempRoot, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); restoreEnvVar("CODEX_HOME", previous.codexHome); restoreEnvVar("OPENCLAW_CONFIG_PATH", previous.configPath); restoreEnvVar("OPENCLAW_GATEWAY_TOKEN", previous.gatewayToken); diff --git a/test/scripts/docker-e2e-plan.test.ts b/test/scripts/docker-e2e-plan.test.ts index 079bb80f558c..f3c3f802bb4f 100644 --- a/test/scripts/docker-e2e-plan.test.ts +++ b/test/scripts/docker-e2e-plan.test.ts @@ -677,11 +677,11 @@ describe("scripts/lib/docker-e2e-plan", () => { { credentials: ["anthropic", "gemini"], name: "live-gateway" }, { credentials: ["anthropic"], name: "live-cli-backend-claude" }, { credentials: ["gemini"], name: "live-cli-backend-gemini" }, - { credentials: ["codex"], name: "live-codex-harness" }, + { credentials: ["openai"], name: "live-codex-harness" }, { credentials: ["openai"], name: "live-codex-media-path" }, { credentials: ["openai"], name: "live-mcp-code-mode-gateway" }, { credentials: ["openai"], name: "live-subagent-announce" }, - { credentials: ["codex"], name: "live-codex-bind" }, + { credentials: ["openai"], name: "live-codex-bind" }, { credentials: ["anthropic"], name: "live-acp-bind-claude" }, { credentials: ["codex", "openai"], name: "live-acp-bind-codex" }, { credentials: ["factory"], name: "live-acp-bind-droid" }, @@ -694,6 +694,18 @@ describe("scripts/lib/docker-e2e-plan", () => { } }); + it("plans Codex harness Docker-all lanes for API-key Testbox auth", () => { + for (const name of ["live-codex-harness", "live-codex-bind"]) { + const plan = planFor({ selectedLaneNames: [name] }); + const lane = requireFirstLane(plan); + + expect(plan.credentials, name).toEqual(["openai"]); + expect(lane.command, name).toContain("OPENCLAW_LIVE_CODEX_HARNESS_AUTH=api-key"); + expect(lane.resources, name).toContain("live:openai"); + expect(lane.resources, name).not.toContain("live:codex"); + } + }); + it("plans the Codex npm plugin live lane as package-backed OpenAI proof", () => { const plan = planFor({ selectedLaneNames: ["live-codex-npm-plugin"] });