From 470c69ced0f6310e8cfbe137eaacda0360f580ba Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 23 Sep 2026 08:19:25 -0700 Subject: [PATCH] test(ui): keep reconnect session fixtures consistent The reconnect cases supplied new titles only in a deferred sessions.list reply while the mock Gateway retained old canonical rows. A later sessions.describe response could correctly outrank that older list request and leave the sidebar showing the old title. Update the canonical fixture rows before reconnecting, defer the exact global preview request, and release its normal response. Preserve every assertion and reconnect/poll budget. Validation: two-CPU Testbox pressure with one busy process; controlled before failure 1/1 and after failure 0/1; natural baseline 0/3 and candidate 0/20 failures (220 assertions). All three original 78-file shard replays passed 303/303, including 33/33 target cases. A fresh-main integration also passed all 11 cases. P2 review, formatting, diff checks, typed lint, rejecting canary, coercion and dead-export checks passed. The full UI type graph retains two independently reproduced baseline Promise.withResolvers errors in chat-person-reference and chat-scroll tests. The existing browser reconnect, observer and row-retention coverage costs 44.133 seconds for all 11 cases with one worker. Sighting: CI run 35673180468, job 106574292641. Native Testbox proof: run 35869189032; integration: run 35877301218. --- .../session-management.sidebar.e2e.test.ts | 60 ++++++++++--------- 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/ui/src/e2e/session-management.sidebar.e2e.test.ts b/ui/src/e2e/session-management.sidebar.e2e.test.ts index 5291a4ef82b8..a7ad982f99bf 100644 --- a/ui/src/e2e/session-management.sidebar.e2e.test.ts +++ b/ui/src/e2e/session-management.sidebar.e2e.test.ts @@ -25,6 +25,7 @@ import { const suite = createSessionManagementE2eSuite(); const rosterMatch = { includeGlobal: true }; +const rosterPreviewMatch = { ...rosterMatch, includeLastMessage: true }; function sessionActionPresentation(button: Locator) { return button.evaluate((element) => { @@ -506,7 +507,6 @@ suite.define(() => { await expect.poll(() => pinnedEntry.count()).toBe(1); const sidebarRows = page.locator(".sidebar-recent-session"); await expect.poll(() => sidebarRows.count()).toBe(3); - const initialListCount = (await gateway.getRequests("sessions.list", rosterMatch)).length; const socketsBefore = await gateway.getSocketCount(); await gateway.setOnline(false); @@ -532,14 +532,23 @@ suite.define(() => { await expect.poll(() => pinnedEntry.count()).toBe(1); await captureUiProof(suite, page, "sidebar-sessions-during-client-replacement.png"); - await gateway.deferNext("sessions.list", { includeLastMessage: true }); + const refreshedResponse = sessionsListResponse([ + sessionRow(sessionKey, "Reconnect refreshed", Date.parse("2026-07-01T16:01:00.000Z")), + sessionRow(otherSessionKeys[0], "Other A", Date.parse("2026-07-01T15:59:00.000Z")), + sessionRow(otherSessionKeys[1], "Other B", Date.parse("2026-07-01T15:58:00.000Z")), + ]); + // Reconnect descriptors and roster reads must observe the same Gateway-owned rows. + await gateway.setSessionsListResponse(refreshedResponse); + await gateway.deferNext("sessions.list", rosterPreviewMatch); + const reconnectPreviewCount = (await gateway.getRequests("sessions.list", rosterPreviewMatch)) + .length; await gateway.setOnline(true); await waitForControlUiGatewayReady(page); await expect - .poll(async () => (await gateway.getRequests("sessions.list", rosterMatch)).length, { + .poll(async () => (await gateway.getRequests("sessions.list", rosterPreviewMatch)).length, { timeout: 15_000, }) - .toBeGreaterThan(initialListCount); + .toBeGreaterThan(reconnectPreviewCount); await sidebarRow.waitFor({ state: "visible" }); expect(await sidebarRows.count()).toBe(3); for (const otherKey of otherSessionKeys) { @@ -550,12 +559,7 @@ suite.define(() => { const firstReconnectListCount = (await gateway.getRequests("sessions.list", rosterMatch)) .length; - const refreshedResponse = sessionsListResponse([ - sessionRow(sessionKey, "Reconnect refreshed", Date.parse("2026-07-01T16:01:00.000Z")), - sessionRow(otherSessionKeys[0], "Other A", Date.parse("2026-07-01T15:59:00.000Z")), - sessionRow(otherSessionKeys[1], "Other B", Date.parse("2026-07-01T15:58:00.000Z")), - ]); - await gateway.resolveDeferred("sessions.list", refreshedResponse); + await gateway.resolveDeferred("sessions.list"); await expect.poll(() => sidebarRow.textContent()).toContain("Reconnect refreshed"); await expect.poll(() => sidebarRows.count()).toBe(3); await expectRequestCountStable( @@ -599,7 +603,6 @@ suite.define(() => { await expect.poll(() => new URL(page.url()).pathname).toBe(controlUiSessionPath(selectedKey)); await expect.poll(() => selectedRow.getAttribute("class")).toContain("--active"); const initialObserverCount = (await gateway.getRequests("sessions.subscribe")).length; - const initialListCount = (await gateway.getRequests("sessions.list", rosterMatch)).length; const socketsBefore = await gateway.getSocketCount(); await gateway.setOnline(false); @@ -609,8 +612,21 @@ suite.define(() => { await expect .poll(() => gateway.getSocketCount(), { timeout: 15_000 }) .toBe(socketsBefore + 1); + await gateway.setSessionsListResponse( + sessionsListResponse([ + sessionRow(firstKey, "First session", Date.parse("2026-07-01T16:00:00.000Z")), + sessionRow( + selectedKey, + "Selected session recovered", + Date.parse("2026-07-01T16:01:00.000Z"), + ), + ]), + ); await gateway.deferNext("sessions.subscribe"); - await gateway.deferNext("sessions.list", { includeLastMessage: true }); + await gateway.deferNext("sessions.list", rosterPreviewMatch); + // Capture after disconnect so late requests from the old client cannot satisfy this wait. + const reconnectPreviewCount = (await gateway.getRequests("sessions.list", rosterPreviewMatch)) + .length; await gateway.setOnline(true); await waitForControlUiGatewayReady(page); await expect @@ -629,23 +645,9 @@ suite.define(() => { await gateway.resolveDeferred("sessions.subscribe", { subscribed: true }); await expect - .poll(async () => - (await gateway.getRequests("sessions.list", rosterMatch)) - .slice(initialListCount) - .some((request) => requireRecord(request.params).includeLastMessage === true), - ) - .toBe(true); - await gateway.resolveDeferred( - "sessions.list", - sessionsListResponse([ - sessionRow(firstKey, "First session", Date.parse("2026-07-01T16:00:00.000Z")), - sessionRow( - selectedKey, - "Selected session recovered", - Date.parse("2026-07-01T16:01:00.000Z"), - ), - ]), - ); + .poll(async () => (await gateway.getRequests("sessions.list", rosterPreviewMatch)).length) + .toBeGreaterThan(reconnectPreviewCount); + await gateway.resolveDeferred("sessions.list"); await expect.poll(() => selectedRow.textContent()).toContain("Selected session recovered"); await expect.poll(() => selectedRow.getAttribute("class")).toContain("--active"); await expect.poll(() => page.locator(".sidebar-recent-session--active").count()).toBe(1);