859 lines
31 KiB
JavaScript
859 lines
31 KiB
JavaScript
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 proxies Workbench launches with trace context", 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,
|
|
cookie: request.headers.cookie,
|
|
traceparent: request.headers.traceparent,
|
|
otelTraceId: request.headers["x-hwlab-otel-trace-id"],
|
|
body
|
|
});
|
|
response.writeHead(404, {
|
|
"content-type": "application/json",
|
|
"x-hwlab-otel-trace-id": "11111111111111111111111111111111"
|
|
});
|
|
response.end(JSON.stringify({ error: "not_found" }));
|
|
});
|
|
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 body = JSON.stringify({ projectId: "project_d601", taskRef: "task_2136" });
|
|
const response = await fetch(`${serverUrl(cloudWeb)}/v1/workbench/launches`, {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
cookie: "hwlab_session=session-launch",
|
|
traceparent: "00-11111111111111111111111111111111-2222222222222222-01",
|
|
"x-hwlab-otel-trace-id": "11111111111111111111111111111111"
|
|
},
|
|
body
|
|
});
|
|
assert.equal(response.status, 404);
|
|
assert.equal(response.headers.get("x-hwlab-otel-trace-id"), "11111111111111111111111111111111");
|
|
assert.deepEqual(await response.json(), { error: "not_found" });
|
|
assert.deepEqual(upstreamRequests, [{
|
|
method: "POST",
|
|
url: "/v1/workbench/launches",
|
|
cookie: "hwlab_session=session-launch",
|
|
traceparent: "00-11111111111111111111111111111111-2222222222222222-01",
|
|
otelTraceId: "11111111111111111111111111111111",
|
|
body
|
|
}]);
|
|
} 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 restoreEnv = withEnv({
|
|
HWLAB_CLOUD_WEB_DISPLAY_TIME_ZONE: "Asia/Shanghai",
|
|
HWLAB_CLOUD_WEB_DISPLAY_TIME_LOCALE: "zh-CN",
|
|
HWLAB_CLOUD_WEB_DISPLAY_TIME_LABEL: "北京时间"
|
|
});
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-cloud-web-runtime-"));
|
|
await writeFile(path.join(root, "index.html"), "<div id=\"root\"></div>\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);
|
|
const html = await response.text();
|
|
assert.match(html, /<div id="root"><\/div>/u, route);
|
|
assert.match(html, /HWLAB_CLOUD_WEB_CONFIG/u, 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 {
|
|
restoreEnv();
|
|
await close(cloudWeb);
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("cloud web injects trace explorer runtime config from env", async () => {
|
|
const restoreEnv = withEnv({
|
|
HWLAB_CLOUD_WEB_DISPLAY_TIME_ZONE: "Asia/Shanghai",
|
|
HWLAB_CLOUD_WEB_DISPLAY_TIME_LOCALE: "zh-CN",
|
|
HWLAB_CLOUD_WEB_DISPLAY_TIME_LABEL: "北京时间",
|
|
HWLAB_WORKBENCH_TRACE_EXPLORER_URL_TEMPLATE: "/v1/workbench/traces/{trace_id}/events"
|
|
});
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-cloud-web-runtime-"));
|
|
await writeFile(path.join(root, "index.html"), "<html><head></head><body><div id=\"root\"></div></body></html>\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 {
|
|
const response = await fetch(`${serverUrl(cloudWeb)}/workbench`, { headers: { accept: "text/html" } });
|
|
assert.equal(response.status, 200);
|
|
const html = await response.text();
|
|
assert.match(html, /traceExplorerUrlTemplate":"\/v1\/workbench\/traces\/\{trace_id\}\/events/u);
|
|
assert.doesNotMatch(html, /api key|secret|password/iu);
|
|
} finally {
|
|
restoreEnv();
|
|
await close(cloudWeb);
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("cloud web OpenCode proxy requires an authenticated HWLAB session", async () => {
|
|
const authRequests = [];
|
|
let opencodeRequests = 0;
|
|
const cloudApi = createServer((request, response) => {
|
|
authRequests.push({
|
|
traceparent: request.headers.traceparent,
|
|
otelTraceId: request.headers["x-hwlab-otel-trace-id"],
|
|
requestId: request.headers["x-request-id"],
|
|
sourceServiceId: request.headers["x-source-service-id"]
|
|
});
|
|
request.resume();
|
|
response.writeHead(401, { "content-type": "application/json" });
|
|
response.end(JSON.stringify({ authenticated: false }));
|
|
});
|
|
const opencode = createServer((request, response) => {
|
|
opencodeRequests += 1;
|
|
request.resume();
|
|
response.writeHead(200, { "content-type": "text/plain" });
|
|
response.end("opencode should not be reached");
|
|
});
|
|
await listen(cloudApi);
|
|
await listen(opencode);
|
|
|
|
const cloudWeb = createCloudWebServer({
|
|
serviceId: "hwlab-cloud-web",
|
|
roots: [],
|
|
cloudApiBaseUrl: serverUrl(cloudApi),
|
|
cloudApiProxyTimeoutMs: 1000,
|
|
opencodeUpstreamUrl: serverUrl(opencode),
|
|
opencodeProxyHost: "127.0.0.1",
|
|
opencodeProxyTimeoutMs: 1000,
|
|
opencodeUsername: "oc_user",
|
|
opencodePassword: "oc_password",
|
|
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)}/`, { headers: { accept: "text/html" } });
|
|
assert.equal(response.status, 401);
|
|
const body = await response.json();
|
|
assert.equal(body.error, "opencode_auth_required");
|
|
assert.match(response.headers.get("traceparent") ?? "", /^00-[0-9a-f]{32}-[0-9a-f]{16}-01$/u);
|
|
assert.match(response.headers.get("x-hwlab-otel-trace-id") ?? "", /^[0-9a-f]{32}$/u);
|
|
assert.match(response.headers.get("x-request-id") ?? "", /^req_[0-9a-f]{32}$/u);
|
|
assert.equal(body.diagnostic.traceId, response.headers.get("x-hwlab-otel-trace-id"));
|
|
assert.equal(body.diagnostic.requestId, response.headers.get("x-request-id"));
|
|
assert.deepEqual(authRequests, [{
|
|
traceparent: response.headers.get("traceparent"),
|
|
otelTraceId: response.headers.get("x-hwlab-otel-trace-id"),
|
|
requestId: response.headers.get("x-request-id"),
|
|
sourceServiceId: "hwlab-cloud-web"
|
|
}]);
|
|
assert.equal(opencodeRequests, 0);
|
|
} finally {
|
|
await close(cloudWeb);
|
|
await close(opencode);
|
|
await close(cloudApi);
|
|
}
|
|
});
|
|
|
|
test("cloud web OpenCode frame-url endpoint mints traced tickets", async () => {
|
|
const restoreEnv = withEnv({
|
|
HWLAB_CLOUD_WEB_DISPLAY_TIME_ZONE: "Asia/Shanghai",
|
|
HWLAB_CLOUD_WEB_DISPLAY_TIME_LOCALE: "zh-CN",
|
|
HWLAB_CLOUD_WEB_DISPLAY_TIME_LABEL: "北京时间",
|
|
HWLAB_CLOUD_WEB_OPENCODE_URL: "https://opencode.example.test"
|
|
});
|
|
const cloudApiRequests = [];
|
|
const opencodeRequests = [];
|
|
const cloudApi = createServer((request, response) => {
|
|
cloudApiRequests.push({
|
|
cookie: request.headers.cookie,
|
|
traceparent: request.headers.traceparent,
|
|
otelTraceId: request.headers["x-hwlab-otel-trace-id"],
|
|
requestId: request.headers["x-request-id"],
|
|
sourceServiceId: request.headers["x-source-service-id"]
|
|
});
|
|
request.resume();
|
|
response.writeHead(200, { "content-type": "application/json" });
|
|
response.end(JSON.stringify({ authenticated: request.headers.cookie === "hwlab_session=session-a" }));
|
|
});
|
|
const opencode = createServer((request, response) => {
|
|
opencodeRequests.push({ url: request.url, cookie: request.headers.cookie });
|
|
request.resume();
|
|
response.writeHead(200, { "content-type": "text/plain" });
|
|
response.end("opencode ok\n");
|
|
});
|
|
await listen(cloudApi);
|
|
await listen(opencode);
|
|
|
|
const cloudWeb = createCloudWebServer({
|
|
serviceId: "hwlab-cloud-web",
|
|
roots: [],
|
|
cloudApiBaseUrl: serverUrl(cloudApi),
|
|
cloudApiProxyTimeoutMs: 1000,
|
|
opencodeUpstreamUrl: serverUrl(opencode),
|
|
opencodeProxyHost: "",
|
|
opencodeProxyTimeoutMs: 1000,
|
|
opencodeUsername: "oc_user",
|
|
opencodePassword: "oc_password",
|
|
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)}/opencode/frame-url`, {
|
|
headers: { accept: "application/json", cookie: "hwlab_session=session-a", "x-request-id": "req_opencode_frame_url" }
|
|
});
|
|
assert.equal(response.status, 200);
|
|
assert.equal(response.headers.get("x-request-id"), "req_opencode_frame_url");
|
|
assert.match(response.headers.get("traceparent") ?? "", /^00-[0-9a-f]{32}-[0-9a-f]{16}-01$/u);
|
|
const body = await response.json();
|
|
const frameUrl = new URL(body.url);
|
|
const ticket = frameUrl.searchParams.get("hwlab_opencode_ticket");
|
|
assert.ok(ticket, "dynamic endpoint should append an OpenCode ticket");
|
|
assert.equal(body.diagnostic.traceId, response.headers.get("x-hwlab-otel-trace-id"));
|
|
assert.deepEqual(cloudApiRequests, [{
|
|
cookie: "hwlab_session=session-a",
|
|
traceparent: response.headers.get("traceparent"),
|
|
otelTraceId: response.headers.get("x-hwlab-otel-trace-id"),
|
|
requestId: "req_opencode_frame_url",
|
|
sourceServiceId: "hwlab-cloud-web"
|
|
}]);
|
|
|
|
const proxied = await fetch(`${serverUrl(cloudWeb)}/_opencode/?hwlab_opencode_ticket=${encodeURIComponent(ticket)}`, {
|
|
headers: { accept: "text/plain" }
|
|
});
|
|
assert.equal(proxied.status, 200);
|
|
assert.equal(await proxied.text(), "opencode ok\n");
|
|
assert.deepEqual(opencodeRequests, [{ url: "/", cookie: undefined }]);
|
|
} finally {
|
|
restoreEnv();
|
|
await close(cloudWeb);
|
|
await close(opencode);
|
|
await close(cloudApi);
|
|
}
|
|
});
|
|
|
|
test("cloud web OpenCode proxy injects upstream Basic Auth without forwarding HWLAB cookie", async () => {
|
|
const opencodeRequests = [];
|
|
const cloudApi = createServer((request, response) => {
|
|
request.resume();
|
|
response.writeHead(200, { "content-type": "application/json" });
|
|
response.end(JSON.stringify({ authenticated: request.headers.cookie === "hwlab_session=session-a" }));
|
|
});
|
|
const opencode = createServer((request, response) => {
|
|
opencodeRequests.push({
|
|
url: request.url,
|
|
authorization: request.headers.authorization,
|
|
cookie: request.headers.cookie
|
|
});
|
|
request.resume();
|
|
response.writeHead(200, { "content-type": "text/javascript" });
|
|
response.end("export const ok = true;\n");
|
|
});
|
|
await listen(cloudApi);
|
|
await listen(opencode);
|
|
|
|
const cloudWeb = createCloudWebServer({
|
|
serviceId: "hwlab-cloud-web",
|
|
roots: [],
|
|
cloudApiBaseUrl: serverUrl(cloudApi),
|
|
cloudApiProxyTimeoutMs: 1000,
|
|
opencodeUpstreamUrl: serverUrl(opencode),
|
|
opencodeProxyHost: "127.0.0.1",
|
|
opencodeProxyTimeoutMs: 1000,
|
|
opencodeUsername: "oc_user",
|
|
opencodePassword: "oc_password",
|
|
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)}/assets/index.js`, {
|
|
headers: { accept: "text/javascript", cookie: "hwlab_session=session-a", "x-trace-id": "trc_opencode_proxy" }
|
|
});
|
|
assert.equal(response.status, 200);
|
|
assert.equal(await response.text(), "export const ok = true;\n");
|
|
assert.deepEqual(opencodeRequests, [{
|
|
url: "/assets/index.js",
|
|
authorization: `Basic ${Buffer.from("oc_user:oc_password", "utf8").toString("base64")}`,
|
|
cookie: undefined
|
|
}]);
|
|
} finally {
|
|
await close(cloudWeb);
|
|
await close(opencode);
|
|
await close(cloudApi);
|
|
}
|
|
});
|
|
|
|
test("cloud web OpenCode proxy accepts short-lived tickets minted by the shell", async () => {
|
|
const restoreEnv = withEnv({
|
|
HWLAB_CLOUD_WEB_DISPLAY_TIME_ZONE: "Asia/Shanghai",
|
|
HWLAB_CLOUD_WEB_DISPLAY_TIME_LOCALE: "zh-CN",
|
|
HWLAB_CLOUD_WEB_DISPLAY_TIME_LABEL: "北京时间",
|
|
HWLAB_CLOUD_WEB_OPENCODE_URL: "https://opencode.example.test"
|
|
});
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-cloud-web-runtime-"));
|
|
await writeFile(path.join(root, "index.html"), "<html><head></head><body><div id=\"root\"></div></body></html>\n", "utf8");
|
|
const cloudApiCookies = [];
|
|
const opencodeRequests = [];
|
|
const cloudApi = createServer((request, response) => {
|
|
cloudApiCookies.push(request.headers.cookie);
|
|
request.resume();
|
|
response.writeHead(200, { "content-type": "application/json" });
|
|
response.end(JSON.stringify({ authenticated: request.headers.cookie === "hwlab_session=session-a" }));
|
|
});
|
|
const opencode = createServer((request, response) => {
|
|
opencodeRequests.push({
|
|
url: request.url,
|
|
authorization: request.headers.authorization,
|
|
cookie: request.headers.cookie
|
|
});
|
|
request.resume();
|
|
response.writeHead(200, { "content-type": "text/plain" });
|
|
response.end("opencode ok\n");
|
|
});
|
|
await listen(cloudApi);
|
|
await listen(opencode);
|
|
|
|
const cloudWeb = createCloudWebServer({
|
|
serviceId: "hwlab-cloud-web",
|
|
roots: [root],
|
|
cloudApiBaseUrl: serverUrl(cloudApi),
|
|
cloudApiProxyTimeoutMs: 1000,
|
|
opencodeUpstreamUrl: serverUrl(opencode),
|
|
opencodeProxyHost: "",
|
|
opencodeProxyTimeoutMs: 1000,
|
|
opencodeUsername: "oc_user",
|
|
opencodePassword: "oc_password",
|
|
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 shell = await fetch(`${serverUrl(cloudWeb)}/opencode`, {
|
|
headers: { accept: "text/html", cookie: "hwlab_session=session-a" }
|
|
});
|
|
assert.equal(shell.status, 200);
|
|
const html = await shell.text();
|
|
const config = JSON.parse(html.match(/window\.HWLAB_CLOUD_WEB_CONFIG=([^<]+);<\/script>/u)?.[1] ?? "null");
|
|
const frameUrl = new URL(config.opencode.url);
|
|
const ticket = frameUrl.searchParams.get("hwlab_opencode_ticket");
|
|
assert.ok(ticket, "ticket should be appended to OpenCode iframe URL");
|
|
|
|
const first = await fetch(`${serverUrl(cloudWeb)}/_opencode/?hwlab_opencode_ticket=${encodeURIComponent(ticket)}`, {
|
|
headers: { accept: "text/plain" }
|
|
});
|
|
assert.equal(first.status, 200);
|
|
assert.equal(await first.text(), "opencode ok\n");
|
|
const ticketCookie = first.headers.get("set-cookie") ?? "";
|
|
assert.match(ticketCookie, /hwlab_opencode_ticket=/u);
|
|
assert.match(ticketCookie, /HttpOnly/u);
|
|
assert.match(ticketCookie, /SameSite=None/u);
|
|
|
|
const second = await fetch(`${serverUrl(cloudWeb)}/_opencode/assets/index.js`, {
|
|
headers: { accept: "text/plain", cookie: ticketCookie.split(";")[0] }
|
|
});
|
|
assert.equal(second.status, 200);
|
|
assert.equal(await second.text(), "opencode ok\n");
|
|
assert.deepEqual(cloudApiCookies, ["hwlab_session=session-a", "hwlab_session=session-a", "hwlab_session=session-a"]);
|
|
assert.deepEqual(opencodeRequests, [
|
|
{
|
|
url: "/",
|
|
authorization: `Basic ${Buffer.from("oc_user:oc_password", "utf8").toString("base64")}`,
|
|
cookie: undefined
|
|
},
|
|
{
|
|
url: "/assets/index.js",
|
|
authorization: `Basic ${Buffer.from("oc_user:oc_password", "utf8").toString("base64")}`,
|
|
cookie: undefined
|
|
}
|
|
]);
|
|
} finally {
|
|
restoreEnv();
|
|
await close(cloudWeb);
|
|
await close(opencode);
|
|
await close(cloudApi);
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
function withEnv(values) {
|
|
const previous = new Map();
|
|
for (const [key, value] of Object.entries(values)) {
|
|
previous.set(key, process.env[key]);
|
|
process.env[key] = value;
|
|
}
|
|
return () => {
|
|
for (const [key, value] of previous.entries()) {
|
|
if (value === undefined) delete process.env[key];
|
|
else process.env[key] = value;
|
|
}
|
|
};
|
|
}
|
|
|
|
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}`;
|
|
}
|