import assert from "node:assert/strict"; import { createServer } from "node:http"; import test from "node:test"; import { upstreamRequestHeaders } from "./cloud-web-proxy.mjs"; import { cloudWebProxyRoutePolicy } from "./cloud-web-routes.mjs"; import { proxyHttpRequest } from "./http.mjs"; test("cloud web route policy proxies public Code Agent chat without gating other writes", () => { assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/v1/agent/chat"), { proxy: true, authRequired: false, publicRoute: true, routeKey: "POST /v1/agent/chat" }); assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/v1/agent/chat/cancel"), { proxy: true, authRequired: false, publicRoute: true, routeKey: "POST /v1/agent/chat/cancel" }); assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/v1/agent/chat/steer"), { proxy: true, authRequired: false, publicRoute: true, routeKey: "POST /v1/agent/chat/steer" }); assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/v1/m3/io"), { proxy: true, authRequired: true, publicRoute: false, routeKey: "POST /v1/m3/io" }); assert.deepEqual(cloudWebProxyRoutePolicy("GET", "/v1/agent/traces/trc_live"), { proxy: true, authRequired: false, publicRoute: true, routeKey: "GET /v1/agent/traces/trc_live" }); assert.deepEqual(cloudWebProxyRoutePolicy("GET", "/v1/agent/chat/trace/trc_live"), { proxy: true, authRequired: true, publicRoute: false, routeKey: "GET /v1/agent/chat/trace/trc_live" }); assert.deepEqual(cloudWebProxyRoutePolicy("GET", "/v1/agent/chat/result/trc_live"), { proxy: true, authRequired: false, publicRoute: true, routeKey: "GET /v1/agent/chat/result/trc_live" }); assert.deepEqual(cloudWebProxyRoutePolicy("GET", "/v1/agent/turns/trc_live"), { proxy: true, authRequired: false, publicRoute: true, routeKey: "GET /v1/agent/turns/trc_live" }); assert.deepEqual(cloudWebProxyRoutePolicy("GET", "/v1"), { proxy: true, authRequired: true, publicRoute: false, routeKey: "GET /v1" }); assert.deepEqual(cloudWebProxyRoutePolicy("GET", "/v1/m3/status"), { proxy: true, authRequired: true, publicRoute: false, routeKey: "GET /v1/m3/status" }); assert.deepEqual(cloudWebProxyRoutePolicy("GET", "/app.js"), { proxy: false, authRequired: false, publicRoute: false, routeKey: "GET /app.js" }); }); test("cloud web route policy delegates auth authority to cloud-api", () => { assert.deepEqual(cloudWebProxyRoutePolicy("GET", "/auth/session"), { proxy: true, authRequired: false, publicRoute: true, routeKey: "GET /auth/session" }); assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/auth/login"), { proxy: true, authRequired: false, publicRoute: true, routeKey: "POST /auth/login" }); assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/auth/logout"), { proxy: true, authRequired: false, publicRoute: true, routeKey: "POST /auth/logout" }); }); test("cloud web proxy forwards user credentials without forwarding session-token credentials", () => { const headers = upstreamRequestHeaders({ headers: { accept: "application/json", authorization: "Bearer hwl_live_must-forward", cookie: "hwlab_session=session-a", "x-hwlab-session-token": "session-a-secret", "x-trace-id": "trc_hwpod_session_proxy" } }); assert.equal(headers["x-hwlab-session-token"], undefined); assert.equal(headers.cookie, "hwlab_session=session-a"); assert.equal(headers["x-trace-id"], "trc_hwpod_session_proxy"); assert.equal(headers.authorization, "Bearer hwl_live_must-forward"); }); test("dev entrypoint proxy allows slow first response beyond legacy 4500ms", async () => { const upstream = createServer((request, response) => { request.resume(); setTimeout(() => { response.writeHead(200, { "content-type": "application/json" }); response.end(JSON.stringify({ status: "completed", traceId: request.headers["x-trace-id"] ?? null })); }, 4700); }); await listen(upstream); const proxy = createServer((request, response) => { proxyHttpRequest({ request, response, upstream: serverUrl(upstream), timeoutMs: 6000 }); }); await listen(proxy); try { const response = await fetch(`${serverUrl(proxy)}/v1/agent/chat`, { method: "POST", headers: { "content-type": "application/json", "x-trace-id": "trc_dev-entrypoint-proxy-slow-success" }, body: JSON.stringify({ message: "列出你能使用的所有skill" }) }); assert.equal(response.status, 200); const payload = await response.json(); assert.equal(payload.status, "completed"); assert.equal(payload.traceId, "trc_dev-entrypoint-proxy-slow-success"); } finally { await close(proxy); await close(upstream); } }); test("dev entrypoint proxy returns 502 when hard timeout expires", async () => { const upstream = createServer((request, response) => { request.resume(); setTimeout(() => { response.writeHead(200, { "content-type": "application/json" }); response.end(JSON.stringify({ status: "completed" })); }, 250); }); await listen(upstream); const proxy = createServer((request, response) => { proxyHttpRequest({ request, response, upstream: serverUrl(upstream), timeoutMs: 50 }); }); await listen(proxy); try { const response = await fetch(`${serverUrl(proxy)}/v1/agent/chat`, { method: "POST", headers: { "content-type": "application/json", "x-trace-id": "trc_dev-entrypoint-proxy-timeout" }, body: JSON.stringify({ message: "slow" }) }); assert.equal(response.status, 502); const payload = await response.json(); assert.equal(payload.status, "failed"); assert.equal(payload.error.code, "proxy_timeout"); assert.equal(payload.error.layer, "proxy"); assert.equal(payload.error.category, "timeout"); assert.equal(payload.error.retryable, true); assert.equal(payload.error.timeoutMs, 50); assert.equal(payload.error.traceId, "trc_dev-entrypoint-proxy-timeout"); assert.match(payload.error.userMessage, /超过 50ms/u); assert.match(payload.error.message, /输入已保留/u); assert.equal(payload.error.blocker.code, "proxy_timeout"); assert.equal(payload.error.blocker.retryable, true); assert.equal(payload.traceId, "trc_dev-entrypoint-proxy-timeout"); assert.match(payload.message, /Code Agent 代理等待上游超过 50ms/u); } finally { await close(proxy); await close(upstream); } }); function listen(server) { return new Promise((resolve, reject) => { server.once("error", reject); server.listen(0, "127.0.0.1", resolve); }); } function close(server) { return new Promise((resolve, reject) => { server.close((error) => (error ? reject(error) : resolve())); }); } function serverUrl(server) { const address = server.address(); return `http://127.0.0.1:${address.port}`; }