199 lines
9.3 KiB
TypeScript
199 lines
9.3 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { test } from "bun:test";
|
|
|
|
import { createCloudApiServer } from "./server.ts";
|
|
import { DASHBOARD_SUMMARY_CONTRACT_VERSION } from "./server-dashboard-http.ts";
|
|
|
|
test("dashboard summary returns one typed bounded read model from real domain contracts", async () => {
|
|
const sessionToken = "hws_dashboard_contract_secret";
|
|
const requestedNavIds: string[] = [];
|
|
const auth = {
|
|
ok: true,
|
|
actor: { id: "usr_dashboard", username: "operator", displayName: "控制台操作员", role: "admin" },
|
|
authMethod: "user-billing-session",
|
|
session: { id: "ses_dashboard" },
|
|
userBilling: { active: true, token: sessionToken, valuesRedacted: true }
|
|
};
|
|
const userBillingClient = {
|
|
configured: true,
|
|
async billingSummary(token: string, options: { limit: number }) {
|
|
assert.equal(token, sessionToken);
|
|
assert.deepEqual(options, { limit: 5 });
|
|
return {
|
|
ok: true,
|
|
status: 200,
|
|
body: {
|
|
contractVersion: "user-billing-summary-v1",
|
|
status: "ready",
|
|
planId: "plan_team",
|
|
plan: { id: "plan_team", displayName: "Team", status: "active" },
|
|
credits: { balance: 240, reserved: 20, available: 220 },
|
|
entitlements: [{ resourceType: "code-agent" }, { resourceType: "hwpod" }],
|
|
reservations: { activeCount: 1 }
|
|
}
|
|
};
|
|
}
|
|
};
|
|
const server = createCloudApiServer({
|
|
env: { HWLAB_PROJECT_MANAGEMENT_URL: "http://project-management.test", HWLAB_WORKBENCH_EMPTY_SESSION_GC_ENABLED: "0" },
|
|
accessController: dashboardAccessController(auth, userBillingClient, requestedNavIds),
|
|
userBillingClient,
|
|
hwpodNodeWsRegistry: dashboardHwpodRegistry(),
|
|
dashboardDiscoverHwpodSpecs: async () => [dashboardHwpodSpec()],
|
|
dashboardNow: () => "2026-07-13T12:00:00.000Z",
|
|
fetchImpl: async (input: URL | RequestInfo) => projectManagementResponse(new URL(String(input))),
|
|
kafkaEventBridge: disabledKafkaBridge()
|
|
});
|
|
await listen(server);
|
|
|
|
try {
|
|
const response = await fetch(`http://127.0.0.1:${(server.address() as any).port}/v1/dashboard/summary`);
|
|
assert.equal(response.status, 200);
|
|
const body = await response.json();
|
|
assert.equal(body.contractVersion, DASHBOARD_SUMMARY_CONTRACT_VERSION);
|
|
assert.equal(body.route, "/v1/dashboard/summary");
|
|
assert.equal(body.status, "ready");
|
|
assert.equal(body.actor.id, "usr_dashboard");
|
|
assert.deepEqual(body.sources.map((source: any) => [source.id, source.state]), [["work", "ready"], ["hwpod", "ready"], ["billing", "ready"]]);
|
|
assert.equal(body.work.projectCount, 2);
|
|
assert.deepEqual(body.work.taskCounts, { open: 2, inProgress: 1, blocked: 1 });
|
|
assert.deepEqual(body.work.continuations.map((task: any) => task.taskId), ["R2", "R1"]);
|
|
assert.equal(body.work.continuations[0].href, "/projects/mdtodo/sources/source-main/files/file-main/tasks/R2");
|
|
assert.equal(body.hwpod.nodeCount, 1);
|
|
assert.equal(body.hwpod.onlineNodeCount, 1);
|
|
assert.equal(body.hwpod.availableDeviceCount, 1);
|
|
assert.deepEqual(body.billing.plan, { id: "plan_team", displayName: "Team", status: "active" });
|
|
assert.deepEqual(body.billing.credits, { balance: 240, reserved: 20, available: 220 });
|
|
assert.equal(body.billing.entitlementCount, 2);
|
|
assert.equal(body.billing.activeReservationCount, 1);
|
|
assert.equal(body.issues.some((issue: any) => issue.code === "mdtodo_tasks_blocked" && issue.count === 1), true);
|
|
assert.equal(JSON.stringify(body).includes(sessionToken), false);
|
|
assert.equal(body.valuesRedacted, true);
|
|
assert.equal(requestedNavIds.includes("user.dashboard"), true);
|
|
} finally {
|
|
await close(server);
|
|
}
|
|
});
|
|
|
|
test("dashboard summary keeps successful sections when upstreams are partial, stale, or restricted", async () => {
|
|
const auth = {
|
|
ok: true,
|
|
actor: { id: "usr_dashboard_partial", username: "member", role: "user" },
|
|
authMethod: "user-billing-session",
|
|
session: { id: "ses_dashboard_partial" },
|
|
userBilling: { active: true, token: "hws_dashboard_partial", valuesRedacted: true }
|
|
};
|
|
const userBillingClient = {
|
|
configured: true,
|
|
async billingSummary() {
|
|
return { ok: true, status: 200, body: { status: "stale", observedAt: "2026-07-12T12:00:00.000Z", planId: "default", credits: { available: 8 } } };
|
|
}
|
|
};
|
|
const requestedNavIds: string[] = [];
|
|
const accessController = dashboardAccessController(auth, userBillingClient, requestedNavIds, (navId) => navId !== "admin.hwpodGroups");
|
|
const server = createCloudApiServer({
|
|
env: { HWLAB_PROJECT_MANAGEMENT_URL: "http://project-management.test", HWLAB_WORKBENCH_EMPTY_SESSION_GC_ENABLED: "0" },
|
|
accessController,
|
|
userBillingClient,
|
|
hwpodNodeWsRegistry: dashboardHwpodRegistry(),
|
|
dashboardDiscoverHwpodSpecs: async () => { throw new Error("restricted HWPOD discovery must not run"); },
|
|
dashboardNow: () => "2026-07-13T12:00:00.000Z",
|
|
fetchImpl: async (input: URL | RequestInfo) => {
|
|
const url = new URL(String(input));
|
|
if (url.searchParams.get("status") === "in_progress") return jsonResponse(503, { ok: false, error: { code: "projection_refreshing" } });
|
|
return projectManagementResponse(url);
|
|
},
|
|
kafkaEventBridge: disabledKafkaBridge()
|
|
});
|
|
await listen(server);
|
|
|
|
try {
|
|
const response = await fetch(`http://127.0.0.1:${(server.address() as any).port}/v1/dashboard/summary`);
|
|
assert.equal(response.status, 200);
|
|
const body = await response.json();
|
|
assert.equal(body.status, "partial");
|
|
assert.equal(body.work.state, "partial");
|
|
assert.equal(body.work.projectCount, 2);
|
|
assert.equal(body.work.taskCounts.inProgress, null);
|
|
assert.equal(body.hwpod.state, "restricted");
|
|
assert.equal(body.hwpod.nodeCount, null);
|
|
assert.equal(body.billing.state, "stale");
|
|
assert.equal(body.billing.credits.available, 8);
|
|
assert.equal(body.issues.some((issue: any) => issue.code === "work_summary_partial"), true);
|
|
assert.equal(body.issues.some((issue: any) => issue.code === "billing_summary_stale"), true);
|
|
assert.equal(body.issues.some((issue: any) => issue.domain === "hwpod"), false);
|
|
assert.equal(body.sources.find((source: any) => source.id === "hwpod").reason, "当前身份无 HWPOD topology 访问权限");
|
|
assert.equal(requestedNavIds.includes("user.dashboard"), true);
|
|
} finally {
|
|
await close(server);
|
|
}
|
|
});
|
|
|
|
function dashboardAccessController(auth: any, userBilling: any, requestedNavIds: string[], allow: (navId: string) => boolean = () => true) {
|
|
return {
|
|
required: true,
|
|
userBilling,
|
|
async requireNavAccess(_request: unknown, _response: unknown, navId: string) {
|
|
requestedNavIds.push(navId);
|
|
return allow(navId) ? auth : null;
|
|
},
|
|
async authenticate() { return auth; },
|
|
navAccessAllowed(_actor: unknown, navId: string) { return allow(navId); }
|
|
};
|
|
}
|
|
|
|
function projectManagementResponse(url: URL) {
|
|
if (url.pathname === "/v1/project-management/projects") {
|
|
return jsonResponse(200, { ok: true, contractVersion: "project-management-v1", projects: [{ projectId: "project_alpha", name: "Alpha", status: "active" }, { projectId: "project_beta", name: "Beta", status: "ready" }] });
|
|
}
|
|
if (url.pathname === "/v1/project-management/mdtodo/tasks") {
|
|
const status = url.searchParams.get("status");
|
|
const task = (taskId: string, taskStatus: string) => ({ taskRef: `mdtodo:source-main:file-main:${taskId}`, taskId, title: `Task ${taskId}`, status: taskStatus, projectId: "project_alpha", sourceId: "source-main", fileRef: "file-main", updatedAt: "2026-07-13T10:00:00.000Z" });
|
|
if (status === "in_progress") return jsonResponse(200, { ok: true, tasks: [task("R2", status)], page: { offset: 0, limit: 6, total: 1, hasMore: false } });
|
|
if (status === "open") return jsonResponse(200, { ok: true, tasks: [task("R1", status)], page: { offset: 0, limit: 6, total: 2, hasMore: true } });
|
|
if (status === "blocked") return jsonResponse(200, { ok: true, tasks: [task("R3", status)], page: { offset: 0, limit: 6, total: 1, hasMore: false } });
|
|
}
|
|
return jsonResponse(404, { ok: false, error: { code: "not_found" } });
|
|
}
|
|
|
|
function dashboardHwpodSpec() {
|
|
return {
|
|
ok: true,
|
|
hwpodId: "hwpod-demo",
|
|
name: "hwpod-demo",
|
|
nodeId: "node-demo",
|
|
authority: "test-registry",
|
|
specPath: "/redacted/hwpod-spec.yaml",
|
|
availability: { ok: true, status: "available", checkedAt: "2026-07-13T11:59:00.000Z" },
|
|
document: { spec: {} }
|
|
};
|
|
}
|
|
|
|
function dashboardHwpodRegistry() {
|
|
return {
|
|
describe() {
|
|
return {
|
|
mode: "hwpod-node-outbound-native-ws",
|
|
pendingCount: 0,
|
|
nodes: [{ nodeId: "node-demo", name: "Demo Node", status: "online", capabilities: ["node.health", "node.version", "node.inventory", "node.diagnostics"], inFlightCount: 0, maxInFlight: 1 }]
|
|
};
|
|
}
|
|
};
|
|
}
|
|
|
|
function disabledKafkaBridge() {
|
|
return { ready: Promise.resolve(), async stop() {}, describe() { return { status: "disabled" }; } };
|
|
}
|
|
|
|
function jsonResponse(status: number, body: unknown) {
|
|
return new Response(JSON.stringify(body), { status, headers: { "content-type": "application/json" } });
|
|
}
|
|
|
|
async function listen(server: any) {
|
|
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
}
|
|
|
|
async function close(server: any) {
|
|
await new Promise<void>((resolve, reject) => server.close((error: Error | null) => error ? reject(error) : resolve()));
|
|
}
|