Files
next-tjwater-frontend/tests/browser/workbench-sync-reconnect.e2e.ts
T
jiang ec23d44b02 feat(agent): support Responses steering and project-aware tools
Use the dedicated TJWATER_AGENT_URL and Responses stream protocol. Keep running
turns active while users steer or queue follow-ups and edit pending delivery.
Refresh the model catalog before generation and preserve approval mode selection.

Integrate scoped investigation messaging/presentation, geographic place selection,
current Agent composer styles and supporting workbench contracts.

Validation: 77 focused Vitest checks, 21 browser cases, lint, typecheck and
production build. Preserve direct Server/Agent routing and project authentication.
2026-10-19 18:46:58 +08:00

96 lines
4.5 KiB
TypeScript

import { expect, test } from "@playwright/test";
import { mockAgentApi } from "./support/mock-agent-api";
import { mockWorkbenchContext } from "./support/mock-workbench-context";
type SyncProbe = {
available: boolean;
connections: number;
cursors: string[];
warnings: number;
disconnect: (scheduled: boolean) => void;
};
declare global {
interface Window { syncProbe: SyncProbe }
}
test("scheduled renewals stay silent while a sustained outage warns and recovers", async ({ page }) => {
await mockWorkbenchContext(page);
await mockAgentApi(page);
await page.route("**/api/v1/workbench/sync-snapshot", (route) => route.fulfill({ json: {
schema: "workbench-sync-snapshot@1", event_cursor: 41, generated_at: "2026-10-12T02:00:00Z",
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: "2026-10-12T02:00:00Z", conditions: [], alerts: [], scenarios: [],
latest_analysis: null, work_orders: [] }, tasks: [], notifications: []
} }));
await page.addInitScript(() => {
const originalFetch = window.fetch.bind(window);
let stream: ReadableStreamDefaultController<Uint8Array> | undefined;
window.syncProbe = {
available: true, connections: 0, cursors: [], warnings: 0,
disconnect: (scheduled) => {
if (!stream) throw new Error("No active event stream");
if (scheduled) {
stream.enqueue(new TextEncoder().encode("event: reconnect\ndata: {}\n\n"));
stream.close();
} else {
stream.error(new Error("Synthetic connection loss"));
}
stream = undefined;
}
};
new MutationObserver(() => {
if (document.body?.textContent?.includes("业务同步正在重连")) window.syncProbe.warnings += 1;
}).observe(document, { subtree: true, childList: true, characterData: true });
window.fetch = async (input, init) => {
const url = typeof input === "string" ? input : input instanceof URL ? input.href : input.url;
if (!url.includes("/api/v1/workbench/events")) return originalFetch(input, init);
window.syncProbe.cursors.push(new Headers(init?.headers).get("Last-Event-ID") ?? "");
if (!window.syncProbe.available) return new Response("{}", { status: 503 });
window.syncProbe.connections += 1;
const body = new ReadableStream<Uint8Array>({
start(controller) { stream = controller; },
cancel() { stream = undefined; }
});
return new Response(body, { headers: { "Content-Type": "text/event-stream" } });
};
});
const now = new Date("2026-10-12T02:00:00Z");
await page.clock.install({ time: now });
await page.clock.pauseAt(now);
await page.goto("/", { waitUntil: "domcontentloaded" });
await expect.poll(() => page.evaluate(() => window.syncProbe.connections), { timeout: 15_000 }).toBe(1);
for (let cycle = 0; cycle < 3; cycle += 1) {
await page.evaluate(() => window.syncProbe.disconnect(true));
await page.clock.runFor(1_100);
await expect.poll(() => page.evaluate(() => window.syncProbe.connections)).toBe(cycle + 2);
expect(await page.evaluate(() => window.syncProbe.warnings)).toBe(0);
}
await page.evaluate(() => {
window.syncProbe.available = false;
window.syncProbe.disconnect(false);
});
await page.clock.runFor(14_000);
await expect(page.getByText("业务同步正在重连", { exact: true })).toHaveCount(0);
await page.clock.runFor(1_100);
await expect(page.getByText("业务同步正在重连", { exact: true })).toBeVisible();
await page.clock.runFor(500);
expect(await page.evaluate(() => window.syncProbe.warnings)).toBeGreaterThan(0);
const notice = page.getByText("业务同步正在重连", { exact: true });
await expect(notice.locator("xpath=ancestor::li[@data-sonner-toast]")).toHaveCSS("opacity", "1");
await expect(notice).toBeInViewport();
await page.screenshot({ path: test.info().outputPath("sustained-outage.png") });
await page.evaluate(() => { window.syncProbe.available = true; });
await page.clock.runFor(16_000);
await expect.poll(() => page.evaluate(() => window.syncProbe.connections)).toBe(5);
await page.clock.runFor(500);
await expect(page.getByText("业务同步正在重连", { exact: true })).toHaveCount(0);
expect(await page.evaluate(() => window.syncProbe.cursors.every((cursor) => cursor === "41"))).toBe(true);
await page.screenshot({ path: test.info().outputPath("recovered.png") });
});