fix(workbench): share sync reads and retry failed invalidations
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
import { mockWorkbenchContext } from "./support/mock-workbench-context";
|
||||
import { TEST_PROJECT_ID } from "./support/mock-scada-api";
|
||||
|
||||
test("源工单推送等待投影完成后自动进入预约任务,无需刷新快照", async ({ page }) => {
|
||||
async function expectPushedOrder(page: Page, failFirstDetail = false) {
|
||||
await mockWorkbenchContext(page);
|
||||
const base = "https://tjwater-api.invalid/api/v1";
|
||||
const taskId = "72000000-0000-4000-8000-000000000041";
|
||||
@@ -53,6 +53,7 @@ test("源工单推送等待投影完成后自动进入预约任务,无需刷
|
||||
});
|
||||
await page.route(`${base}/operational-tasks/${taskId}`, (route) => {
|
||||
detailRequests += 1;
|
||||
if (failFirstDetail && detailRequests === 1) return route.fulfill({ headers, status: 503, json: { detail: "temporary" } });
|
||||
return route.fulfill({ headers, json: task });
|
||||
});
|
||||
await page.route(`${base}/workbench/events**`, async (route) => {
|
||||
@@ -71,7 +72,12 @@ test("源工单推送等待投影完成后自动进入预约任务,无需刷
|
||||
occurred_at: now,
|
||||
metadata: sequence === 1 ? { source: "peripheral_sample", work_order_id: orderId } : { task_id: taskId }
|
||||
};
|
||||
return route.fulfill({ headers, contentType: "text/event-stream", body: `id: ${sequence}\nevent: workbench_event\ndata: ${JSON.stringify(event)}\n\n` });
|
||||
const projection = { ...event, event_seq: 3,
|
||||
topic: "workbench.work_order.changed", aggregate_type: "operational_task",
|
||||
event_id: "75000000-0000-4000-8000-000000000003" };
|
||||
const body = `id: ${sequence}\nevent: workbench_event\ndata: ${JSON.stringify(event)}\n\n`
|
||||
+ (sequence === 2 ? `id: 3\nevent: workbench_event\ndata: ${JSON.stringify(projection)}\n\n` : "");
|
||||
return route.fulfill({ headers, contentType: "text/event-stream", body });
|
||||
});
|
||||
const waitingForProjection = page.waitForRequest((request) =>
|
||||
request.url().includes("/workbench/events?after=1")
|
||||
@@ -84,8 +90,84 @@ test("源工单推送等待投影完成后自动进入预约任务,无需刷
|
||||
await waitingForProjection;
|
||||
expect(snapshotRequests).toBe(1);
|
||||
await expect(panel).not.toContainText(title);
|
||||
const caughtUp = page.waitForRequest((request) => request.url().includes("/workbench/events?after=3"));
|
||||
releaseProjection();
|
||||
await expect(panel).toContainText(title);
|
||||
expect(detailRequests).toBe(1);
|
||||
await caughtUp;
|
||||
expect(detailRequests).toBe(failFirstDetail ? 2 : 1);
|
||||
expect(snapshotRequests).toBe(1);
|
||||
}
|
||||
|
||||
test("源工单推送等待投影完成后自动进入预约任务,无需刷新快照", async ({ page }) => {
|
||||
await expectPushedOrder(page);
|
||||
});
|
||||
|
||||
test("事件详情短暂失败后定向重试,无需新的业务事件或全量快照", async ({ page }) => {
|
||||
await expectPushedOrder(page, true);
|
||||
});
|
||||
|
||||
test("事件日志清空后的合法游标重置会从零重连,不循环请求快照", async ({ page }) => {
|
||||
await mockWorkbenchContext(page);
|
||||
const base = "https://tjwater-api.invalid/api/v1";
|
||||
const now = new Date().toISOString();
|
||||
const runtime = {
|
||||
availability: "not_configured", source: "none", scenario_availability: "not_configured",
|
||||
artifact_availability: "not_configured", schedule_availability: "not_configured",
|
||||
work_order_availability: "not_configured", alert_availability: "not_configured",
|
||||
generated_at: now, conditions: [], alerts: [], scenarios: [], latest_analysis: null, work_orders: []
|
||||
};
|
||||
let snapshots = 0;
|
||||
const cursors: string[] = [];
|
||||
const headers = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "*" };
|
||||
await page.route(`${base}/workbench/sync-snapshot`, (route) => {
|
||||
snapshots += 1;
|
||||
return route.fulfill({ headers, json: {
|
||||
schema: "workbench-sync-snapshot@1", event_cursor: snapshots === 1 ? 41 : 0,
|
||||
runtime, tasks: [], notifications: [], generated_at: now
|
||||
} });
|
||||
});
|
||||
await page.route(`${base}/workbench/events**`, (route) => {
|
||||
const cursor = new URL(route.request().url()).searchParams.get("after")!;
|
||||
cursors.push(cursor);
|
||||
return route.fulfill({ headers, contentType: "text/event-stream",
|
||||
body: cursor === "41" ? "event: reset_required\ndata: {}\n\n" : ": keepalive\n\n" });
|
||||
});
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await expect.poll(() => cursors.includes("0")).toBe(true);
|
||||
expect(cursors.slice(0, 2)).toEqual(["41", "0"]);
|
||||
expect(snapshots).toBe(2);
|
||||
await expect(page.getByRole("button", { name: "工况任务", exact: true })).toBeVisible();
|
||||
});
|
||||
|
||||
test("运行态事件的快照请求短暂失败后重试,不等待其他事件", async ({ page }) => {
|
||||
await mockWorkbenchContext(page);
|
||||
const base = "https://tjwater-api.invalid/api/v1";
|
||||
const now = new Date().toISOString();
|
||||
const runtime = {
|
||||
availability: "not_configured", source: "none", scenario_availability: "not_configured",
|
||||
artifact_availability: "not_configured", schedule_availability: "not_configured",
|
||||
work_order_availability: "not_configured", alert_availability: "not_configured",
|
||||
generated_at: now, conditions: [], alerts: [], scenarios: [], latest_analysis: null, work_orders: []
|
||||
};
|
||||
let snapshots = 0;
|
||||
const headers = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "*" };
|
||||
await page.route(`${base}/workbench/sync-snapshot`, (route) => {
|
||||
snapshots += 1;
|
||||
if (snapshots === 2) return route.fulfill({ headers, status: 503, json: { detail: "temporary" } });
|
||||
return route.fulfill({ headers, json: {
|
||||
schema: "workbench-sync-snapshot@1", event_cursor: snapshots === 1 ? 41 : 42,
|
||||
runtime, tasks: [], notifications: [], generated_at: now
|
||||
} });
|
||||
});
|
||||
await page.route(`${base}/workbench/events**`, (route) => {
|
||||
const event = { schema: "workbench-event@1", event_seq: 42,
|
||||
event_id: "75000000-0000-4000-8000-000000000042", project_id: TEST_PROJECT_ID,
|
||||
topic: "analysis.run.changed", aggregate_type: "analysis_run", aggregate_id: "run-1",
|
||||
aggregate_revision: 1, occurred_at: now, metadata: {} };
|
||||
return route.fulfill({ headers, contentType: "text/event-stream", body:
|
||||
new URL(route.request().url()).searchParams.get("after") === "41"
|
||||
? `id: 42\nevent: workbench_event\ndata: ${JSON.stringify(event)}\n\n` : ": keepalive\n\n" });
|
||||
});
|
||||
await page.goto("/", { waitUntil: "domcontentloaded" });
|
||||
await expect.poll(() => snapshots).toBe(3);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user