From ecdaf0dc24feb4f28aa6da64ffc518bc549c81c6 Mon Sep 17 00:00:00 2001 From: imccyu <276526105+imccyu@users.noreply.github.com> Date: Mon, 27 Jul 2026 08:25:00 +0800 Subject: [PATCH] test(web): connect a Workspace in the e2e boot path and refresh goldens MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The startup-selection flow leaves a fresh world (no Workspace) in the locked view state, so every e2e scenario that types into the composer now connects one first via the shared connectFreshWorkspace helper (hero picker create-by-name dialog; the default 'workspace' name keeps the session-header cwd assertions intact). Golden refreshes carry the current composer chrome: the plan/model control seats are empty until their owning plugins register (the seats shipped without occupants on this branch), the sidebar shows the connected workspace group pre-send, and the bash details material renders Input/code/Output as separate nodes. The cancel scenario polls the frozen-partial swap instead of counting synchronously — the abort frame reaches the browser over SSE after the host settles. --- apps/web/tests/code-mode-round.e2e.ts | 4 ++- apps/web/tests/lifecycle-chrome.e2e.ts | 4 ++- apps/web/tests/live-interactions.e2e.ts | 10 +++++--- apps/web/tests/question-composer.e2e.ts | 4 ++- apps/web/tests/replay-round-trip.e2e.ts | 4 ++- apps/web/tests/smoke-real.e2e.ts | 4 ++- .../snapshots/code-mode-round/ui.expected.md | 6 ----- .../snapshots/fresh-round-trip/ui.expected.md | 6 ----- .../lifecycle-chrome/hero.expected.md | 15 +++++------ .../lifecycle-chrome/reloaded.expected.md | 6 ----- .../live-interactions/cancel.expected.md | 6 ----- .../live-interactions/error-auth.expected.md | 6 ----- .../live-interactions/retry.expected.md | 6 ----- .../navigation-panes/details-open.expected.md | 4 ++- .../question-composer/answered.expected.md | 6 ----- .../snapshots/seeded-history/ui.expected.md | 6 ----- .../snapshots/steering/settled.expected.md | 6 ----- apps/web/tests/steering.e2e.ts | 4 ++- apps/web/tests/support.ts | 25 +++++++++++++++++++ 19 files changed, 61 insertions(+), 71 deletions(-) diff --git a/apps/web/tests/code-mode-round.e2e.ts b/apps/web/tests/code-mode-round.e2e.ts index 8b25bad5ca..32c51a2a2a 100644 --- a/apps/web/tests/code-mode-round.e2e.ts +++ b/apps/web/tests/code-mode-round.e2e.ts @@ -18,7 +18,7 @@ import { captureStableAria, compareOrRefreshGolden, fixtureUserPrompts, launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold, } from './scaffold.ts' -import { saveFailureShot } from './support.ts' +import { connectFreshWorkspace, saveFailureShot } from './support.ts' const FIXTURE = fileURLToPath(new URL('./snapshots/code-mode-round/session.jsonl', import.meta.url)) const UI_EXPECTED = fileURLToPath(new URL('./snapshots/code-mode-round/ui.expected.md', import.meta.url)) @@ -48,6 +48,8 @@ describe('web e2e: Code Mode round renders nested sub-calls', () => { tripwire = watchConsole(page) await page.goto(scaffold.baseUrl, { waitUntil: 'load' }) await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) + // Fresh world: connect a Workspace so the composer scenarios start live. + await connectFreshWorkspace(page) }, 120_000) afterAll(async () => { diff --git a/apps/web/tests/lifecycle-chrome.e2e.ts b/apps/web/tests/lifecycle-chrome.e2e.ts index e52e316862..4b54242495 100644 --- a/apps/web/tests/lifecycle-chrome.e2e.ts +++ b/apps/web/tests/lifecycle-chrome.e2e.ts @@ -20,7 +20,7 @@ import { acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts, launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold, } from './scaffold.ts' -import { saveFailureShot } from './support.ts' +import { connectFreshWorkspace, saveFailureShot } from './support.ts' const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/lifecycle-chrome', import.meta.url)) const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl') @@ -47,6 +47,8 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', () tripwire = watchConsole(page) await page.goto(scaffold.baseUrl, { waitUntil: 'load' }) await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) + // Fresh world: connect a Workspace so the composer scenarios start live. + await connectFreshWorkspace(page) }, 120_000) afterAll(async () => { diff --git a/apps/web/tests/live-interactions.e2e.ts b/apps/web/tests/live-interactions.e2e.ts index a74833cef6..692210b352 100644 --- a/apps/web/tests/live-interactions.e2e.ts +++ b/apps/web/tests/live-interactions.e2e.ts @@ -23,7 +23,7 @@ import { assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts, launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold, } from './scaffold.ts' -import { saveFailureShot } from './support.ts' +import { connectFreshWorkspace, saveFailureShot } from './support.ts' const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/live-interactions', import.meta.url)) const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl') @@ -94,6 +94,8 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => { tripwire = watchConsole(page) await page.goto(scaffold.baseUrl, { waitUntil: 'load' }) await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) + // Fresh world: connect a Workspace so the composer scenarios start live. + await connectFreshWorkspace(page) } /** @@ -134,9 +136,11 @@ describe('web e2e: live-turn interactions (cancel / error / retry)', () => { await page.getByRole('button', { name: 'Stop generating' }).click() await settled expect(turnEndReasons(sessionEvents).at(-1)).toBe('aborted') - // Composer recovered; no streaming node lingers. + // Composer recovered; no streaming node lingers. The host settled first + // (awaited above), but the abort frame reaches the browser over SSE — the + // frozen-partial swap is eventually consistent, so poll rather than count. await expect.poll(() => page.locator('textarea').first().isEnabled(), { timeout: 10_000 }).toBe(true) - expect(await page.locator('[data-streaming="true"]').count()).toBe(0) + await expect.poll(() => page.locator('[data-streaming="true"]').count(), { timeout: 10_000 }).toBe(0) // Golden of the aborted end-state: the prompt bubble plus the frozen // partial ('partial' is the hang entry's replayed prefix) and no more. const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold!.workspaceCwd) diff --git a/apps/web/tests/question-composer.e2e.ts b/apps/web/tests/question-composer.e2e.ts index 2c2709a8f0..46f6af7b86 100644 --- a/apps/web/tests/question-composer.e2e.ts +++ b/apps/web/tests/question-composer.e2e.ts @@ -18,7 +18,7 @@ import { assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts, launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold, } from './scaffold.ts' -import { saveFailureShot } from './support.ts' +import { connectFreshWorkspace, saveFailureShot } from './support.ts' const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/question-composer', import.meta.url)) const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl') @@ -45,6 +45,8 @@ describe('web e2e: resident question composer round trip', () => { tripwire = watchConsole(page) await page.goto(scaffold.baseUrl, { waitUntil: 'load' }) await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) + // Fresh world: connect a Workspace so the composer scenarios start live. + await connectFreshWorkspace(page) }, 120_000) afterAll(async () => { diff --git a/apps/web/tests/replay-round-trip.e2e.ts b/apps/web/tests/replay-round-trip.e2e.ts index 131f3fa5fd..10f374c920 100644 --- a/apps/web/tests/replay-round-trip.e2e.ts +++ b/apps/web/tests/replay-round-trip.e2e.ts @@ -18,7 +18,7 @@ import { assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts, launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold, } from './scaffold.ts' -import { saveFailureShot } from './support.ts' +import { connectFreshWorkspace, saveFailureShot } from './support.ts' const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/fresh-round-trip', import.meta.url)) const FIXTURE = fileURLToPath(new URL('./snapshots/fresh-round-trip/session.jsonl', import.meta.url)) @@ -47,6 +47,8 @@ describe('web e2e: fresh round trip through the real assembly', () => { tripwire = watchConsole(page) await page.goto(scaffold.baseUrl, { waitUntil: 'load' }) await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) + // Fresh world: connect a Workspace so the composer scenarios start live. + await connectFreshWorkspace(page) }, 120_000) afterAll(async () => { diff --git a/apps/web/tests/smoke-real.e2e.ts b/apps/web/tests/smoke-real.e2e.ts index 063ed71839..2980458fec 100644 --- a/apps/web/tests/smoke-real.e2e.ts +++ b/apps/web/tests/smoke-real.e2e.ts @@ -24,7 +24,7 @@ import { pathToFileURL } from 'node:url' import type { Browser, Page } from 'playwright' import { chromium } from 'playwright' import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' -import { REPO_ROOT, probeFreePort, requireDist, saveFailureShot } from './support.ts' +import { REPO_ROOT, connectFreshWorkspace, probeFreePort, requireDist, saveFailureShot } from './support.ts' /** Repo-root .env → process.env (never overrides an already-set variable). */ function loadRootEnv(): void { @@ -404,6 +404,8 @@ describe.skipIf(!process.env.DEEPSEEK_API_KEY || notReady.length > 0)('web smoke it('2+3 empty-state first send completes a real model round', async () => { onTestFailed(() => saveFailureShot(page, 'w5-first-round')) + // Fresh world: connect a Workspace so the composer starts live. + await connectFreshWorkspace(page) const input = page.locator('textarea').first() await input.waitFor({ timeout: 10_000 }) await screen(page, '02-empty-state') diff --git a/apps/web/tests/snapshots/code-mode-round/ui.expected.md b/apps/web/tests/snapshots/code-mode-round/ui.expected.md index 1c93ff2d36..99f92014ef 100644 --- a/apps/web/tests/snapshots/code-mode-round/ui.expected.md +++ b/apps/web/tests/snapshots/code-mode-round/ui.expected.md @@ -23,13 +23,7 @@ - textbox "Message the agent" - button "Add attachment": - img -- combobox "Plan mode": - - option "Plan" [selected] - - option "Agent" - combobox "Access mode": - option "Read-only" [selected] - option "Read-write" -- combobox "Model": - - option "DeepSeek-V4-Pro High" [selected] - - option "DeepSeek-V4-Pro" - button "Send message" [disabled] diff --git a/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md b/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md index a6d1203d9d..7f2d8cf09f 100644 --- a/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md +++ b/apps/web/tests/snapshots/fresh-round-trip/ui.expected.md @@ -19,13 +19,7 @@ - textbox "Message the agent" - button "Add attachment": - img -- combobox "Plan mode": - - option "Plan" [selected] - - option "Agent" - combobox "Access mode": - option "Read-only" [selected] - option "Read-write" -- combobox "Model": - - option "DeepSeek-V4-Pro High" [selected] - - option "DeepSeek-V4-Pro" - button "Send message" [disabled] diff --git a/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md b/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md index 407e1c7c5a..f280e35fc6 100644 --- a/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md +++ b/apps/web/tests/snapshots/lifecycle-chrome/hero.expected.md @@ -11,7 +11,11 @@ - button "Search sessions": - img - textbox "Search name, keywords..." -- tree "Sessions": No sessions yet +- tree "Sessions": + - treeitem "workspace 1 session" [expanded]: + - img + - text: workspace 1 session + - treeitem "New Session now" [selected] - button "设置": - img - text: 设置 @@ -23,13 +27,10 @@ - textbox "Describe what you want to build" - button "Add attachment": - img -- combobox "Plan mode": - - option "Plan" [selected] - - option "Agent" - combobox "Access mode": - option "Read-only" [selected] - option "Read-write" -- combobox "Model": - - option "DeepSeek-V4-Pro High" [selected] - - option "DeepSeek-V4-Pro" - button "Send message" [disabled] +- text: 详情 +- button "关闭详情" +- text: 点击消息流中的工具行查看详情 diff --git a/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md b/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md index 6c0b20cc22..1227617de5 100644 --- a/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md +++ b/apps/web/tests/snapshots/lifecycle-chrome/reloaded.expected.md @@ -15,13 +15,7 @@ - textbox "Message the agent" - button "Add attachment": - img -- combobox "Plan mode": - - option "Plan" [selected] - - option "Agent" - combobox "Access mode": - option "Read-only" [selected] - option "Read-write" -- combobox "Model": - - option "DeepSeek-V4-Pro High" [selected] - - option "DeepSeek-V4-Pro" - button "Send message" [disabled] diff --git a/apps/web/tests/snapshots/live-interactions/cancel.expected.md b/apps/web/tests/snapshots/live-interactions/cancel.expected.md index 1c0807b33b..c883524170 100644 --- a/apps/web/tests/snapshots/live-interactions/cancel.expected.md +++ b/apps/web/tests/snapshots/live-interactions/cancel.expected.md @@ -12,13 +12,7 @@ - textbox "Message the agent" - button "Add attachment": - img -- combobox "Plan mode": - - option "Plan" [selected] - - option "Agent" - combobox "Access mode": - option "Read-only" [selected] - option "Read-write" -- combobox "Model": - - option "DeepSeek-V4-Pro High" [selected] - - option "DeepSeek-V4-Pro" - button "Send message" [disabled] diff --git a/apps/web/tests/snapshots/live-interactions/error-auth.expected.md b/apps/web/tests/snapshots/live-interactions/error-auth.expected.md index 5862e97ab6..2a5ecc7b14 100644 --- a/apps/web/tests/snapshots/live-interactions/error-auth.expected.md +++ b/apps/web/tests/snapshots/live-interactions/error-auth.expected.md @@ -10,13 +10,7 @@ - textbox "Message the agent" - button "Add attachment": - img -- combobox "Plan mode": - - option "Plan" [selected] - - option "Agent" - combobox "Access mode": - option "Read-only" [selected] - option "Read-write" -- combobox "Model": - - option "DeepSeek-V4-Pro High" [selected] - - option "DeepSeek-V4-Pro" - button "Send message" [disabled] diff --git a/apps/web/tests/snapshots/live-interactions/retry.expected.md b/apps/web/tests/snapshots/live-interactions/retry.expected.md index ed77fac08b..bfc7a2d267 100644 --- a/apps/web/tests/snapshots/live-interactions/retry.expected.md +++ b/apps/web/tests/snapshots/live-interactions/retry.expected.md @@ -15,13 +15,7 @@ - textbox "Message the agent" - button "Add attachment": - img -- combobox "Plan mode": - - option "Plan" [selected] - - option "Agent" - combobox "Access mode": - option "Read-only" [selected] - option "Read-write" -- combobox "Model": - - option "DeepSeek-V4-Pro High" [selected] - - option "DeepSeek-V4-Pro" - button "Send message" [disabled] diff --git a/apps/web/tests/snapshots/navigation-panes/details-open.expected.md b/apps/web/tests/snapshots/navigation-panes/details-open.expected.md index 39bf528542..d69a95eb2d 100644 --- a/apps/web/tests/snapshots/navigation-panes/details-open.expected.md +++ b/apps/web/tests/snapshots/navigation-panes/details-open.expected.md @@ -1,3 +1,5 @@ - text: bash - button "关闭详情" -- text: "Input { \"command\": \"echo NAVIGATION_OK\", \"description\": \"Print NAVIGATION_OK\" } Output NAVIGATION_OK" +- text: Input +- code: "{ \"command\": \"echo NAVIGATION_OK\", \"description\": \"Print NAVIGATION_OK\" }" +- text: Output NAVIGATION_OK diff --git a/apps/web/tests/snapshots/question-composer/answered.expected.md b/apps/web/tests/snapshots/question-composer/answered.expected.md index c0e64f7bf3..be5b958bf2 100644 --- a/apps/web/tests/snapshots/question-composer/answered.expected.md +++ b/apps/web/tests/snapshots/question-composer/answered.expected.md @@ -21,13 +21,7 @@ - textbox "Message the agent" - button "Add attachment": - img -- combobox "Plan mode": - - option "Plan" [selected] - - option "Agent" - combobox "Access mode": - option "Read-only" [selected] - option "Read-write" -- combobox "Model": - - option "DeepSeek-V4-Pro High" [selected] - - option "DeepSeek-V4-Pro" - button "Send message" [disabled] diff --git a/apps/web/tests/snapshots/seeded-history/ui.expected.md b/apps/web/tests/snapshots/seeded-history/ui.expected.md index c919fccec1..3e642bafa1 100644 --- a/apps/web/tests/snapshots/seeded-history/ui.expected.md +++ b/apps/web/tests/snapshots/seeded-history/ui.expected.md @@ -24,13 +24,7 @@ - textbox "Message the agent" - button "Add attachment": - img -- combobox "Plan mode": - - option "Plan" [selected] - - option "Agent" - combobox "Access mode": - option "Read-only" [selected] - option "Read-write" -- combobox "Model": - - option "DeepSeek-V4-Pro High" [selected] - - option "DeepSeek-V4-Pro" - button "Send message" [disabled] diff --git a/apps/web/tests/snapshots/steering/settled.expected.md b/apps/web/tests/snapshots/steering/settled.expected.md index 6faa2f01a3..a887bad8eb 100644 --- a/apps/web/tests/snapshots/steering/settled.expected.md +++ b/apps/web/tests/snapshots/steering/settled.expected.md @@ -21,13 +21,7 @@ - textbox "Message the agent" - button "Add attachment": - img -- combobox "Plan mode": - - option "Plan" [selected] - - option "Agent" - combobox "Access mode": - option "Read-only" [selected] - option "Read-write" -- combobox "Model": - - option "DeepSeek-V4-Pro High" [selected] - - option "DeepSeek-V4-Pro" - button "Send message" [disabled] diff --git a/apps/web/tests/steering.e2e.ts b/apps/web/tests/steering.e2e.ts index 9b023c21d2..dc1bc657ad 100644 --- a/apps/web/tests/steering.e2e.ts +++ b/apps/web/tests/steering.e2e.ts @@ -23,7 +23,7 @@ import { assertFixtureInventory, captureStableAria, compareOrRefreshGolden, fixtureUserPrompts, launchWebScaffold, recordFixture, watchConsole, webSnapshotMode, type WebScaffold, } from './scaffold.ts' -import { saveFailureShot } from './support.ts' +import { connectFreshWorkspace, saveFailureShot } from './support.ts' const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/steering', import.meta.url)) const FIXTURE = join(SNAPSHOT_DIR, 'session.jsonl') @@ -71,6 +71,8 @@ describe('web e2e: mid-turn steering lands durably and visibly', () => { tripwire = watchConsole(page) await page.goto(scaffold.baseUrl, { waitUntil: 'load' }) await page.waitForSelector('[class*="frame"]', { timeout: 30_000 }) + // Fresh world: connect a Workspace so the composer scenarios start live. + await connectFreshWorkspace(page) }, 120_000) afterAll(async () => { diff --git a/apps/web/tests/support.ts b/apps/web/tests/support.ts index ce0a6db799..8041df307d 100644 --- a/apps/web/tests/support.ts +++ b/apps/web/tests/support.ts @@ -32,6 +32,31 @@ export function probeFreePort(): Promise { }) } +/** + * Drive the hero's workspace picker through its create-by-name dialog until + * the live composer unlocks. A fresh world has no Workspace, so the boot + * lands in the locked view state (startup auto-selection has nothing to + * select); every scenario that types into the composer must connect one + * first. The default name 'workspace' keeps the session header cwd at + * /workspace — the materialization proof several scenarios + * assert. + * @param page - the page under test. + * @param name - workspace name typed into the create dialog. + */ +export async function connectFreshWorkspace(page: Page, name = 'workspace'): Promise { + await page.getByRole('button', { name: 'Choose workspace' }).click() + await page.getByRole('menuitem', { name: 'Create workspace' }).hover() + await page.getByRole('menuitem', { name: 'Create a new workspace' }).click() + const dialog = page.getByRole('dialog', { name: 'Create a new workspace' }) + await dialog.waitFor({ timeout: 10_000 }) + await dialog.getByLabel('New workspace name').fill(name) + await dialog.getByRole('button', { name: 'Create workspace' }).click() + // The pick connected the workspace: the blank session's live composer + // replaces the locked placeholder and enables. + await page.locator('textarea:enabled[placeholder="Describe what you want to build"]') + .waitFor({ timeout: 15_000 }) +} + /** Failure evidence goes to the gitignored .artifacts/ (repo convention). */ export async function saveFailureShot(page: Page, name: string): Promise { const dir = fileURLToPath(new URL('../../../.artifacts', import.meta.url))