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,