From 2b3c01738e5f7839b7865b0e01272b2aebb85225 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marc-Andr=C3=A9=20Bombeck?= Date: Wed, 9 Sep 2026 23:03:05 +0200 Subject: [PATCH] test(fence): switch the record over the page's own connection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FENCE-AC-07 drove its external switch through the browser context's API request context, which shares one process-wide keep-alive agent with every other API context in the runner. A POST there can be handed a connection the server has already closed on its five-second idle timeout, and a POST is never replayed on a fresh one — twice this line ended the spec with `read ECONNRESET`, on all three attempts. The switch is sent with the page's own `fetch`. It stays external in the sense the case needs: the switch UI is what runs the cache wipe and a bare `fetch` is not it, and the service worker returns without touching any non-GET, so the request reaches the network unaltered. The positive control below still proves the cached entry survived. --- e2e/v137-record-session-fence-offline.spec.ts | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/e2e/v137-record-session-fence-offline.spec.ts b/e2e/v137-record-session-fence-offline.spec.ts index d2e281d6e..13faa6b88 100644 --- a/e2e/v137-record-session-fence-offline.spec.ts +++ b/e2e/v137-record-session-fence-offline.spec.ts @@ -411,12 +411,28 @@ test.describe.serial("FENCE-AC-07 record-fence disk layers", () => { expect(stored!.epoch).toMatch(/^\d+$/); expect(stored!.scope).not.toBe("self"); - // The EXTERNAL switch: driven from a request context, so the app never - // runs its cache wipe and the entry above survives. - const left = await context.request.post("/api/account/switch", { - data: { accountId: null }, + // The EXTERNAL switch: nothing in the app initiates it, so + // `clearOfflineCachesForRecordSwitch` never runs and the entry above + // survives — the positive control below is what proves that. + // + // Sent with the page's own `fetch` rather than through + // `context.request`. Both are outside the app: the switch UI is what runs + // the wipe, and a bare `fetch` is not it (the service worker returns + // without touching any non-GET, so the request reaches the network + // unaltered). What differs is the socket. Every Playwright API context in + // the runner shares one process-wide keep-alive agent, so this POST could + // be handed a connection the server had already closed on its five-second + // idle timeout, and a POST is never replayed on a fresh one — twice this + // line ended the spec with `read ECONNRESET`, on all three attempts. + const left = await page.evaluate(async () => { + const res = await fetch("/api/account/switch", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ accountId: null }), + }); + return res.status; }); - expect(left.status()).toBe(200); + expect(left).toBe(200); const me = await context.request.get("/api/auth/me"); const now = (await me.json()).data.recordSession as { epoch: number;