Files

250 lines
8.1 KiB
JavaScript

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("POST", "/v1/project-management/mdtodo/sources"), {
proxy: true,
authRequired: true,
publicRoute: false,
routeKey: "POST /v1/project-management/mdtodo/sources"
});
assert.deepEqual(cloudWebProxyRoutePolicy("POST", "/v1/project-management/mdtodo/sources/d601-f103-v2-mdtodo/reindex"), {
proxy: true,
authRequired: true,
publicRoute: false,
routeKey: "POST /v1/project-management/mdtodo/sources/d601-f103-v2-mdtodo/reindex"
});
assert.deepEqual(cloudWebProxyRoutePolicy("PATCH", "/v1/project-management/mdtodo/tasks/mdtodo%3Asource%3Afile%3AR1.1"), {
proxy: true,
authRequired: true,
publicRoute: false,
routeKey: "PATCH /v1/project-management/mdtodo/tasks/mdtodo%3Asource%3Afile%3AR1.1"
});
assert.deepEqual(cloudWebProxyRoutePolicy("DELETE", "/v1/project-management/mdtodo/tasks/mdtodo%3Asource%3Afile%3AR1.1"), {
proxy: true,
authRequired: true,
publicRoute: false,
routeKey: "DELETE /v1/project-management/mdtodo/tasks/mdtodo%3Asource%3Afile%3AR1.1"
});
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, "upstream_unavailable");
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.equal(payload.error.userMessage, "暂时无法连接上游。");
assert.equal(payload.error.message, "暂时无法连接上游。");
assert.equal(payload.error.blocker.code, "upstream_unavailable");
assert.equal(payload.error.blocker.retryable, true);
assert.equal(payload.traceId, "trc_dev-entrypoint-proxy-timeout");
assert.equal(payload.message, "暂时无法连接上游。");
assert.doesNotMatch(JSON.stringify(payload), /Code Agent|输入已保留|稍后重试/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}`;
}