test: 收敛 Performance 迁移验证 (#1649)

This commit is contained in:
Lyon
2026-06-20 02:26:13 +08:00
committed by GitHub
parent 9493d85259
commit 037043b7c8
4 changed files with 163 additions and 23 deletions
-1
View File
@@ -1307,7 +1307,6 @@ function buildPerformanceDashboard(input: {
generatedAt: input.summary.generatedAt ?? input.observedAt,
aggregationKind: input.summary.aggregationKind ?? "exact",
approximation: input.summary.approximation ?? "exact",
bucketKind: "snapshot",
buckets: [{ id: input.sampleWindow.id, label: input.sampleWindow.label, observedAt: input.observedAt, sampleCount }]
},
freshness: {
@@ -42,6 +42,15 @@ interface ScenarioState {
liveBackfillReadyAtMs: number | null;
}
interface FakePerformanceWindow {
id: string;
label: string;
seconds: number | null;
windowFrom: string | null;
windowTo: string;
generatedAt: string;
}
const cwd = process.cwd();
const args = new Map<string, string>();
for (let index = 2; index < process.argv.length; index += 1) {
@@ -214,7 +223,10 @@ async function handleRequest(request: IncomingMessage, response: ServerResponse)
if (path === "/v1/live-builds") return json(response, 200, { status: "ok", builds: [] });
if (path === "/v1/hwpod/specs") return json(response, 200, hwpodSpecsPayload());
if (path === "/v1/hwpod-node-ops") return json(response, 200, hwpodNodeOpsPayload());
if (path === "/v1/web-performance/summary") return json(response, 200, webPerformanceSummaryPayload());
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 (path.startsWith("/v1/") || path.startsWith("/auth/") || path.startsWith("/health")) return json(response, 500, { ok: false, status: 500, error: { code: "unmocked_request", path } });
return staticFile(response, path);
@@ -261,24 +273,28 @@ function hwpodNodeOpsPayload(): JsonRecord {
};
}
function webPerformanceSummaryPayload(): JsonRecord {
function webPerformanceSummaryPayload(url: URL): JsonRecord {
const sampleWindow = fakePerformanceWindow(url.searchParams.get("window"));
const rows = [
performanceRow({ kind: "workbench-journey", metric: "session-switch-full-load", route: "/workbench/sessions/:sessionId", count: 21, p50: 0.18, p75: 0.31, p95: 1.12, tone: "warn", backend: "agentrun", transport: "sse", targetState: "completed" }),
performanceRow({ kind: "workbench-event-phase", metric: "trace-event-projected", route: "/workbench", count: 28, p50: 0.08, p75: 0.14, p95: 0.42, tone: "ok", eventType: "tool_result", phase: "render", source: "rum" }),
performanceRow({ kind: "workbench-backend-event", metric: "backend-event-visible", route: "/v1/workbench/events", count: 15, p50: 0.2, p75: 0.34, p95: 1.38, tone: "warn", eventType: "agentrun.stdout", backend: "agentrun", cache: "miss" }),
performanceRow({ kind: "web-vital", metric: "LCP", route: "/performance", count: 9, p50: 1.2, p75: 1.8, p95: 2.9, tone: "ok" }),
performanceRow({ kind: "api", metric: "fetchJson", route: "/v1/web-performance/summary", method: "GET", statusClass: "2xx", outcome: "ok", count: 17, p50: 0.09, p75: 0.15, p95: 0.33, tone: "ok" }),
performanceRow({ kind: "long-task", metric: "longtask", route: "/workbench", count: 3, p50: 0.08, p75: 0.11, p95: 0.2, tone: "warn", problem: "main-thread busy" })
performanceRow({ kind: "workbench-journey", metric: "session-switch-full-load", route: "/workbench/sessions/:sessionId", count: 21, p50: 0.18, p75: 0.31, p95: 1.12, tone: "warn", backend: "agentrun", transport: "sse", targetState: "completed", sourceFamily: "workbench" }, sampleWindow),
performanceRow({ kind: "workbench-event-phase", metric: "trace-event-projected", route: "/workbench", count: 28, p50: 0.08, p75: 0.14, p95: 0.42, tone: "ok", eventType: "tool_result", phase: "render", source: "rum", sourceFamily: "workbench" }, sampleWindow),
performanceRow({ kind: "workbench-backend-event", metric: "backend-event-visible", route: "/v1/workbench/events", count: 15, p50: 0.2, p75: 0.34, p95: 1.38, tone: "warn", eventType: "agentrun.stdout", backend: "agentrun", cache: "miss", sourceFamily: "backend" }, sampleWindow),
performanceRow({ kind: "web-vital", metric: "LCP", route: "/performance", count: 9, p50: 1.2, p75: 1.8, p95: 2.9, tone: "ok", sourceFamily: "web" }, sampleWindow),
performanceRow({ kind: "api", metric: "api_request", route: "/v1/web-performance/summary", method: "GET", statusClass: "2xx", outcome: "ok", count: 17, p50: 0.09, p75: 0.15, p95: 0.33, tone: "ok", sourceFamily: "web" }, sampleWindow),
performanceRow({ kind: "api", metric: "api_request", route: "/v1/workbench/sessions", method: "GET", statusClass: "2xx", outcome: "ok", count: 3, p50: 2.5, p75: 5, p95: 10, tone: "warn", problem: "slow-api_request", sampleState: "low-sample", freshness: "low-sample", aggregationKind: "histogram", approximation: "bucketed", sourceFamily: "web" }, sampleWindow),
performanceRow({ kind: "long-task", metric: "longtask", route: "/workbench", count: 3, p50: 0.08, p75: 0.11, p95: 0.2, tone: "warn", problem: "main-thread busy", sourceFamily: "web" }, sampleWindow)
];
const sampleCount = performanceSampleCount(rows);
return {
ok: true,
status: "ready",
source: "fake-server",
observedAt: "2026-06-18T04:30:00.000Z",
observedAt: sampleWindow.generatedAt,
namespace: "hwlab-v03",
gitopsTarget: "D601/v03",
summary: { status: "ready", sampleCount: 93, sampleSeries: 12, problemCount: 2, lowSampleThreshold: 5, workbenchJourneySeries: 1, workbenchEventPhaseSeries: 1, workbenchBackendEventVisibleSeries: 1 },
dashboard: webPerformanceDashboardPayload(rows),
sampleWindow: performanceSampleWindowPayload(sampleWindow),
summary: performanceSummaryContract(sampleWindow, rows, { status: "ready", sampleCount, sampleSeries: 12, problemCount: rows.filter((row) => row.tone === "warn").length }),
dashboard: webPerformanceDashboardPayload(rows, sampleWindow),
rows,
problems: rows.filter((row) => row.tone === "warn"),
webVitals: rows.filter((row) => row.kind === "web-vital"),
@@ -290,25 +306,68 @@ function webPerformanceSummaryPayload(): JsonRecord {
};
}
function performanceRow(row: JsonRecord): JsonRecord {
return { method: "GET", statusClass: "2xx", outcome: "ok", unit: "seconds", sampleState: "ok", ...row };
function webPerformanceEmptyPayload(url: URL): JsonRecord {
const sampleWindow = fakePerformanceWindow(url.searchParams.get("window"));
const rows: JsonRecord[] = [];
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, sampleSeries: 0, problemCount: 0 }),
dashboard: webPerformanceDashboardPayload(rows, sampleWindow),
rows,
problems: [],
webVitals: [],
apiRoutes: [],
longTasks: [],
workbenchJourneys: [],
workbenchEventPhases: [],
workbenchBackendEvents: []
};
}
function webPerformanceDashboardPayload(rows: JsonRecord[]): JsonRecord {
function performanceRow(row: JsonRecord, sampleWindow: FakePerformanceWindow): JsonRecord {
const count = Number(row.count ?? 0);
const sampleState = count === 0 ? "empty" : count < 5 ? "low-sample" : "ok";
return {
method: "GET",
statusClass: "2xx",
outcome: "ok",
unit: "seconds",
sampleState,
sampleCount: count,
sourceFamily: "mixed",
freshness: sampleState === "ok" ? "fresh" : sampleState,
aggregationKind: "exact",
approximation: "exact",
windowFrom: sampleWindow.windowFrom,
windowTo: sampleWindow.windowTo,
generatedAt: sampleWindow.generatedAt,
...row
};
}
function webPerformanceDashboardPayload(rows: JsonRecord[], sampleWindow: FakePerformanceWindow): JsonRecord {
const workbench = rows.filter((row) => String(row.kind).startsWith("workbench"));
const api = rows.filter((row) => row.kind === "api");
const vital = rows.filter((row) => row.kind === "web-vital");
const longTask = rows.filter((row) => row.kind === "long-task");
const sampleCount = performanceSampleCount(rows);
const problemCount = rows.filter((row) => row.tone === "warn").length;
return {
schemaVersion: "hwlab-web-performance-dashboard-v1",
generatedAt: "2026-06-18T04:30:00.000Z",
sampleWindow: { label: "当前采集窗口", bucketKind: "snapshot", buckets: [{ id: "current", label: "当前窗口", observedAt: "2026-06-18T04:30:00.000Z", sampleCount: 93 }] },
freshness: { observedAt: "2026-06-18T04:30:00.000Z", source: "fake-server", namespace: "hwlab-v03", gitopsTarget: "D601/v03", status: "watch", statusLabel: "预警", lowSampleThreshold: 5 },
generatedAt: sampleWindow.generatedAt,
sampleWindow: { ...performanceSampleWindowPayload(sampleWindow), buckets: [{ id: sampleWindow.id, label: sampleWindow.label, observedAt: sampleWindow.generatedAt, sampleCount }] },
freshness: { observedAt: sampleWindow.generatedAt, generatedAt: sampleWindow.generatedAt, windowFrom: sampleWindow.windowFrom, windowTo: sampleWindow.windowTo, sampleCount, source: "fake-server", sourceFamily: "mixed", namespace: "hwlab-v03", gitopsTarget: "D601/v03", status: sampleCount === 0 ? "waiting" : "watch", statusLabel: sampleCount === 0 ? "等待数据" : "预警", lowSampleThreshold: 5 },
cards: [
{ id: "health", label: "采集状态", value: "预警", unit: "状态", tone: "warn", detail: "fake-server" },
{ id: "samples", label: "样本数", value: 93, unit: "样本", tone: "ok", detail: "12 个序列" },
{ id: "workbench", label: "工作台序列", value: 3, unit: "序列", tone: "ok", detail: "hwlab-v03 / D601/v03" },
{ id: "problems", label: "问题行", value: 2, unit: "条", tone: "warn", detail: "低样本阈值 5 个样本" }
{ id: "health", label: "采集状态", value: sampleCount === 0 ? "等待数据" : "预警", unit: "状态", tone: sampleCount === 0 ? "source" : "warn", detail: "fake-server" },
{ id: "samples", label: "样本数", value: sampleCount, unit: "样本", tone: sampleCount === 0 ? "source" : "ok", detail: `${sampleWindow.label} · ${rows.length} 个序列` },
{ id: "workbench", label: "工作台序列", value: workbench.length, unit: "序列", tone: workbench.length > 0 ? "ok" : "source", detail: "hwlab-v03 / D601/v03" },
{ id: "problems", label: "问题行", value: problemCount, unit: "条", tone: problemCount > 0 ? "warn" : "ok", detail: "低样本阈值 5 个样本" }
],
trends: [
{ id: "workbench-experience", title: "工作台体感趋势", subtitle: "首屏、会话切换和提交首个可见结果的 P95", unit: "seconds", points: workbench.map(dashboardPoint) },
@@ -327,6 +386,49 @@ function webPerformanceDashboardPayload(rows: JsonRecord[]): JsonRecord {
};
}
function fakePerformanceWindow(value: string | null): FakePerformanceWindow {
const id = ["5m", "15m", "1h", "6h", "24h", "all"].includes(String(value)) ? String(value) : "15m";
const generatedAt = "2026-06-18T04:30:00.000Z";
const specs: Record<string, Omit<FakePerformanceWindow, "id" | "generatedAt" | "windowTo">> = {
"5m": { label: "最近 5 分钟", seconds: 300, windowFrom: "2026-06-18T04:25:00.000Z" },
"15m": { label: "最近 15 分钟", seconds: 900, windowFrom: "2026-06-18T04:15:00.000Z" },
"1h": { label: "最近 1 小时", seconds: 3600, windowFrom: "2026-06-18T03:30:00.000Z" },
"6h": { label: "最近 6 小时", seconds: 21600, windowFrom: "2026-06-17T22:30:00.000Z" },
"24h": { label: "最近 24 小时", seconds: 86400, windowFrom: "2026-06-17T04:30:00.000Z" },
all: { label: "全部样本", seconds: null, windowFrom: null }
};
const spec = specs[id] ?? { label: "最近 15 分钟", seconds: 900, windowFrom: "2026-06-18T04:15:00.000Z" };
return { id, generatedAt, windowTo: generatedAt, ...spec };
}
function performanceSampleWindowPayload(sampleWindow: FakePerformanceWindow): JsonRecord {
return { id: sampleWindow.id, label: sampleWindow.label, seconds: sampleWindow.seconds, windowFrom: sampleWindow.windowFrom, windowTo: sampleWindow.windowTo, generatedAt: sampleWindow.generatedAt, aggregationKind: "exact", approximation: "exact" };
}
function performanceSummaryContract(sampleWindow: FakePerformanceWindow, rows: JsonRecord[], summary: JsonRecord): JsonRecord {
return {
routeCount: rows.length,
durationSeries: rows.filter((row) => row.unit === "seconds").length,
lowSampleThreshold: 5,
workbenchJourneySeries: rows.filter((row) => row.kind === "workbench-journey").length,
workbenchEventPhaseSeries: rows.filter((row) => row.kind === "workbench-event-phase").length,
workbenchBackendEventVisibleSeries: rows.filter((row) => row.kind === "workbench-backend-event").length,
window: sampleWindow.id,
windowLabel: sampleWindow.label,
windowSeconds: sampleWindow.seconds,
windowFrom: sampleWindow.windowFrom,
windowTo: sampleWindow.windowTo,
generatedAt: sampleWindow.generatedAt,
aggregationKind: "exact",
approximation: "exact",
...summary
};
}
function performanceSampleCount(rows: JsonRecord[]): number {
return rows.reduce((total, row) => total + (Number(row.count) || 0), 0);
}
function dashboardPoint(row: JsonRecord): JsonRecord {
return { label: dashboardMetricLabel(row), detail: dashboardRouteLabel(row.route), value: row.p95, p50: row.p50, p75: row.p75, p95: row.p95, count: row.count, tone: row.tone, sampleState: row.sampleState };
}
+1 -1
View File
@@ -432,7 +432,7 @@ export interface WebPerformanceDashboardCard extends Record<string, unknown> {
export interface WebPerformanceDashboard extends Record<string, unknown> {
schemaVersion?: string;
generatedAt?: string;
sampleWindow?: WebPerformanceSampleWindow & { bucketKind?: string; buckets?: Array<Record<string, unknown>>; [key: string]: unknown };
sampleWindow?: WebPerformanceSampleWindow & { buckets?: Array<Record<string, unknown>>; [key: string]: unknown };
freshness?: { observedAt?: string; source?: string; namespace?: string; gitopsTarget?: string; status?: string; statusLabel?: string; lowSampleThreshold?: number; generatedAt?: string; windowFrom?: string | null; windowTo?: string; sampleCount?: number; sourceFamily?: string; [key: string]: unknown };
cards?: WebPerformanceDashboardCard[];
trends?: WebPerformanceDashboardTrend[];
@@ -20,14 +20,25 @@ test("performance dashboard renders Chinese charts and keeps drill-down reachabl
await page.goto("/performance");
await expect(page.getByRole("heading", { name: "性能监控" })).toBeVisible();
await expect(page.locator(".performance-contract-item")).toHaveCount(5);
await expect(page.locator(".performance-contract-item").filter({ hasText: "观测窗口" })).toContainText("最近 15 分钟");
await expect(page.locator(".performance-contract-item").filter({ hasText: "生成时间" })).toContainText("fake-server");
await expect(page.locator(".performance-contract-item").filter({ hasText: "样本覆盖" })).toContainText("路由");
await expect(page.locator(".performance-contract-item").filter({ hasText: "数据质量" })).toContainText("低样本阈值 5 个样本");
await expect(page.locator(".performance-contract-item").filter({ hasText: "统计口径" })).toContainText("精确窗口分位");
await expect(page.getByText("工作台体感趋势")).toBeVisible();
await expect(page.getByText("状态分布")).toBeVisible();
await expect(page.getByText("慢 API TopN")).toBeVisible();
await expect(page.locator("svg.performance-line-chart")).toHaveCount(3);
await expect(page.locator(".chart-point")).not.toHaveCount(0);
await expect(page.locator(".performance-row-contract").filter({ hasText: "桶化估算" }).first()).toBeVisible();
await expect(page.locator(".performance-row-contract").filter({ hasText: "低样本" }).first()).toBeVisible();
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 小时");
await page.screenshot({ path: ".state/workbench-e2e/performance-dashboard-desktop.png", fullPage: true });
await page.setViewportSize({ width: 390, height: 844 });
@@ -49,3 +60,31 @@ test("performance dashboard renders Chinese charts and keeps drill-down reachabl
const documentScrollWidth = await page.evaluate(() => Math.round(document.documentElement.scrollWidth));
expect(documentScrollWidth).toBeLessThanOrEqual(390);
});
test.describe("performance dashboard empty collection", () => {
test.use({ scenarioId: "performance-empty" });
test("renders the empty summary contract without invented rows", async ({ page }) => {
await page.goto("/performance");
await expect(page.getByRole("heading", { name: "性能监控" })).toBeVisible();
await expect(page.locator(".performance-contract-item")).toHaveCount(5);
await expect(page.locator(".performance-contract-item").filter({ hasText: "样本覆盖" })).toContainText("0 样本");
await expect(page.getByText("等待数据").first()).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" });
test("surfaces the summary read-model failure without stale dashboard data", async ({ page }) => {
await page.goto("/performance");
await expect(page.getByRole("heading", { name: "性能监控" })).toBeVisible();
await expect(page.getByText("性能监控加载失败")).toBeVisible();
await expect(page.getByRole("button", { name: "重试" })).toBeVisible();
await expect(page.locator(".performance-contract-item")).toHaveCount(0);
});
});