From f03d64f6a56e0206cfdc34ad8e9dfaeb07932c2c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 28 Sep 2026 08:46:04 -0700 Subject: [PATCH] test(ui): align e2e fixtures with startup readiness Bisecting 8b3f9bd1d..9fe93a204 identifies d769e31af2b as the introducing ordering change: foreground history now waits for stream admission. Its parent 16393180bad passes the six targeted cases; the commit exposes avatar rendering and sidebar request-count failures. The mock advertised session.members.listEvidence but returned an empty object. The sharing header then crashed on members.map before avatars could render. Supply the required response through the canonical session fixture owner, alongside its existing resolve and describe projections. Force pending startup in the locked-model case, then await enabled effort controls. Force pending relay creation in the Talk case: microphone acquisition happens before admission, so wait for the input processor before emitting relay events. Keep all transcript, layout, lock, and request-count assertions. Retain main's sidebar repair 6b9da9dc028. Validation on main 9b730f0cfb4 plus this diff: - Four complete UI E2E files: 27 passed; one worker, 65.44s wall. Talk file 12.254s; partial-catalog file 8.804s test time. - Both mock Gateway/session owner files: 69 tests passed, one worker. - UI typecheck, targeted lint, formatting, and diff checks passed. - Final independent autoreview: scoped-clean through P2. - Changed checks pass conflict and line-growth gates, then encounter the pre-existing OPENCLAW_* budget failure (484/483), independently reproduced from the untouched index. The coordinator owns that separate main break and explicitly authorized this scoped repair to proceed without changing it. Release-note context: test reliability only; production behavior unchanged. --- .../e2e/browser-talk-start-stop.e2e.test.ts | 24 +++++++++++--- .../model-catalog-partial-refresh.e2e.test.ts | 7 +++- ui/src/test-helpers/control-ui-e2e.ts | 9 ++---- .../control-ui-session-fixtures.ts | 32 ++++++++++++++++--- 4 files changed, 56 insertions(+), 16 deletions(-) diff --git a/ui/src/e2e/browser-talk-start-stop.e2e.test.ts b/ui/src/e2e/browser-talk-start-stop.e2e.test.ts index 18e8d020e947..5fb6d93fe076 100644 --- a/ui/src/e2e/browser-talk-start-stop.e2e.test.ts +++ b/ui/src/e2e/browser-talk-start-stop.e2e.test.ts @@ -688,6 +688,7 @@ suite.define(() => { await suite.withPage({ permissions: ["microphone"] }, async ({ page }) => { const relaySessionId = "relay-e2e-transcript"; const gateway = await installMockGateway(page, { + deferredMethods: ["talk.client.create"], methodResponses: { "talk.client.create": { provider: "openai", @@ -710,20 +711,33 @@ suite.define(() => { await page.setViewportSize({ width: 1366, height: 900 }); await page.getByRole("button", { name: "Start voice input" }).click(); await gateway.waitForRequest("talk.client.create"); - // The request is recorded before its mock response is delivered. Wait for - // microphone setup before probing relay readiness below. + // Microphone acquisition precedes relay admission and cannot prove readiness. + expect( + await page.evaluate(() => { + const state = ( + window as Window & { + openclawTalkE2eState?: { constraints: unknown[]; inputProcessor: unknown }; + } + ).openclawTalkE2eState; + return { + microphoneRequests: state?.constraints.length, + inputReady: state?.inputProcessor != null, + }; + }), + ).toEqual({ microphoneRequests: 1, inputReady: false }); + await gateway.resolveDeferred("talk.client.create"); await expect .poll(() => page.evaluate( () => ( window as Window & { - openclawTalkE2eState?: { constraints: unknown[] }; + openclawTalkE2eState?: { inputProcessor: unknown }; } - ).openclawTalkE2eState?.constraints.length, + ).openclawTalkE2eState?.inputProcessor != null, ), ) - .toBe(1); + .toBe(true); await gateway.emitGatewayEvent("talk.event", { relaySessionId, type: "ready" }); await expect .poll(() => page.locator('.agent-chat__voice-activity[data-status="listening"]').count()) diff --git a/ui/src/e2e/model-catalog-partial-refresh.e2e.test.ts b/ui/src/e2e/model-catalog-partial-refresh.e2e.test.ts index 5bced88e6f06..cf141afd7bf8 100644 --- a/ui/src/e2e/model-catalog-partial-refresh.e2e.test.ts +++ b/ui/src/e2e/model-catalog-partial-refresh.e2e.test.ts @@ -202,8 +202,9 @@ suite.define(() => { it("keeps usable effort controls when the selected model is locked during a partial refresh", async () => { await suite.withPage({ locale: "en-US" }, async ({ page }) => { - await installMockGateway(page, { + const gateway = await installMockGateway(page, { agentModel: partialConfig.agents.defaults.model, + heldMethods: ["chat.startup"], models: catalog.models, methodResponses: { "models.list": catalog, @@ -227,8 +228,12 @@ suite.define(() => { }, }); await page.goto(`${suite.server.baseUrl}chat`); + await gateway.waitForRequest("chat.startup"); const effort = page.locator("[data-chat-thinking-select]"); await expect.poll(() => effort.isVisible()).toBe(true); + expect(await effort.getAttribute("aria-disabled")).toBe("true"); + await gateway.resolveDeferred("chat.startup"); + await page.locator('[data-chat-thinking-select][aria-disabled="false"]').waitFor(); expect(await effort.getAttribute("aria-disabled")).toBe("false"); expect(await page.locator(".chat-controls__effort-picker").getAttribute("aria-hidden")).toBe( "false", diff --git a/ui/src/test-helpers/control-ui-e2e.ts b/ui/src/test-helpers/control-ui-e2e.ts index 27669530a0a2..d003c5bfa435 100644 --- a/ui/src/test-helpers/control-ui-e2e.ts +++ b/ui/src/test-helpers/control-ui-e2e.ts @@ -2097,7 +2097,9 @@ function installControlUiMockGateway( case "artifacts.download": return null; case "sessions.resolve": - return sessions.resolve(isRecord(params) ? params : {}); + case "sessions.describe": + case "session.members.listEvidence": + return sessions.readResponse(method, params, scenario); case "chat.history": case "chat.startup": { const resolution = @@ -2146,11 +2148,6 @@ function installControlUiMockGateway( : {}), }; } - case "sessions.describe": { - const key = - isRecord(params) && typeof params.key === "string" ? params.key : scenario.sessionKey; - return { session: sessions.sessionInfo(key) ?? null }; - } case "chat.metadata": return { commands: [], diff --git a/ui/src/test-helpers/control-ui-session-fixtures.ts b/ui/src/test-helpers/control-ui-session-fixtures.ts index e86a9775ca7b..2ee408c5590a 100644 --- a/ui/src/test-helpers/control-ui-session-fixtures.ts +++ b/ui/src/test-helpers/control-ui-session-fixtures.ts @@ -492,13 +492,37 @@ export function createControlUiSessionFixtures( ? { ok: true, ...only } : { ok: false, ...(matches.length ? { candidates: matches.slice(0, 10) } : {}) }; }; + // History publishes a full row replacement. An unseeded wire-only fixture + // has no canonical metadata to publish until its caller declares the row. + const sessionInfo = (key: string) => + listed.has(canonicalKey(key)) ? sample(read(key), Date.now()) : undefined; return { read, resolve, - // History publishes a full row replacement. An unseeded wire-only fixture - // has no canonical metadata to publish until its caller declares the row. - sessionInfo: (key: string) => - listed.has(canonicalKey(key)) ? sample(read(key), Date.now()) : undefined, + sessionInfo, + readResponse( + method: "sessions.resolve" | "sessions.describe" | "session.members.listEvidence", + params: unknown, + scenario: { sessionKey: string; allowedSessionVisibilities: readonly string[] }, + ) { + if (method === "sessions.resolve") { + return resolve(isRecord(params) ? params : {}); + } + const field = method === "sessions.describe" ? "key" : "sessionKey"; + const requestedKey = isRecord(params) ? params[field] : undefined; + const key = typeof requestedKey === "string" ? requestedKey : scenario.sessionKey; + if (method === "sessions.describe") { + return { session: sessionInfo(key) ?? null }; + } + const row = read(key); + return { + sessionKey: row.key, + members: [], + identities: [], + role: row.sharingRole ?? "admin", + allowedVisibilities: scenario.allowedSessionVisibilities, + }; + }, patch, abortRuns, trackRun,