#!/usr/bin/env node /** * Self-check for the standalone original-UI web host. Run while ./start.sh is up. * node verify.mjs served assets + HTTP surface * node verify.mjs --deep also drive a real app-server RPC through the UI host * * The orchestrator/API layer is intentionally not part of this stack; nothing * here probes port 8899. Everything the UI needs is served by host/web-server.mjs * on UI_PORT, including the local JSON-RPC passthrough and the mock backends. */ import http from "node:http"; const uiPort = Number(process.env.UI_PORT || 8095); const deep = process.argv.includes("--deep"); function request(path, { method = "GET", headers = {}, body = null, timeout = 8000 } = {}) { return new Promise((resolve) => { const req = http.request({ host: "127.0.0.1", port: uiPort, path, method, headers }, (res) => { const chunks = []; res.on("data", (c) => chunks.push(c)); res.on("end", () => resolve({ status: res.statusCode, body: Buffer.concat(chunks).toString("utf8"), })); }); req.setTimeout(timeout, () => req.destroy(new Error("timeout"))); req.on("error", (e) => resolve({ status: 0, body: String(e.message) })); if (body) req.write(body); req.end(); }); } function json(r) { try { return JSON.parse(r.body); } catch { return null; } } const results = []; async function check(name, fn) { let out; try { out = await fn(); } catch (e) { out = { ok: false, detail: String(e.message) }; } results.push({ name, ...out }); console.log(`${out.ok ? "PASS" : "FAIL"} ${name} ${out.detail ?? ""}`); } const post = (path, payload, timeout = 8000) => request(path, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(payload), timeout, }); await check("UI GET / 注入两个宿主脚本", async () => { const r = await request("/"); const ok = r.status === 200 && r.body.includes("/__codex_local_host.js") && r.body.includes("/__codex_shim.js"); return { ok, detail: `HTTP ${r.status}, ${r.body.length}B` }; }); await check("UI GET /__codex_local_host.js", async () => { const r = await request("/__codex_local_host.js"); return { ok: r.status === 200 && r.body.includes("__codexLocalHost"), detail: `HTTP ${r.status}, ${r.body.length}B`, }; }); await check("UI GET /__codex_shim.js", async () => { const r = await request("/__codex_shim.js"); return { ok: r.status === 200 && r.body.length > 1000, detail: `HTTP ${r.status}, ${r.body.length}B` }; }); await check("UI 响应层补丁命中 connect-app-host", async () => { const r = await request("/assets/connect-app-host-90a50ca9e4b4.js"); return { ok: r.status === 200 && r.body.includes("__codexLocalHost"), detail: `HTTP ${r.status}, ${r.body.length}B`, }; }); await check("UI 原版入口 chunk 可加载", async () => { const r = await request("/assets/index-fcd3fcef6840.js"); return { ok: r.status === 200 && r.body.length > 100, detail: `HTTP ${r.status}, ${r.body.length}B` }; }); await check("UI GET /ces/v1/rgstr 遥测丢弃", async () => { const r = await request("/ces/v1/rgstr"); return { ok: r.status === 202, detail: `HTTP ${r.status}` }; }); await check("UI GET /__codex_rpc 拒绝非 POST", async () => { const r = await request("/__codex_rpc"); return { ok: r.status === 405, detail: `HTTP ${r.status}` }; }); await check("UI POST /__codex_projectless 工作区根", async () => { const r = await post("/__codex_projectless", { op: "readWorkspaceRoot" }); const j = json(r); return { ok: r.status === 200 && typeof j?.result?.workspaceRoot === "string", detail: `HTTP ${r.status}, ${j?.result?.workspaceRoot ?? r.body.slice(0, 80)}`, }; }); await check("UI POST /__codex_statsig_bootstrap 空配置", async () => { const r = await post("/__codex_statsig_bootstrap", { stable_id: "verify" }); const j = json(r); const gates = json({ body: j?.statsigPayload ?? "" })?.feature_gates; return { ok: r.status === 200 && gates && Object.keys(gates).length === 0, detail: `HTTP ${r.status}, ${r.body.length}B`, }; }); await check("UI GET /mock/wham/tasks/list 云端替身", async () => { const r = await request("/mock/wham/tasks/list"); const j = json(r); return { ok: r.status === 200 && Array.isArray(j?.items) && j.items.length === 0, detail: `HTTP ${r.status}, ${r.body.trim()}`, }; }); await check("UI GET /__codex_mocklog 诊断日志", async () => { const r = await request("/__codex_mocklog"); const j = json(r); return { ok: r.status === 200 && Array.isArray(j?.requests), detail: `HTTP ${r.status}, ${j?.requests?.length ?? 0} 条`, }; }); if (deep) { await check("UI POST /__codex_rpc -> 真实 app-server thread/list", async () => { const r = await post("/__codex_rpc", { method: "thread/list", params: { limit: 1 } }, 45000); const j = json(r); const ok = r.status === 200 && j && !j.error && j.result != null; return { ok: !!ok, detail: `HTTP ${r.status}, ${r.body.slice(0, 140)}` }; }); await check("UI POST /__codex_rpc -> config/read 含 relay 覆盖", async () => { const r = await post("/__codex_rpc", { method: "config/read", params: {} }, 45000); const j = json(r); const model = j?.result?.config?.model; return { ok: r.status === 200 && typeof model === "string" && model.length > 0, detail: `HTTP ${r.status}, model=${model ?? "n/a"}`, }; }); } const failed = results.filter((r) => !r.ok); console.log(`\n${results.length - failed.length}/${results.length} passed`); console.log("orchestrator 已移除;本栈只有 :8095 一个服务,无 :8899 API。"); if (!deep) console.log("shallow run: pass --deep to exercise the real app-server RPC"); process.exit(failed.length ? 1 : 0);