ソースを参照

test(web): connect a Workspace in the e2e boot path and refresh goldens

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.
imccyu 1 ヶ月 前
コミット
ecdaf0dc24

+ 3 - 1
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 () => {

+ 3 - 1
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 () => {

+ 7 - 3
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)

+ 3 - 1
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 () => {

+ 3 - 1
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 () => {

+ 3 - 1
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')

+ 0 - 6
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]

+ 0 - 6
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]

+ 8 - 7
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: 点击消息流中的工具行查看详情

+ 0 - 6
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]

+ 0 - 6
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]

+ 0 - 6
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]

+ 0 - 6
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]

+ 3 - 1
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

+ 0 - 6
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]

+ 0 - 6
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]

+ 0 - 6
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]

+ 3 - 1
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 () => {

+ 25 - 0
apps/web/tests/support.ts

@@ -32,6 +32,31 @@ export function probeFreePort(): Promise<number> {
   })
 }
 
+/**
+ * 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
+ * <workspaceRoot>/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<void> {
+  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<void> {
   const dir = fileURLToPath(new URL('../../../.artifacts', import.meta.url))