From fc173918f77d907efa13b65205c6f3613b7a3b3f Mon Sep 17 00:00:00 2001 From: cynarAI <155274210+cynarAI@users.noreply.github.com> Date: Thu, 24 Sep 2026 08:16:39 +0200 Subject: [PATCH] fix(e2e): harden onboarding-model-labels rpc/me intercept (#89) * fix(e2e): harden onboarding-model-labels rpc/me intercept Avoid passing a consumed fetch Response into route.fulfill alongside json, which intermittently throws Response has been disposed in CI. Add routePatchRpc helper that reads the body once and fulfills with status, headers, and patched json only. Co-authored-by: cynarAI * fix(e2e): migrate rpc/me intercept anti-pattern across specs Extend helpers with fulfillFetchedRpc for multi-procedure or in-place patches. Move onboarding, mobile, host-computer, and group-chats specs off route.fulfill({ response, json }) after response.json(). Co-authored-by: cynarAI * style(e2e): biome-format host-computer-prompt rpc/me cast --------- Co-authored-by: Cursor Agent Co-authored-by: cynarAI --- apps/web/e2e/group-chats.spec.ts | 5 ++-- apps/web/e2e/helpers.ts | 30 ++++++++++++++++++- apps/web/e2e/host-computer-prompt.spec.ts | 14 +++++---- apps/web/e2e/mobile-sidebar-swipe.spec.ts | 8 ++--- .../e2e/onboarding-model-auto-skip.spec.ts | 11 ++----- apps/web/e2e/onboarding-model-labels.spec.ts | 23 +++++--------- .../web/e2e/onboarding-model-required.spec.ts | 11 ++----- 7 files changed, 54 insertions(+), 48 deletions(-) diff --git a/apps/web/e2e/group-chats.spec.ts b/apps/web/e2e/group-chats.spec.ts index 8804b9f..46046dc 100644 --- a/apps/web/e2e/group-chats.spec.ts +++ b/apps/web/e2e/group-chats.spec.ts @@ -3,6 +3,7 @@ import { captureScreenshot, completeOnboarding, createNamedBot, + fulfillFetchedRpc, openNewGroup, rpc, signup, @@ -35,7 +36,7 @@ async function showFirstMemberWorking(route: Route) { const body = (await response.json()) as { json?: Record }; const payload = body.json; if (!payload) { - await route.fulfill({ response }); + await fulfillFetchedRpc(route, response, body); return; } if ("groups" in payload) payload.groups = patchGroupMembers(payload.groups); @@ -49,7 +50,7 @@ async function showFirstMemberWorking(route: Route) { const current = payload.current as { groups?: unknown }; current.groups = patchGroupMembers(current.groups); } - await route.fulfill({ response, json: body }); + await fulfillFetchedRpc(route, response, body); } test("create group from + and see two bots in one transcript", async ({ page }, testInfo) => { diff --git a/apps/web/e2e/helpers.ts b/apps/web/e2e/helpers.ts index a696374..1e0bd68 100644 --- a/apps/web/e2e/helpers.ts +++ b/apps/web/e2e/helpers.ts @@ -1,4 +1,6 @@ -import { expect, type Locator, type Page, type TestInfo } from "@playwright/test"; +import { expect, type Locator, type Page, type Route, type TestInfo } from "@playwright/test"; + +type RpcEnvelope = { json?: Record }; /** Unterhaltungen rows only — `.sidebar-projekte` is nested inside the same shell block on main. */ export function unterhaltungenOnly(page: Page) { @@ -29,6 +31,32 @@ export async function rpc(page: Page, procedure: string, body: unknown): Prom return parsed.json as T; } +/** Fulfill a fetched RPC response without passing a consumed `response` into `route.fulfill`. */ +export async function fulfillFetchedRpc( + route: Route, + response: Awaited>, + body: RpcEnvelope, +) { + await route.fulfill({ + status: response.status(), + headers: response.headers(), + json: body, + }); +} + +/** Patch a fetched RPC response without passing a consumed `response` into `route.fulfill`. */ +export async function routePatchRpc( + page: Page, + procedure: string, + patch: (json: Record) => Record, +) { + await page.route(`**/rpc/${procedure}`, async (route) => { + const response = await route.fetch(); + const body = (await response.json()) as { json: Record }; + await fulfillFetchedRpc(route, response, { json: patch(body.json) }); + }); +} + async function visible(locator: Locator, timeout: number) { return locator.isVisible({ timeout }).catch(() => false); } diff --git a/apps/web/e2e/host-computer-prompt.spec.ts b/apps/web/e2e/host-computer-prompt.spec.ts index 5d79e8f..f91c1ed 100644 --- a/apps/web/e2e/host-computer-prompt.spec.ts +++ b/apps/web/e2e/host-computer-prompt.spec.ts @@ -1,5 +1,5 @@ import { expect, test } from "@playwright/test"; -import { captureScreenshot, completeOnboarding, signup } from "./helpers"; +import { captureScreenshot, completeOnboarding, fulfillFetchedRpc, signup } from "./helpers"; for (const platform of ["darwin", "win32"]) { test(`host computer choice explains file access on ${platform}`, async ({ page }, testInfo) => { @@ -29,11 +29,15 @@ for (const platform of ["darwin", "win32"]) { }, platform); await page.route(/\/rpc\/(me|bootstrap)$/, async (route) => { const response = await route.fetch(); - const body = await response.json(); + const body = (await response.json()) as { + json: Record & { me?: Record }; + }; const me = route.request().url().endsWith("/me") ? body.json : body.json.me; - me.canChooseHostComputer = true; - me.computerHost = null; - await route.fulfill({ response, json: body }); + if (me) { + me.canChooseHostComputer = true; + me.computerHost = null; + } + await fulfillFetchedRpc(route, response, body); }); await page.reload(); const dialog = page.getByRole("dialog", { name: "Where should bots run?" }); diff --git a/apps/web/e2e/mobile-sidebar-swipe.spec.ts b/apps/web/e2e/mobile-sidebar-swipe.spec.ts index f4c6ef5..d7a20e7 100644 --- a/apps/web/e2e/mobile-sidebar-swipe.spec.ts +++ b/apps/web/e2e/mobile-sidebar-swipe.spec.ts @@ -1,6 +1,6 @@ import type { Page } from "@playwright/test"; import { expect, test } from "@playwright/test"; -import { captureScreenshot, completeOnboarding, signup } from "./helpers"; +import { captureScreenshot, completeOnboarding, routePatchRpc, signup } from "./helpers"; test.use({ hasTouch: true, @@ -9,11 +9,7 @@ test.use({ }); async function prepareOnboarding(page: Page) { - await page.route("**/rpc/me", async (route) => { - const response = await route.fetch(); - const body = (await response.json()) as { json: Record }; - await route.fulfill({ response, json: { json: { ...body.json, needsModel: false } } }); - }); + await routePatchRpc(page, "me", (json) => ({ ...json, needsModel: false })); await page.route("**/rpc/integrationSetup/get", (route) => route.fulfill({ json: { diff --git a/apps/web/e2e/onboarding-model-auto-skip.spec.ts b/apps/web/e2e/onboarding-model-auto-skip.spec.ts index f970cee..94c09b2 100644 --- a/apps/web/e2e/onboarding-model-auto-skip.spec.ts +++ b/apps/web/e2e/onboarding-model-auto-skip.spec.ts @@ -1,17 +1,10 @@ import { expect, test } from "@playwright/test"; -import { signup } from "./helpers"; +import { routePatchRpc, signup } from "./helpers"; test("onboarding skips model connect when a default model is already available", async ({ page, }) => { - await page.route("**/rpc/me", async (route) => { - const response = await route.fetch(); - const body = (await response.json()) as { json: Record }; - await route.fulfill({ - response, - json: { json: { ...body.json, needsModel: false } }, - }); - }); + await routePatchRpc(page, "me", (json) => ({ ...json, needsModel: false })); await page.route("**/rpc/integrationSetup/get", (route) => route.fulfill({ diff --git a/apps/web/e2e/onboarding-model-labels.spec.ts b/apps/web/e2e/onboarding-model-labels.spec.ts index 4fbb4e2..d4a8ee8 100644 --- a/apps/web/e2e/onboarding-model-labels.spec.ts +++ b/apps/web/e2e/onboarding-model-labels.spec.ts @@ -1,22 +1,13 @@ import { expect, test } from "@playwright/test"; -import { signup } from "./helpers"; +import { routePatchRpc, signup } from "./helpers"; test("onboarding no longer gates first run on a model picker", async ({ page }) => { - await page.route("**/rpc/me", async (route) => { - const response = await route.fetch(); - const body = (await response.json()) as { json: Record }; - await route.fulfill({ - response, - json: { - json: { - ...body.json, - needsModel: true, - defaultProvider: "openrouter", - defaultModel: "openai/gpt-5.6-luna", - }, - }, - }); - }); + await routePatchRpc(page, "me", (json) => ({ + ...json, + needsModel: true, + defaultProvider: "openrouter", + defaultModel: "openai/gpt-5.6-luna", + })); const stamp = Date.now(); await signup(page, `model-labels-${stamp}@rakazo.test`, "password12", `Model labels ${stamp}`); diff --git a/apps/web/e2e/onboarding-model-required.spec.ts b/apps/web/e2e/onboarding-model-required.spec.ts index 77dad43..aa4e7b0 100644 --- a/apps/web/e2e/onboarding-model-required.spec.ts +++ b/apps/web/e2e/onboarding-model-required.spec.ts @@ -1,15 +1,8 @@ import { expect, type Page, test } from "@playwright/test"; -import { signup } from "./helpers"; +import { routePatchRpc, signup } from "./helpers"; async function forceNeedsModel(page: Page) { - await page.route("**/rpc/me", async (route) => { - const response = await route.fetch(); - const body = (await response.json()) as { json: Record }; - await route.fulfill({ - response, - json: { json: { ...body.json, needsModel: true } }, - }); - }); + await routePatchRpc(page, "me", (json) => ({ ...json, needsModel: true })); } test("onboarding shows the look picker when a model is still required", async ({ page }) => {