From ea50fe84538e8f4f91ef211d2e54bb16b3bb17c8 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 7 Aug 2026 23:21:54 -0700 Subject: [PATCH] fix(ui): open mock catalog sessions without crashing (#120444) --- scripts/control-ui-mock-dev.ts | 56 ++++++++++++++++++++++++++++ ui/src/e2e/board-fixture.e2e.test.ts | 39 +++++++++++++++++-- 2 files changed, 91 insertions(+), 4 deletions(-) diff --git a/scripts/control-ui-mock-dev.ts b/scripts/control-ui-mock-dev.ts index 3e35f080ecd1..ce193a1accd5 100644 --- a/scripts/control-ui-mock-dev.ts +++ b/scripts/control-ui-mock-dev.ts @@ -1464,6 +1464,7 @@ async function createChatPickerScenario( "sessions.diff", "sessions.files.set", "sessions.catalog.list", + "sessions.catalog.read", "system.info", ], historyMessages: buildScrollableChatHistory(baseTime), @@ -1607,6 +1608,61 @@ async function createChatPickerScenario( }, ], }, + "sessions.catalog.read": { + cases: [ + { + match: { catalogId: "codex", hostId: "gateway", threadId: "codex-thread-1" }, + response: { + hostId: "gateway", + threadId: "codex-thread-1", + items: [ + { + id: "release-checklist-answer", + type: "agentMessage", + text: "The release checklist is complete and ready for review.", + }, + { + id: "release-checklist-request", + type: "userMessage", + text: "Please sweep the release checklist for anything we missed.", + }, + ], + }, + }, + { + match: { catalogId: "codex", hostId: "gateway", threadId: "codex-thread-2" }, + response: { + hostId: "gateway", + threadId: "codex-thread-2", + items: [ + { + id: "sidebar-context-menu-answer", + type: "agentMessage", + text: "The sidebar context menu behaves as expected.", + }, + ], + }, + }, + { + match: { + catalogId: "claude-code", + hostId: "gateway", + threadId: "claude-thread-1", + }, + response: { + hostId: "gateway", + threadId: "claude-thread-1", + items: [ + { + id: "docs-refresh-answer", + type: "agentMessage", + text: "The documentation refresh is ready for review.", + }, + ], + }, + }, + ], + }, "system.info": { machineName: "Peters-Mac-Studio", hostname: "peters-mac-studio.local", diff --git a/ui/src/e2e/board-fixture.e2e.test.ts b/ui/src/e2e/board-fixture.e2e.test.ts index a1fedfd7896b..cb19ddf992e7 100644 --- a/ui/src/e2e/board-fixture.e2e.test.ts +++ b/ui/src/e2e/board-fixture.e2e.test.ts @@ -16,7 +16,8 @@ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../ const chromiumExecutablePath = resolvePlaywrightChromiumExecutablePath(chromium.executablePath()); const chromiumAvailable = canRunPlaywrightChromium(chromiumExecutablePath); const allowMissingChromium = process.env.OPENCLAW_UI_E2E_ALLOW_MISSING_CHROMIUM === "1"; -const describeBoardFixture = chromiumAvailable || !allowMissingChromium ? describe : describe.skip; +const describeStandaloneMockServer = + chromiumAvailable || !allowMissingChromium ? describe : describe.skip; type FixtureProcess = ChildProcessByStdio; @@ -74,7 +75,7 @@ async function startFixtureServer(): Promise { for (let attempt = 0; attempt < 100; attempt += 1) { if (child.exitCode !== null || child.signalCode !== null) { - throw new Error(`board fixture server exited before startup\n${output}`); + throw new Error(`Control UI mock server exited before startup\n${output}`); } try { const response = await fetch(url); @@ -86,7 +87,7 @@ async function startFixtureServer(): Promise { } child.kill("SIGTERM"); - throw new Error(`timed out waiting for board fixture server\n${output}`); + throw new Error(`timed out waiting for Control UI mock server\n${output}`); } async function stopFixtureServer(server: FixtureServer | undefined): Promise { @@ -154,7 +155,7 @@ async function readMenuColors(page: Page): Promise<{ background: string; foregro let browser: Browser; let fixtureServer: FixtureServer; -describeBoardFixture("standalone board fixture", () => { +describeStandaloneMockServer("standalone Control UI mock server", () => { beforeAll(async () => { fixtureServer = await startFixtureServer(); browser = await chromium.launch({ executablePath: chromiumExecutablePath, headless: true }); @@ -213,4 +214,34 @@ describeBoardFixture("standalone board fixture", () => { await context.close(); } }); + + it("opens a visible catalog session with its transcript in chronological order", async () => { + const page = await browser.newPage(); + try { + await page.goto(new URL("/chat", fixtureServer.url).toString(), { waitUntil: "networkidle" }); + await page.getByText("Release checklist sweep", { exact: true }).click(); + + const transcript = [ + "Please sweep the release checklist for anything we missed.", + "The release checklist is complete and ready for review.", + ]; + await Promise.all(transcript.map((text) => page.getByText(text, { exact: true }).waitFor())); + + await expect + .poll(() => + page + .locator(".chat-thread .chat-bubble") + .allTextContents() + .then((messages) => messages.map((message) => message.trim())), + ) + .toEqual(transcript); + expect( + await page + .getByText("Cannot read properties of undefined (reading 'toReversed')", { exact: false }) + .count(), + ).toBe(0); + } finally { + await page.close(); + } + }); });