91 lines
5.8 KiB
TypeScript
91 lines
5.8 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import type { ApiResult, HwpodNodeOpsResponse, HwpodSpecsResponse, LiveProbePayload, LiveSurface } from "../src/types/index.ts";
|
|
import { traceExplorerHref } from "../src/config/runtime.ts";
|
|
import { summarizeBuilds, summarizeHwpodNodeOps, summarizeProbeRows } from "../src/stores/workbench-live.ts";
|
|
import { liveCall } from "../src/stores/workbench.ts";
|
|
|
|
Object.defineProperty(globalThis, "window", {
|
|
configurable: true,
|
|
value: {
|
|
HWLAB_CLOUD_WEB_CONFIG: {
|
|
displayTime: { timeZone: "Asia/Shanghai", locale: "zh-CN", label: "北京时间" }
|
|
}
|
|
}
|
|
});
|
|
|
|
test("R3 probe summary exposes React WorkbenchProbe rows", () => {
|
|
const summary = summarizeProbeRows(liveSurface());
|
|
assert.equal(summary.rows.length, 3);
|
|
assert.equal(summary.okCount, 3);
|
|
assert.equal(summary.tone, "ok");
|
|
assert.ok(summary.rows.some((row) => row.path === "/health/live" && row.detail.includes("revision=c0cbdbb38d")));
|
|
});
|
|
|
|
test("R3 build summary maps live-builds services instead of raw JSON", () => {
|
|
const summary = summarizeBuilds(liveSurface());
|
|
assert.equal(summary.rows.length, 2);
|
|
assert.equal(summary.tone, "warn");
|
|
assert.match(summary.countsText ?? "", /服务 2 个/);
|
|
assert.ok(summary.rows.some((row) => row.label === "hwlab-cloud-web" && row.reason?.includes("构建时间不可用")));
|
|
});
|
|
|
|
test("R3 HWPOD summary combines specs availability, node-ops and blocker stream", () => {
|
|
const summary = summarizeHwpodNodeOps({ live: liveSurface() });
|
|
assert.equal(summary.contractVersion, "hwpod-node-ops-v1");
|
|
assert.equal(summary.hwpodName, "d601-f103-v2");
|
|
assert.equal(summary.nodeId, "node-d601-f103-v2");
|
|
assert.equal(summary.supportedOpsCount, 3);
|
|
assert.equal(summary.connectedCount, 0);
|
|
assert.ok(summary.tiles.some((tile) => tile.label === "Contract"));
|
|
assert.ok(summary.tiles.some((tile) => tile.label === "Ops"));
|
|
assert.ok(summary.errors.some((error) => error.code === "hwpod_node_unavailable"));
|
|
assert.ok(summary.streamLines.some((line) => line.includes("op_01_workspace_ls workspace.ls hwpod_node_unavailable")));
|
|
});
|
|
|
|
test("R3 live refresh keeps partial evidence when one endpoint throws", async () => {
|
|
const failed = await liveCall("live builds", async () => {
|
|
throw new Error("network edge reset");
|
|
});
|
|
assert.equal(failed.ok, false);
|
|
assert.equal(failed.status, 0);
|
|
assert.match(failed.error ?? "", /live builds: network edge reset/);
|
|
|
|
const passed = await liveCall("health/live", async () => ok<LiveProbePayload>({ status: "ok", revision: "155530d8d9355887a23a08e341291994cc312f54" }));
|
|
assert.equal(passed.ok, true);
|
|
assert.equal(passed.data?.revision, "155530d8d9355887a23a08e341291994cc312f54");
|
|
});
|
|
|
|
test("R3 trace explorer href is config gated and trace_id only", () => {
|
|
window.HWLAB_CLOUD_WEB_CONFIG = {
|
|
displayTime: { timeZone: "Asia/Shanghai", locale: "zh-CN", label: "北京时间" },
|
|
workbench: { traceExplorerUrlTemplate: "/v1/workbench/traces/{trace_id}/events" }
|
|
};
|
|
assert.equal(traceExplorerHref("trc_completed"), "/v1/workbench/traces/trc_completed/events");
|
|
assert.equal(traceExplorerHref("bad/trace"), null);
|
|
|
|
window.HWLAB_CLOUD_WEB_CONFIG.workbench = { traceExplorerUrlTemplate: "/trace/{trace_id}?prompt={prompt}" };
|
|
assert.equal(traceExplorerHref("trc_completed"), null);
|
|
|
|
window.HWLAB_CLOUD_WEB_CONFIG.workbench = {};
|
|
assert.equal(traceExplorerHref("trc_completed"), null);
|
|
});
|
|
|
|
function liveSurface(): LiveSurface {
|
|
const liveProbe = ok<LiveProbePayload>({ serviceId: "hwlab-cloud-api", environment: "v03", status: "ok", ready: true, revision: "c0cbdbb38de77f1ecc9d1222438a13294d81d4dd", observedAt: "2026-06-13T17:59:15.000Z" });
|
|
return {
|
|
healthLive: liveProbe,
|
|
health: ok({ serviceId: "hwlab-edge-proxy", status: "ok", ready: true, revision: "c0cbdbb38de77f1ecc9d1222438a13294d81d4dd" }),
|
|
restIndex: ok({ serviceId: "hwlab-cloud-api", status: "ok", ready: true, revision: "c0cbdbb38de77f1ecc9d1222438a13294d81d4dd" }),
|
|
hwpodSpecs: ok<HwpodSpecsResponse>({ ok: true, status: "completed", count: 1, availableCount: 0, nodeOpsContractVersion: "hwpod-node-ops-v1", specs: [{ name: "d601-f103-v2", hwpodId: "d601-f103-v2", nodeId: "node-d601-f103-v2", workspacePath: "F:\\Work\\D601-HWLAB", source: { caseId: "d601-f103-v2-compile" }, availability: { ok: false, status: "blocked", checkedAt: "2026-06-13T17:59:15.785Z", results: [{ opId: "op_01_workspace_ls", op: "workspace.ls", ok: false, status: "blocked", blocker: { code: "hwpod_node_unavailable", layer: "hwpod-node", retryable: true, summary: "no outbound WebSocket hwpod-node is connected" } }] } }] }),
|
|
hwpodNodeOps: ok<HwpodNodeOpsResponse>({ ok: true, status: "ready", contractVersion: "hwpod-node-ops-v1", route: "/v1/hwpod-node-ops", supportedOps: ["node.health", "workspace.ls", "debug.build"], websocket: { connectedCount: 0, pendingCount: 0, nodes: [] } }),
|
|
liveBuilds: ok({ status: "ok", counts: { total: 2, withBuildTime: 0, unavailable: 2, external: 0 }, services: [{ serviceId: "hwlab-cloud-api", status: "build_time_unavailable", build: { createdAt: null, unavailableReason: "构建时间不可用:live tag 与 deploy metadata 不匹配", metadataSource: "live-health" }, image: { tag: "env-abc", digest: "sha256:abcdef0123456789" }, commit: { id: "c0cbdbb38de77f1ecc9d1222438a13294d81d4dd" }, revision: "c0cbdbb38de77f1ecc9d1222438a13294d81d4dd" }, { serviceId: "hwlab-cloud-web", status: "build_time_unavailable", build: { createdAt: null, unavailableReason: "构建时间不可用:live tag 与 deploy metadata 不匹配" }, image: { tag: "env-web" }, commit: { id: "c0cbdbb38de77f1ecc9d1222438a13294d81d4dd" }, revision: "c0cbdbb38de77f1ecc9d1222438a13294d81d4dd" }] }),
|
|
loadedAt: "2026-06-13T18:00:00.000Z"
|
|
};
|
|
}
|
|
|
|
function ok<T>(data: T): ApiResult<T> {
|
|
return { ok: true, status: 200, data, error: null };
|
|
}
|