From ae58352699b7ffd3a21c7f97902feb01da86dfec Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 15:04:09 +0000 Subject: [PATCH 1/3] fix(ui-e2e): studio-customize-journey reads open a connection each, off the keep-alive pool The spec's API reads are split by Studio steps the browser drives, and Playwright's request context reuses a pooled keep-alive socket across them. The read after the customize step reaches that socket after about five seconds idle, against a Node 22 server that closes idle sockets at six, so on a slower runner the read hits a socket the server is closing and fails with read ECONNRESET. The spec's request fixture now sends Connection: close, so every read opens its own connection. Every assertion is unchanged. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8 --- ui/e2e/live/studio-customize-journey.spec.ts | 24 +++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/ui/e2e/live/studio-customize-journey.spec.ts b/ui/e2e/live/studio-customize-journey.spec.ts index 3b1eba7bc0b..447670b0af5 100644 --- a/ui/e2e/live/studio-customize-journey.spec.ts +++ b/ui/e2e/live/studio-customize-journey.spec.ts @@ -1,4 +1,4 @@ -import { test, expect, type APIRequestContext, type Page, type Response } from '@playwright/test'; +import { test as base, expect, type APIRequestContext, type Page, type Response } from '@playwright/test'; /** * Live e2e for the ADR-0146 customization journey, walked through the Studio @@ -43,6 +43,28 @@ import { test, expect, type APIRequestContext, type Page, type Response } from ' * two-minute budget for the specs step; the readings are on that card's PR. */ +/** + * The spec's API reads each open their own connection (`Connection: close`). + * + * Playwright's `request` pools its sockets (`http.Agent({ keepAlive: true })`, + * no idle timeout of its own), and the reads here are split by Studio steps + * the browser drives: the read after the customize step reuses a socket that + * has sat idle for about five seconds (4.7–5.2 s measured locally), the read + * after the extension step for about four. A Node 22 server closes an idle + * keep-alive socket at `keepAliveTimeout` + `keepAliveTimeoutBuffer` = 6 s, so + * on a slower runner the read reaches a socket the server is closing and fails + * with `read ECONNRESET` instead of an answer (objectstack#30524, reproduced + * against @hono/node-server at a 5996–5999 ms idle). With a connection per + * read no read depends on that race. The browser's own traffic is unchanged. + */ +const test = base.extend({ + request: async ({ playwright }, use) => { + const request = await playwright.request.newContext({ extraHTTPHeaders: { Connection: 'close' } }); + await use(request); + await request.dispose(); + }, +}); + const API = process.env.LIVE_API_URL || 'http://localhost:3000'; const BASE = 'com.example.showcase'; const PAIR = `${BASE}.custom`; From a51f4c83a1509ecae9115f90fbd4706f6e96f202 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 15:42:55 +0000 Subject: [PATCH 2/3] fix(ui-e2e): pass the project's request options to the journey's own request context by name The spec's request fixture override built its context from extraHTTPHeaders alone and relied on the runner filling in every option the call leaves out. It now passes the project's request-relevant use options explicitly (storageState, baseURL, userAgent, extraHTTPHeaders, ignoreHTTPSErrors), merging Connection: close into any extra headers, so the reads run as the signed-in admin session by declaration rather than by default. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8 --- ui/e2e/live/studio-customize-journey.spec.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/ui/e2e/live/studio-customize-journey.spec.ts b/ui/e2e/live/studio-customize-journey.spec.ts index 447670b0af5..984f72a56e1 100644 --- a/ui/e2e/live/studio-customize-journey.spec.ts +++ b/ui/e2e/live/studio-customize-journey.spec.ts @@ -56,10 +56,22 @@ import { test as base, expect, type APIRequestContext, type Page, type Response * with `read ECONNRESET` instead of an answer (objectstack#30524, reproduced * against @hono/node-server at a 5996–5999 ms idle). With a connection per * read no read depends on that race. The browser's own traffic is unchanged. + * + * The context is otherwise the one the default `request` fixture builds: the + * project's request-relevant `use` options (the signed-in `storageState`, + * `baseURL`, the device's `userAgent`, any `extraHTTPHeaders` and + * `ignoreHTTPSErrors`) are passed on by name, so the reads run as the admin + * session the spec was written for. */ const test = base.extend({ - request: async ({ playwright }, use) => { - const request = await playwright.request.newContext({ extraHTTPHeaders: { Connection: 'close' } }); + request: async ({ playwright, storageState, baseURL, userAgent, extraHTTPHeaders, ignoreHTTPSErrors }, use) => { + const request = await playwright.request.newContext({ + storageState, + baseURL, + userAgent, + ignoreHTTPSErrors, + extraHTTPHeaders: { ...extraHTTPHeaders, Connection: 'close' }, + }); await use(request); await request.dispose(); }, From 22191f13b5dd1722807d70130f9a041d1195f76b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 15:59:27 +0000 Subject: [PATCH 3/3] test(ui-live): name the request fixture's callback provide, so the hooks lint rule does not read it as React's use Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017ihL2ebbZkr2QAEG1x7UC8 --- ui/e2e/live/studio-customize-journey.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/e2e/live/studio-customize-journey.spec.ts b/ui/e2e/live/studio-customize-journey.spec.ts index 984f72a56e1..e2455bd1115 100644 --- a/ui/e2e/live/studio-customize-journey.spec.ts +++ b/ui/e2e/live/studio-customize-journey.spec.ts @@ -64,7 +64,7 @@ import { test as base, expect, type APIRequestContext, type Page, type Response * session the spec was written for. */ const test = base.extend({ - request: async ({ playwright, storageState, baseURL, userAgent, extraHTTPHeaders, ignoreHTTPSErrors }, use) => { + request: async ({ playwright, storageState, baseURL, userAgent, extraHTTPHeaders, ignoreHTTPSErrors }, provide) => { const request = await playwright.request.newContext({ storageState, baseURL, @@ -72,7 +72,7 @@ const test = base.extend({ ignoreHTTPSErrors, extraHTTPHeaders: { ...extraHTTPHeaders, Connection: 'close' }, }); - await use(request); + await provide(request); await request.dispose(); }, });