test(web): govern performance dashboard windows (#1683)

This commit is contained in:
Lyon
2026-06-20 10:53:32 +08:00
committed by GitHub
parent 001a430c36
commit 2a7fbf5771
2 changed files with 99 additions and 4 deletions
@@ -227,7 +227,9 @@ async function handleRequest(request: IncomingMessage, response: ServerResponse)
if (path === "/v1/hwpod-node-ops") return json(response, 200, hwpodNodeOpsPayload());
if (path === "/v1/web-performance/summary") {
if (state.scenarioId === "performance-summary-error") return json(response, 503, { ok: false, status: 503, error: { code: "performance_summary_unavailable", message: "performance summary read model is unavailable" } });
return json(response, 200, state.scenarioId === "performance-empty" ? webPerformanceEmptyPayload(url) : webPerformanceSummaryPayload(url));
if (state.scenarioId === "performance-empty") return json(response, 200, webPerformanceEmptyPayload(url));
if (state.scenarioId === "performance-stale-window") return json(response, 200, webPerformanceStaleWindowPayload(url));
return json(response, 200, webPerformanceSummaryPayload(url));
}
if (path.startsWith("/v1/") || path.startsWith("/auth/") || path.startsWith("/health")) return json(response, 500, { ok: false, status: 500, error: { code: "unmocked_request", path } });
@@ -364,6 +366,51 @@ function webPerformanceEmptyPayload(url: URL): JsonRecord {
};
}
function webPerformanceStaleWindowPayload(url: URL): JsonRecord {
const sampleWindow = fakePerformanceWindow(url.searchParams.get("window"));
const rows: JsonRecord[] = [];
const collectionHealth = {
schemaVersion: "hwlab-web-performance-collection-health-v1",
status: "waiting",
statusLabel: "等待数据",
reason: "no_samples_in_window",
reasonLabel: "当前窗口暂无样本",
sampleCount: 0,
retainedSampleCount: 37,
sampleSeries: 0,
lowSampleThreshold: 5,
firstSampleAt: null,
lastSampleAt: null,
lastRetainedSampleAt: "2026-06-18T03:20:00.000Z",
secondsSinceLastSample: 4200,
windowCoverageSeconds: null,
sourceFamilyCounts: [{ sourceFamily: "web", rowCount: 0, sampleCount: 37 }],
unknownAttribution: { workbenchRows: 0, rowsWithUnknown: 0, samplesWithUnknown: 0, backendUnknownRows: 0, transportUnknownRows: 0 },
diagnostics: [{ code: "no_samples_in_window", label: "当前窗口无样本", tone: "warn", detail: "保留样本存在,但当前窗口没有性能样本。" }]
};
return {
ok: true,
status: "waiting",
source: "fake-server",
observedAt: sampleWindow.generatedAt,
namespace: "hwlab-v03",
gitopsTarget: "D601/v03",
sampleWindow: performanceSampleWindowPayload(sampleWindow),
summary: performanceSummaryContract(sampleWindow, rows, { status: "waiting", sampleCount: 0, retainedSampleCount: 37, sampleSeries: 0, problemCount: 0 }),
backendEvidence: { schemaVersion: "hwlab-backend-performance-evidence-v1", source: "fake-server-backend-performance-store", observedAt: sampleWindow.generatedAt, sampleCount: 0, retainedSampleCount: 37, routeCount: 0, rows: [], available: true, valuesRedacted: true },
collectionHealth,
dashboard: webPerformanceDashboardPayload(rows, sampleWindow, collectionHealth),
rows,
problems: [],
webVitals: [],
apiRoutes: [],
longTasks: [],
workbenchJourneys: [],
workbenchEventPhases: [],
workbenchBackendEvents: []
};
}
function performanceRow(row: JsonRecord, sampleWindow: FakePerformanceWindow): JsonRecord {
const count = Number(row.count ?? 0);
const sampleState = count === 0 ? "empty" : count < 5 ? "low-sample" : "ok";
@@ -1,4 +1,18 @@
import { expect, test } from "../fixtures/test";
import type { Page } from "@playwright/test";
import { expect, fakeServerState, test } from "../fixtures/test";
type RequestLedgerEntry = { path?: string; query?: Record<string, string> };
async function performanceSummaryRequests(page: Page): Promise<RequestLedgerEntry[]> {
const state = await fakeServerState(page);
const ledger = Array.isArray(state.requestLedger) ? state.requestLedger : [];
return ledger.filter((entry): entry is RequestLedgerEntry => Boolean(entry && typeof entry === "object" && (entry as RequestLedgerEntry).path === "/v1/web-performance/summary"));
}
function countWindowRequests(entries: RequestLedgerEntry[], windowId: string): number {
return entries.filter((entry) => entry.query?.window === windowId).length;
}
test("admin HWPOD groups renders node-ops as structured UI before raw JSON is requested", async ({ page }) => {
await page.goto("/admin/hwpod-groups");
@@ -44,8 +58,25 @@ test("performance dashboard renders Chinese charts and keeps drill-down reachabl
await expect(page.locator("body")).not.toContainText(/Metric|Route|Count|Status|Samples|Problems|Workbench|Web Vitals|API Timing|Long Tasks/u);
await expect(page.locator("body")).not.toContainText(/sessionId|traceId|runId|stdout|stderr|Secret/u);
await page.getByRole("tab", { name: "1 小时" }).click();
await expect(page.locator(".performance-contract-item").filter({ hasText: "观测窗口" })).toContainText("最近 1 小时");
const windowChecks = [
{ tab: "5 分钟", windowId: "5m", label: "最近 5 分钟" },
{ tab: "15 分钟", windowId: "15m", label: "最近 15 分钟" },
{ tab: "1 小时", windowId: "1h", label: "最近 1 小时" },
{ tab: "6 小时", windowId: "6h", label: "最近 6 小时" },
{ tab: "24 小时", windowId: "24h", label: "最近 24 小时" },
{ tab: "全部", windowId: "all", label: "全部样本" }
];
for (const item of windowChecks) {
await page.getByRole("tab", { name: item.tab, exact: true }).click();
await expect(page.locator(".performance-contract-item").filter({ hasText: "观测窗口" })).toContainText(item.label);
}
const summaryRequests = await performanceSummaryRequests(page);
for (const item of windowChecks) expect(countWindowRequests(summaryRequests, item.windowId)).toBeGreaterThan(0);
const allRequestsBeforeRefresh = countWindowRequests(summaryRequests, "all");
await page.getByLabel("采集新鲜度").getByRole("button", { name: "刷新" }).click();
await expect.poll(async () => countWindowRequests(await performanceSummaryRequests(page), "all")).toBeGreaterThan(allRequestsBeforeRefresh);
await expect(page.locator(".performance-contract-item").filter({ hasText: "观测窗口" })).toContainText("全部样本");
await page.screenshot({ path: ".state/workbench-e2e/performance-dashboard-desktop.png", fullPage: true });
@@ -88,6 +119,23 @@ test.describe("performance dashboard empty collection", () => {
});
});
test.describe("performance dashboard stale current window", () => {
test.use({ scenarioId: "performance-stale-window" });
test("keeps retained samples separate from the empty current window", async ({ page }) => {
await page.goto("/performance");
await expect(page.getByRole("heading", { name: "性能监控" })).toBeVisible();
await expect(page.locator(".performance-contract-item").filter({ hasText: "样本覆盖" })).toContainText("0 样本");
await expect(page.locator(".performance-collection-item").filter({ hasText: "最近样本" })).toContainText("当前窗口暂无样本");
await expect(page.locator(".performance-collection-item").filter({ hasText: "当前窗口样本" })).toContainText("0 样本");
await expect(page.locator(".performance-collection-item").filter({ hasText: "当前窗口样本" })).toContainText("保留 37 样本");
await expect(page.getByText("当前窗口无样本")).toBeVisible();
await expect(page.getByText("保留样本存在,但当前窗口没有性能样本。")).toBeVisible();
await expect(page.locator(".performance-row-contract")).toHaveCount(0);
});
});
test.describe("performance dashboard summary error", () => {
test.use({ scenarioId: "performance-summary-error" });