From 0c7d235d3235f3a0068592185eececeb3e990a57 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Thu, 17 Sep 2026 05:12:38 +0800 Subject: [PATCH] test(dashboard): wait for the steward picker to settle and name its resolution The steward-picker assertion in the personal-workspace browser smoke read the Goal-composer runtime label once, right after the page opened. That label is resolved from the same capabilities response that carries the channel binding, so a single read can still observe the pre-fetch `Chat Codex` fallback. The execution-chip assertions never hit this, because the chip only renders once the binding lands while the picker renders immediately. The assertion now waits for the declared value, and when it never arrives it fails with the three facts that decide the label -- the rendered label, the declared endpoint and the adapters the page actually received -- plus a first-viewport screenshot. The required `dashboard-acceptance` check fails on this line in CI (`Chat runtime picker ignored the declared steward executor: Chat Codex`) and that message carried neither the resolved adapters nor an artifact, so it could not be attributed from the log alone. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../execution-chip.mjs | 77 ++++++++++++++++--- 1 file changed, 65 insertions(+), 12 deletions(-) diff --git a/examples/personal-workspace-browser/execution-chip.mjs b/examples/personal-workspace-browser/execution-chip.mjs index 72df23e75b..55e7eef9bb 100644 --- a/examples/personal-workspace-browser/execution-chip.mjs +++ b/examples/personal-workspace-browser/execution-chip.mjs @@ -94,6 +94,64 @@ async function chipText(page) { return (await page.locator(".personal-execution-chip").innerText()).replace(/\s+/g, " ").trim(); } +const pickerSelector = "div.personal-agent-select button.personal-select-trigger"; + +async function readPickerLabel(page) { + return (await page.locator(pickerSelector).innerText()).replace(/\s+/g, " ").trim(); +} + +// What the page resolved, phrased so a failed CI run is diagnosable without a +// local reproduction: the declared endpoint, the adapters the page actually +// received and the label it rendered are the three facts that decide the label. +async function pickerResolution(page) { + const capabilities = await page.evaluate(async () => { + try { + const response = await fetch("/api/chat/capabilities"); + const body = await response.json(); + return { + status: response.status, + declaredEndpoint: body.manager?.channel_binding?.executor_endpoint ?? null, + adapters: (body.adapters ?? []).map( + (adapter) => `${adapter.agent_id}:${adapter.available ? "available" : "unavailable"}`, + ), + }; + } catch (error) { + return { status: "unavailable", declaredEndpoint: null, adapters: [], error: String(error) }; + } + }); + return [ + `label=${await readPickerLabel(page)}`, + `declared=${capabilities.declaredEndpoint ?? ""}`, + `adapters=[${capabilities.adapters.join(", ")}]`, + `capabilities=${capabilities.status}${capabilities.error ? ` (${capabilities.error})` : ""}`, + ].join(" "); +} + +// The picker resolves from the same capabilities response that carries the +// channel binding, and the execution chip only renders once that binding +// lands. A single read therefore asserts on a state the page never promised was +// settled, and it can still hold the pre-fetch Codex fallback. Wait for the +// declared value instead, and name what the page really resolved if the wait +// runs out. +async function waitForPickerLabel(page, settled, timeoutMs = 15_000) { + const deadline = Date.now() + timeoutMs; + let label = await readPickerLabel(page); + while (!settled(label) && Date.now() < deadline) { + await page.waitForTimeout(100); + label = await readPickerLabel(page); + } + if (settled(label)) { + return label; + } + const resolution = await pickerResolution(page); + await page.screenshot({ + animations: "disabled", + fullPage: false, + path: resolve(outputDir, "execution-chip-picker-unresolved.png"), + }); + throw new Error(`Chat runtime picker never settled: ${resolution}`); +} + async function assertHairlineRow(page) { const headerBox = await page.locator(".personal-channel-header").boundingBox(); const chipBox = await page.locator(".personal-execution-chip").boundingBox(); @@ -214,12 +272,10 @@ export const executionChipScenario = { collectCoverage, }); try { - const pickerLabel = (await stewardPicker.page - .locator("div.personal-agent-select button.personal-select-trigger") - .innerText()).replace(/\s+/g, " ").trim(); - if (!pickerLabel.includes("DeepSeek Harness (managed)")) { - throw new Error(`Chat runtime picker ignored the declared steward executor: ${pickerLabel}`); - } + const pickerLabel = await waitForPickerLabel( + stewardPicker.page, + (label) => label.includes("DeepSeek Harness (managed)"), + ); if (pickerLabel.includes("Codex")) { throw new Error(`Chat runtime picker advertised a discovered CLI as the steward: ${pickerLabel}`); } @@ -242,12 +298,9 @@ export const executionChipScenario = { collectCoverage, }); try { - const pickerLabel = (await undeclaredSteward.page - .locator("div.personal-agent-select button.personal-select-trigger") - .innerText()).replace(/\s+/g, " ").trim(); - if (pickerLabel !== "Chat Codex") { - throw new Error(`An undeclared steward executor no longer used the shipped default: ${pickerLabel}`); - } + // The shipped default is the settled value a machine with no declared + // steward executor must keep. + await waitForPickerLabel(undeclaredSteward.page, (label) => label === "Chat Codex"); } finally { coverageEntries.push(...await undeclaredSteward.close()); }