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 <cynarAI@users.noreply.github.com>

* 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 <cynarAI@users.noreply.github.com>

* style(e2e): biome-format host-computer-prompt rpc/me cast

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: cynarAI <cynarAI@users.noreply.github.com>
This commit is contained in:
cynarAI 2026-09-24 08:16:39 +02:00 committed by GitHub
parent 8bf343f0f8
commit fc173918f7
7 changed files with 54 additions and 48 deletions

View File

@ -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<string, unknown> };
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) => {

View File

@ -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<string, unknown> };
/** 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<T>(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<ReturnType<Route["fetch"]>>,
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<string, unknown>) => Record<string, unknown>,
) {
await page.route(`**/rpc/${procedure}`, async (route) => {
const response = await route.fetch();
const body = (await response.json()) as { json: Record<string, unknown> };
await fulfillFetchedRpc(route, response, { json: patch(body.json) });
});
}
async function visible(locator: Locator, timeout: number) {
return locator.isVisible({ timeout }).catch(() => false);
}

View File

@ -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<string, unknown> & { me?: Record<string, unknown> };
};
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?" });

View File

@ -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<string, unknown> };
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: {

View File

@ -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<string, unknown> };
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({

View File

@ -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<string, unknown> };
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}`);

View File

@ -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<string, unknown> };
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 }) => {