import assert from "node:assert/strict"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { createServer } from "node:http"; import { connect } from "node:net"; import os from "node:os"; import path from "node:path"; import test from "node:test"; import { createCloudWebServer, isClientDisconnectError } from "./cloud-web-runtime.mjs"; test("cloud web classifies Bun client disconnect ECONNRESET as non-fatal", () => { const error = new Error("aborted"); error.code = "ECONNRESET"; assert.equal(isClientDisconnectError(error), true); assert.equal(isClientDisconnectError(new Error("database migration failed")), false); }); test("cloud web auth requests are proxied once", async () => { let upstreamRequests = 0; const upstream = createServer((request, response) => { upstreamRequests += 1; request.resume(); response.writeHead(200, { "content-type": "application/json" }); response.end(JSON.stringify({ authenticated: true, path: request.url })); }); await listen(upstream); const cloudWeb = createCloudWebServer({ serviceId: "hwlab-cloud-web", cloudApiBaseUrl: serverUrl(upstream), cloudApiProxyTimeoutMs: 1000, healthPayload: () => ({ status: "ok" }), sendJson(response, statusCode, body) { const payload = JSON.stringify(body); response.writeHead(statusCode, { "content-type": "application/json", "content-length": Buffer.byteLength(payload) }); response.end(payload); } }); await listen(cloudWeb); try { const response = await fetch(`${serverUrl(cloudWeb)}/auth/session`, { headers: { cookie: "hwlab_session=token" } }); assert.equal(response.status, 200); assert.deepEqual(await response.json(), { authenticated: true, path: "/auth/session" }); assert.equal(upstreamRequests, 1); } finally { await close(cloudWeb); await close(upstream); } }); test("cloud web proxy reports a unified upstream unavailable error", async () => { const cloudWeb = createCloudWebServer({ serviceId: "hwlab-cloud-web", cloudApiBaseUrl: "http://127.0.0.1:1", cloudApiProxyTimeoutMs: 1000, healthPayload: () => ({ status: "ok" }), roots: [], sendJson(response, statusCode, body) { const payload = JSON.stringify(body); response.writeHead(statusCode, { "content-type": "application/json", "content-length": Buffer.byteLength(payload) }); response.end(payload); } }); await listen(cloudWeb); try { const response = await fetch(`${serverUrl(cloudWeb)}/v1/web-performance/summary?window=15m`, { headers: { accept: "application/json", "x-trace-id": "trc_proxy_upstream_unavailable" } }); 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.retryable, true); assert.equal(payload.error.userMessage, "暂时无法连接上游。"); assert.equal(payload.error.message, "暂时无法连接上游。"); assert.equal(payload.error.blocker.code, "upstream_unavailable"); assert.equal(payload.error.blocker.summary, "暂时无法连接上游。"); assert.equal(payload.reason, "暂时无法连接上游。"); assert.doesNotMatch(JSON.stringify(payload), /Code Agent|输入已保留|稍后重试/u); } finally { await close(cloudWeb); } }); test("cloud web proxies Admin Access write routes", async () => { const upstreamRequests = []; const upstream = createServer(async (request, response) => { let body = ""; for await (const chunk of request) body += chunk; upstreamRequests.push({ method: request.method, url: request.url, authorization: request.headers.authorization, cookie: request.headers.cookie, body }); response.writeHead(200, { "content-type": "application/json" }); response.end(JSON.stringify({ ok: true, path: request.url })); }); await listen(upstream); const cloudWeb = createCloudWebServer({ serviceId: "hwlab-cloud-web", cloudApiBaseUrl: serverUrl(upstream), cloudApiProxyTimeoutMs: 1000, healthPayload: () => ({ status: "ok" }), roots: [], sendJson(response, statusCode, body) { const payload = JSON.stringify(body); response.writeHead(statusCode, { "content-type": "application/json", "content-length": Buffer.byteLength(payload) }); response.end(payload); } }); await listen(cloudWeb); try { const checkBody = JSON.stringify({ user: "usr_alice", relation: "can_use", object: "tool:hwpod" }); const checkResponse = await fetch(`${serverUrl(cloudWeb)}/v1/admin/access/check`, { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer hwl_live_admin", cookie: "hwlab_session=session-a" }, body: checkBody }); assert.equal(checkResponse.status, 200); assert.deepEqual(await checkResponse.json(), { ok: true, path: "/v1/admin/access/check" }); const grantResponse = await fetch(`${serverUrl(cloudWeb)}/v1/admin/access/users/usr_alice/tools/hwpod/can-use`, { method: "PUT", headers: { "content-type": "application/json", authorization: "Bearer hwl_live_admin" }, body: "{}" }); assert.equal(grantResponse.status, 200); assert.equal(upstreamRequests.length, 2); assert.deepEqual(upstreamRequests[0], { method: "POST", url: "/v1/admin/access/check", authorization: "Bearer hwl_live_admin", cookie: "hwlab_session=session-a", body: checkBody }); assert.deepEqual(upstreamRequests[1], { method: "PUT", url: "/v1/admin/access/users/usr_alice/tools/hwpod/can-use", authorization: "Bearer hwl_live_admin", cookie: undefined, body: "{}" }); } finally { await close(cloudWeb); await close(upstream); } }); test("cloud web proxies provider profile management write routes", async () => { const upstreamRequests = []; const upstream = createServer(async (request, response) => { let body = ""; for await (const chunk of request) body += chunk; upstreamRequests.push({ method: request.method, url: request.url, authorization: request.headers.authorization, body }); response.writeHead(200, { "content-type": "application/json" }); response.end(JSON.stringify({ ok: true, path: request.url })); }); await listen(upstream); const cloudWeb = createCloudWebServer({ serviceId: "hwlab-cloud-web", cloudApiBaseUrl: serverUrl(upstream), cloudApiProxyTimeoutMs: 1000, healthPayload: () => ({ status: "ok" }), roots: [], sendJson(response, statusCode, body) { const payload = JSON.stringify(body); response.writeHead(statusCode, { "content-type": "application/json", "content-length": Buffer.byteLength(payload) }); response.end(payload); } }); await listen(cloudWeb); try { const credentialBody = JSON.stringify({ apiKey: "sk-test-redacted" }); const credentialResponse = await fetch(`${serverUrl(cloudWeb)}/v1/admin/provider-profiles/deepseek/credential`, { method: "PUT", headers: { "content-type": "application/json", authorization: "Bearer hwl_live_admin" }, body: credentialBody }); assert.equal(credentialResponse.status, 200); const validateResponse = await fetch(`${serverUrl(cloudWeb)}/v1/admin/provider-profiles/deepseek/validate`, { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer hwl_live_admin" }, body: "{}" }); assert.equal(validateResponse.status, 200); assert.equal(upstreamRequests.length, 2); assert.deepEqual(upstreamRequests[0], { method: "PUT", url: "/v1/admin/provider-profiles/deepseek/credential", authorization: "Bearer hwl_live_admin", body: credentialBody }); assert.deepEqual(upstreamRequests[1], { method: "POST", url: "/v1/admin/provider-profiles/deepseek/validate", authorization: "Bearer hwl_live_admin", body: "{}" }); } finally { await close(cloudWeb); await close(upstream); } }); test("cloud web forwards hwpod node-ops plans to cloud-api", async () => { const upstreamRequests = []; const upstream = createServer(async (request, response) => { let body = ""; for await (const chunk of request) body += chunk; upstreamRequests.push({ method: request.method, url: request.url, authorization: request.headers.authorization, body }); response.writeHead(200, { "content-type": "application/json" }); response.end(JSON.stringify({ ok: false, status: "blocked", path: request.url, blocker: { code: "hwpod_node_unavailable" } })); }); await listen(upstream); const cloudWeb = createCloudWebServer({ serviceId: "hwlab-cloud-web", cloudApiBaseUrl: serverUrl(upstream), cloudApiProxyTimeoutMs: 1000, healthPayload: () => ({ status: "ok" }), roots: [], sendJson(response, statusCode, body) { const payload = JSON.stringify(body); response.writeHead(statusCode, { "content-type": "application/json", "content-length": Buffer.byteLength(payload) }); response.end(payload); } }); await listen(cloudWeb); try { const plan = { contractVersion: "hwpod-node-ops-v1", planId: "hwpod_plan_proxy_test", hwpodId: "hwpod-test", nodeId: "node-test", ops: [{ opId: "op_01", op: "node.health", args: {} }] }; const response = await fetch(`${serverUrl(cloudWeb)}/v1/hwpod-node-ops`, { method: "POST", headers: { "content-type": "application/json", authorization: "Bearer hwl_live_operator" }, body: JSON.stringify(plan) }); assert.equal(response.status, 200); assert.deepEqual(await response.json(), { ok: false, status: "blocked", path: "/v1/hwpod-node-ops", blocker: { code: "hwpod_node_unavailable" } }); assert.equal(upstreamRequests.length, 1); assert.deepEqual(upstreamRequests[0], { method: "POST", url: "/v1/hwpod-node-ops", authorization: "Bearer hwl_live_operator", body: JSON.stringify(plan) }); } finally { await close(cloudWeb); await close(upstream); } }); test("cloud web remains healthy after a client disconnects during static response", async () => { const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-cloud-web-runtime-")); await writeFile(path.join(root, "large.txt"), "x".repeat(512 * 1024), "utf8"); const cloudWeb = createCloudWebServer({ serviceId: "hwlab-cloud-web", roots: [root], cloudApiBaseUrl: "http://127.0.0.1:1", healthPayload: () => ({ status: "ok", process: { uptimeSeconds: 1 } }), sendJson(response, statusCode, body) { const payload = JSON.stringify(body); response.writeHead(statusCode, { "content-type": "application/json", "content-length": Buffer.byteLength(payload) }); response.end(payload); } }); await listen(cloudWeb); try { await disconnectDuringResponse(serverUrl(cloudWeb), "/large.txt"); const response = await fetch(`${serverUrl(cloudWeb)}/health/live`); assert.equal(response.status, 200); assert.equal((await response.json()).status, "ok"); } finally { await close(cloudWeb); await rm(root, { recursive: true, force: true }); } }); test("cloud web serves client deep links through the Vue shell", async () => { const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-cloud-web-runtime-")); await writeFile(path.join(root, "index.html"), "
\n", "utf8"); await writeFile(path.join(root, "asset.txt"), "asset body\n", "utf8"); const cloudWeb = createCloudWebServer({ serviceId: "hwlab-cloud-web", roots: [root], cloudApiBaseUrl: "http://127.0.0.1:1", healthPayload: () => ({ status: "ok" }), sendJson(response, statusCode, body) { const payload = JSON.stringify(body); response.writeHead(statusCode, { "content-type": "application/json", "content-length": Buffer.byteLength(payload) }); response.end(payload); } }); await listen(cloudWeb); try { for (const route of ["/access", "/register", "/api-keys", "/admin/users"]) { const response = await fetch(`${serverUrl(cloudWeb)}${route}`, { headers: { accept: "text/html" } }); assert.equal(response.status, 200, route); assert.equal(response.headers.get("content-type"), "text/html; charset=utf-8", route); assert.equal(await response.text(), "
\n", route); } const assetResponse = await fetch(`${serverUrl(cloudWeb)}/asset.txt`, { headers: { accept: "text/plain" } }); assert.equal(assetResponse.status, 200); assert.equal(await assetResponse.text(), "asset body\n"); const missingAssetResponse = await fetch(`${serverUrl(cloudWeb)}/missing.css`, { headers: { accept: "text/css,*/*" } }); assert.equal(missingAssetResponse.status, 404); } finally { await close(cloudWeb); await rm(root, { recursive: true, force: true }); } }); 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 disconnectDuringResponse(baseUrl, pathname) { const url = new URL(pathname, baseUrl); return new Promise((resolve, reject) => { const socket = connect(Number(url.port), url.hostname); const timeout = setTimeout(() => { socket.destroy(); reject(new Error("client disconnect probe timed out")); }, 2000); socket.once("connect", () => { socket.write(`GET ${url.pathname} HTTP/1.1\r\nHost: ${url.host}\r\nConnection: close\r\n\r\n`); }); socket.once("data", () => { clearTimeout(timeout); socket.destroy(); setTimeout(resolve, 50); }); socket.once("error", (error) => { clearTimeout(timeout); reject(error); }); }); } function serverUrl(server) { const address = server.address(); return `http://127.0.0.1:${address.port}`; }