Skip to content

Commit 939935b

Browse files
committed
feat(h12): WS drop+reconnect mid-train recovery e2e
Closes V5-G08 honesty gap: V5 only added the bottom-strip 'bottom-strip-reconnecting' testid; nobody had asserted the actual recovery path. The new scenario: 1. Patches window.WebSocket with an init script that records every instance on `window.__vbgui_sockets`. 2. Starts an N=32 Train. 3. Calls page.evaluate to force-close every open socket → useRpc's onclose handler fires → backend.status="disconnected"; the 2s reconnect timer schedules a fresh attempt. 4. Polls backend-status until it shows "Reconnecting/Disconnected". 5. Polls until "Backend connected" returns (real backend stayed up; the reconnect succeeds on the next attempt). 6. Asserts the pipeline.run HTTP request still completed — Train row in the result modal shows status "ok". Tests: - vbgui e2e 66_ws_reconnect.spec.ts: 1/1 passing (~3.4s). - vbgui e2e V6 suite (55–66): 13/13 passing — no regression. Backend WS 1Hz heartbeat (H12.7) was already in place (cppmega_v4/jsonrpc/server.py:117), so no backend changes needed.
1 parent 9b30206 commit 939935b

1 file changed

Lines changed: 76 additions & 0 deletions

File tree

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// H12: simulate a WS drop mid-training (via Playwright's routeWebSocket
2+
// to close the open ws://…/ws connection after a short delay) and
3+
// assert the BottomStrip transitions through reconnecting →
4+
// connected once the network/route recovers, while the Train
5+
// pipeline.run HTTP request still completes (modal shows train ok).
6+
// No spurious error modal should surface during the offline window —
7+
// the WS lives on its own reconnect timer (useRpc).
8+
9+
import { test, expect } from "@playwright/test";
10+
import { gotoApp, selectPreset, closeModal } from "../fixtures";
11+
12+
test("H12: WS drop+reconnect during Train preserves modal completion",
13+
async ({ page }) => {
14+
test.setTimeout(180_000);
15+
16+
// Wrap WebSocket so the test can close all open instances on demand.
17+
// useRpc's onclose handler then fires → disconnected → 2s reconnect.
18+
await page.addInitScript(() => {
19+
const NativeWS = window.WebSocket;
20+
const all: WebSocket[] = [];
21+
(window as unknown as { __vbgui_sockets: WebSocket[] }).__vbgui_sockets
22+
= all;
23+
class TrackedWS extends NativeWS {
24+
constructor(url: string | URL, protocols?: string | string[]) {
25+
super(url, protocols);
26+
all.push(this as unknown as WebSocket);
27+
}
28+
}
29+
// @ts-expect-error - overwriting global ctor is intentional
30+
window.WebSocket = TrackedWS;
31+
});
32+
33+
await gotoApp(page);
34+
35+
// Wait for initial backend.status=connected.
36+
await expect.poll(async () =>
37+
await page.getByTestId("backend-status").textContent(),
38+
{ timeout: 8_000 },
39+
).toContain("Backend connected");
40+
41+
await selectPreset(page, "llama3_8b");
42+
43+
// Kick off a Train.
44+
await page.getByTestId("run-pipeline-toggle").click();
45+
await page.getByTestId("train-num-steps").fill("32");
46+
await page.getByTestId("run-pipeline-train").click();
47+
await page.waitForTimeout(200);
48+
49+
// Force-close every open WebSocket on the page → useRpc.onclose
50+
// fires → fire("disconnected") → 2s reconnect timer.
51+
await page.evaluate(() => {
52+
const w = window as unknown as { __vbgui_sockets: WebSocket[] };
53+
for (const s of w.__vbgui_sockets) {
54+
try { s.close(); } catch { /* ignore */ }
55+
}
56+
});
57+
58+
// UI acknowledges the drop.
59+
await expect.poll(async () =>
60+
await page.getByTestId("backend-status").textContent(),
61+
{ timeout: 8_000 },
62+
).toMatch(/Reconnecting|Disconnected/);
63+
64+
// useRpc retries on a 2s timer → reconnects to the real backend.
65+
await expect.poll(async () =>
66+
await page.getByTestId("backend-status").textContent(),
67+
{ timeout: 15_000 },
68+
).toContain("Backend connected");
69+
70+
// The pipeline.run HTTP call should still complete; modal shows up.
71+
await page.getByTestId("run-result-modal").waitFor({ timeout: 90_000 });
72+
const trainRow = page.getByTestId("run-result-stage-train");
73+
await expect(trainRow).toBeVisible();
74+
await expect(trainRow).toContainText("ok");
75+
await closeModal(page);
76+
});

0 commit comments

Comments
 (0)