test(ui): align e2e fixtures with startup readiness

Bisecting 8b3f9bd1d..9fe93a204 identifies d769e31af2 as the
introducing ordering change: foreground history now waits for stream
admission. Its parent 16393180ba 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 6b9da9dc02.

Validation on main 9b730f0cfb 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.
This commit is contained in:
Peter Steinberger 2026-09-28 08:46:04 -07:00
parent 9b730f0cfb
commit f03d64f6a5
4 changed files with 56 additions and 16 deletions

View file

@ -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())

View file

@ -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",

View file

@ -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: [],

View file

@ -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,