#!/usr/bin/env node import http from "node:http"; import net from "node:net"; import { once } from "node:events"; import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { spawn } from "node:child_process"; const scriptsRoot = import.meta.dirname; const installScript = path.join(scriptsRoot, "install-for-current-provider.ps1"); const restoreScript = path.join(scriptsRoot, "restore-codex-config.ps1"); function assert(condition, message) { if (!condition) { throw new Error(message); } } async function getFreePort() { const server = net.createServer(); server.listen(0, "127.0.0.1"); await once(server, "listening"); const address = server.address(); const port = address && typeof address === "object" ? address.port : null; server.close(); await once(server, "close"); if (!port) { throw new Error("Failed to allocate a free port"); } return port; } function startFakeUpstream(port) { const server = http.createServer((req, res) => { if (req.method === "GET" && req.url === "/v1/models") { res.writeHead(200, { "content-type": "application/json; charset=utf-8", "x-upstream-test": "install-flow-ok", }); res.end(JSON.stringify({ object: "list", data: [{ id: "install-test-model" }] })); return; } if (req.method === "POST" && (req.url === "/responses" || req.url === "/v1/responses")) { let body = ""; req.setEncoding("utf8"); req.on("data", (chunk) => { body += chunk; }); req.on("end", () => { const parsed = JSON.parse(body || "{}"); const reasoning = parsed.test_reasoning_tokens ?? 128; res.writeHead(200, { "content-type": "application/json; charset=utf-8" }); res.end( JSON.stringify({ id: "install-test-response", usage: { output_tokens_details: { reasoning_tokens: reasoning, }, }, }), ); }); return; } res.writeHead(404, { "content-type": "application/json; charset=utf-8" }); res.end(JSON.stringify({ error: "not found" })); }); return new Promise((resolve, reject) => { server.once("error", reject); server.listen(port, "127.0.0.1", () => resolve(server)); }); } async function runPowerShellScript(scriptPath, args) { const child = spawn( "powershell", ["-ExecutionPolicy", "Bypass", "-File", scriptPath, ...args], { stdio: ["ignore", "pipe", "pipe"] }, ); let stdout = ""; let stderr = ""; child.stdout.on("data", (chunk) => { stdout += chunk.toString(); }); child.stderr.on("data", (chunk) => { stderr += chunk.toString(); }); const [exitCode] = await once(child, "exit"); if (exitCode !== 0) { throw new Error(`PowerShell script failed: ${scriptPath}\nstdout:\n${stdout}\nstderr:\n${stderr}`); } return { stdout, stderr }; } async function run() { const tempRoot = await mkdtemp(path.join(os.tmpdir(), "codex-retry-gateway-install-")); const codexDir = path.join(tempRoot, ".codex"); const stateRoot = path.join(tempRoot, ".codex-retry-gateway"); const codexConfigPath = path.join(codexDir, "config.toml"); const upstreamPort = await getFreePort(); const gatewayPort = await getFreePort(); await mkdir(codexDir, { recursive: true }); await writeFile( codexConfigPath, [ 'model_provider = "custom"', "", "[model_providers.custom]", 'name = "Install Test"', `base_url = "http://127.0.0.1:${upstreamPort}"`, 'wire_api = "responses"', "", ].join("\n"), "utf8", ); const upstream = await startFakeUpstream(upstreamPort); try { await runPowerShellScript(installScript, [ "-CodexConfigPath", codexConfigPath, "-StateRoot", stateRoot, "-ListenPort", String(gatewayPort), ]); const updatedConfig = await readFile(codexConfigPath, "utf8"); assert( updatedConfig.includes(`base_url = "http://127.0.0.1:${gatewayPort}"`), "Install script did not redirect base_url to local gateway", ); const gatewayConfig = JSON.parse( await readFile(path.join(stateRoot, "config", "config.json"), "utf8"), ); assert( gatewayConfig.upstream_base_url === `http://127.0.0.1:${upstreamPort}`, "Gateway config did not preserve original upstream_base_url", ); assert( gatewayConfig.reasoning_match_mode === "formula_518n_minus_2", "Gateway config did not default reasoning_match_mode to formula_518n_minus_2", ); assert(Array.isArray(gatewayConfig.endpoints), "Gateway config endpoints must be an array"); assert( gatewayConfig.endpoints.includes("/responses") && gatewayConfig.endpoints.includes("/chat/completions") && gatewayConfig.endpoints.includes("/v1/responses") && gatewayConfig.endpoints.includes("/v1/chat/completions"), "Gateway config endpoints did not include both root and /v1 variants", ); const proxiedModels = await fetch(`http://127.0.0.1:${gatewayPort}/v1/models`); assert(proxiedModels.status === 200, `/v1/models through installed gateway failed: ${proxiedModels.status}`); assert( proxiedModels.headers.get("x-upstream-test") === "install-flow-ok", "Installed gateway did not preserve upstream header", ); const uiResponse = await fetch(`http://127.0.0.1:${gatewayPort}/__codex_retry_gateway/ui`); const uiHtml = await uiResponse.text(); assert(uiResponse.status === 200, `Management UI failed to load: ${uiResponse.status}`); assert(uiHtml.includes("Codex Retry Gateway"), "Management UI HTML did not include expected title"); assert(uiHtml.includes("516 命中次数"), "Management UI HTML did not include 516 match stats"); assert(uiHtml.includes("516 占比"), "Management UI HTML did not include 516 ratio stats"); assert(uiHtml.includes("实时日志"), "Management UI HTML did not include live log panel"); const statusResponse = await fetch(`http://127.0.0.1:${gatewayPort}/__codex_retry_gateway/api/status`); const statusPayload = await statusResponse.json(); assert(statusResponse.status === 200, `Status API failed: ${statusResponse.status}`); assert(statusPayload.config?.upstream_base_url === `http://127.0.0.1:${upstreamPort}`, "Status API did not expose config"); assert(statusPayload.state?.original_base_url === `http://127.0.0.1:${upstreamPort}`, "Status API did not expose install state"); assert(statusPayload.metrics?.inspected_response_count === 0, "Status API did not expose initial inspected count"); assert(statusPayload.metrics?.reasoning_516_count === 0, "Status API did not expose initial 516 count"); const normalResponse = await fetch(`http://127.0.0.1:${gatewayPort}/responses`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ test_reasoning_tokens: 128 }), }); assert(normalResponse.status === 200, `Expected a passthrough response before 516 test: ${normalResponse.status}`); const blocked516Response = await fetch(`http://127.0.0.1:${gatewayPort}/responses`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ test_reasoning_tokens: 516 }), }); assert(blocked516Response.status === 502, `Default 516 block did not trigger: ${blocked516Response.status}`); const blocked2070Response = await fetch(`http://127.0.0.1:${gatewayPort}/responses`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ test_reasoning_tokens: 2070 }), }); assert(blocked2070Response.status === 502, `Default 2070 formula block did not trigger: ${blocked2070Response.status}`); const metricsStatusResponse = await fetch(`http://127.0.0.1:${gatewayPort}/__codex_retry_gateway/api/status`); const metricsStatusPayload = await metricsStatusResponse.json(); assert(metricsStatusResponse.status === 200, `Status API failed after traffic: ${metricsStatusResponse.status}`); assert(metricsStatusPayload.metrics?.inspected_response_count === 2, "Status API inspected count was not updated"); assert(metricsStatusPayload.metrics?.matched_response_count === 1, "Status API matched count was not updated"); assert(metricsStatusPayload.metrics?.reasoning_516_count === 1, "Status API 516 count was not updated"); assert(metricsStatusPayload.metrics?.reasoning_516_ratio === 0.5, "Status API 516 ratio was not updated"); const logsResponse = await fetch(`http://127.0.0.1:${gatewayPort}/__codex_retry_gateway/api/logs`); const logsPayload = await logsResponse.json(); assert(logsResponse.status === 200, `Logs API failed: ${logsResponse.status}`); assert(Array.isArray(logsPayload.entries), "Logs API did not return entries array"); assert( logsPayload.entries.some((entry) => `${entry.message || ""}`.includes("[start]")), "Logs API did not include gateway start log", ); assert( logsPayload.entries.some((entry) => `${entry.message || ""}`.includes("reasoning_tokens=516")), "Logs API did not include 516 match log", ); const saveConfigResponse = await fetch(`http://127.0.0.1:${gatewayPort}/__codex_retry_gateway/api/config`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ reasoning_match_mode: "manual", reasoning_equals: [1024], retryable_status_codes: [429, 503, 529], retryable_error_messages: ["Selected model is at capacity. Please try a different model."], endpoints: ["/responses", "/v1/responses"], non_stream_status_code: 503, log_match: false, }), }); const saveConfigPayload = await saveConfigResponse.json(); assert(saveConfigResponse.status === 200, `Save config API failed: ${saveConfigResponse.status}`); assert(saveConfigPayload.config?.non_stream_status_code === 503, "Save config API did not return updated config"); assert(saveConfigPayload.config?.reasoning_match_mode === "manual", "Save config API did not return updated reasoning_match_mode"); const updatedGatewayConfig = JSON.parse( await readFile(path.join(stateRoot, "config", "config.json"), "utf8"), ); assert( updatedGatewayConfig.reasoning_match_mode === "manual", "Saved config file did not persist reasoning_match_mode", ); assert( JSON.stringify(updatedGatewayConfig.reasoning_equals) === JSON.stringify([1024]), "Saved config file did not persist reasoning_equals", ); assert( JSON.stringify(updatedGatewayConfig.retryable_status_codes) === JSON.stringify([429, 503, 529]), "Saved config file did not persist retryable_status_codes", ); const incrementalLogsResponse = await fetch( `http://127.0.0.1:${gatewayPort}/__codex_retry_gateway/api/logs?since_seq=${logsPayload.latest_seq}`, ); const incrementalLogsPayload = await incrementalLogsResponse.json(); assert(incrementalLogsResponse.status === 200, `Incremental logs API failed: ${incrementalLogsResponse.status}`); assert( incrementalLogsPayload.entries.some((entry) => `${entry.message || ""}`.includes("[config] updated")), "Incremental logs API did not include config update log", ); const blockedAfterSave = await fetch(`http://127.0.0.1:${gatewayPort}/responses`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ test_reasoning_tokens: 1024 }), }); assert(blockedAfterSave.status === 503, `Hot reloaded config did not take effect: ${blockedAfterSave.status}`); const manualModePassthrough = await fetch(`http://127.0.0.1:${gatewayPort}/responses`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ test_reasoning_tokens: 2070 }), }); assert(manualModePassthrough.status === 200, `manual 模式下 2070 不应继续被拦截: ${manualModePassthrough.status}`); const restoreViaUiResponse = await fetch(`http://127.0.0.1:${gatewayPort}/__codex_retry_gateway/api/restore`, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({}), }); const restoreViaUiPayload = await restoreViaUiResponse.json(); assert(restoreViaUiResponse.status === 202, `Restore API failed: ${restoreViaUiResponse.status}`); assert(restoreViaUiPayload.ok === true, "Restore API did not acknowledge the restore request"); const restoreStartedAt = Date.now(); while (Date.now() - restoreStartedAt < 10000) { const restoredCandidate = await readFile(codexConfigPath, "utf8"); if (restoredCandidate.includes(`base_url = "http://127.0.0.1:${upstreamPort}"`)) { break; } await new Promise((resolve) => setTimeout(resolve, 200)); } const restoredConfig = await readFile(codexConfigPath, "utf8"); assert( restoredConfig.includes(`base_url = "http://127.0.0.1:${upstreamPort}"`), "Restore script did not recover original base_url", ); process.stdout.write("PASS install-restore flow\n"); } finally { upstream.close(); await once(upstream, "close"); await rm(tempRoot, { recursive: true, force: true }); } } run().catch((error) => { process.stderr.write(`${error?.stack || error}\n`); process.exit(1); });