Selaa lähdekoodia

test(web): shrink the assembled jsdom lane to one built-bundle boot smoke

The seven behavior-pinning snapshots (workspace-flow, slash-flow,
todo-display, session-title, session-actions, terminal-card,
code-mode-fixture) asserted per-package behavior through the whole
built app: every assertion now lives in the owning package's suite at
equal strength (SlotTestRuntime benches assemble the same plugin apply
over the production slot machinery), while the assembled layer kept
re-failing on unrelated churn at 10s-timeout debugging cost.

What only this lane can prove is the artifact plane: lib/client.js
bundles loading through the ModuleLoader path, staged boot activation
across the inject layers, per-plugin CSS injection, and a journey
reaching chat content over the fixture transport. built-boot.snapshot.ts
pins exactly that, and nothing else — behavior assertions belong to the
package suites.
imccyu 2 kuukautta sitten
vanhempi
sitoutus
4d1ab81f05

+ 43 - 56
apps/web/tests/session-actions.snapshot.ts → apps/web/tests/built-boot.snapshot.ts

@@ -1,6 +1,15 @@
 // @vitest-environment jsdom
-// Session row actions in the assembled fixture app: Rename opens the
-// browser-owned dialog and settles the title from the unary response.
+// The built-bundle boot smoke: the ONE assembled-jsdom test that loads the
+// real `packages/client/*/lib/client.js` artifacts through AppWebEntry's
+// ModuleLoader path (fetchBundle/executeBundle) and proves the boot graph
+// assembles — staged activation across the immediately tier and the inject
+// layers, per-plugin CSS injection, and a rendered journey reaching chat
+// content from the keyless FixtureApiClient transport.
+//
+// Behavior assertions do NOT belong here: component and wiring behavior is
+// pinned by the per-package suites (SlotTestRuntime benches over src), which
+// this smoke's plugin set cannot influence — bundling, module-table
+// resolution, and boot layering are the only failure modes left to it.
 import { readFileSync } from 'node:fs'
 import { join } from 'node:path'
 import { act, cleanup, fireEvent, screen, waitFor, within } from '@testing-library/react'
@@ -16,9 +25,18 @@ const PLUGINS: readonly (WebBootEntry & { dir: string })[] = [
   { id: '@deepseek-ai/dsh-client-ui-layout', dir: 'ui-layout', url: '/plugins/ui-layout.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime'] },
   { id: '@deepseek-ai/dsh-client-ui-sidebar', dir: 'ui-sidebar', url: '/plugins/ui-sidebar.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
   { id: '@deepseek-ai/dsh-client-ui-conversation', dir: 'ui-conversation', url: '/plugins/ui-conversation.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
-  { id: '@deepseek-ai/dsh-client-ui-slash', dir: 'ui-slash', url: '/plugins/ui-slash.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-ui-conversation'] },
-  { id: '@deepseek-ai/dsh-client-ui-command', dir: 'ui-command', url: '/plugins/ui-command.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-slash', '@deepseek-ai/dsh-client-ui-conversation'] },
-  { id: '@deepseek-ai/dsh-client-ui-workspace', dir: 'ui-workspace', url: '/plugins/ui-workspace.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-ui-conversation', '@deepseek-ai/dsh-client-ui-sidebar'] },
+  {
+    id: '@deepseek-ai/dsh-client-ui-workspace',
+    dir: 'ui-workspace',
+    url: '/plugins/ui-workspace.js',
+    rev: 'fx',
+    inject: [
+      '@deepseek-ai/dsh-client-runtime',
+      '@deepseek-ai/dsh-client-ui-conversation',
+      '@deepseek-ai/dsh-client-ui-sidebar',
+    ],
+  },
+  { id: '@deepseek-ai/dsh-client-ui-trajectory', dir: 'ui-trajectory', url: '/plugins/ui-trajectory.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-conversation'] },
 ]
 
 const bundles = new Map(PLUGINS.map(plugin => [
@@ -42,16 +60,11 @@ let unmount: (() => void) | undefined
 
 beforeEach(() => {
   localStorage.clear()
-  history.replaceState(null, '', '/?fixture')
   document.title = 'DeepSeek Harness'
-  const root = document.createElement('div')
-  root.id = 'root'
-  document.body.appendChild(root)
   vi.stubGlobal('ResizeObserver', ResizeObserverStub)
   vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
     setTimeout(() => { callback(0) }, 0) as unknown as number)
   vi.stubGlobal('cancelAnimationFrame', (id: number) => { clearTimeout(id) })
-  win.__DSH_BOOT__ = { rev: 'fx', entries: PLUGINS.map(({ dir: _dir, ...plugin }) => plugin) }
 })
 
 afterEach(() => {
@@ -60,7 +73,6 @@ afterEach(() => {
   cleanup()
   delete win.__DSH_BOOT__
   delete win.__ModuleLoader__
-  delete (globalThis as Record<string, unknown>).__fxTiming
   document.body.innerHTML = ''
   document.head.querySelectorAll('style[data-plugin]').forEach((style) => { style.remove() })
   document.title = ''
@@ -68,9 +80,12 @@ afterEach(() => {
   vi.unstubAllGlobals()
 })
 
-async function bootApp(): Promise<void> {
-  const root = document.querySelector<HTMLElement>('#root')
-  if (root === null) throw new Error('snapshot root missing')
+it('boots the built plugin graph and renders a fixture session end to end', async () => {
+  history.replaceState(null, '', '/?fixture')
+  const root = document.createElement('div')
+  root.id = 'root'
+  document.body.appendChild(root)
+  win.__DSH_BOOT__ = { rev: 'fx', entries: PLUGINS.map(({ dir: _dir, ...plugin }) => plugin) }
   act(() => {
     const entry = new AppWebEntry(root, {
       fetchBundle: (url) => {
@@ -82,49 +97,21 @@ async function bootApp(): Promise<void> {
     void entry.run()
     unmount = () => { entry.dispose() }
   })
-  await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
-}
-
-/** The session row element carrying the given visible label. */
-function rowOf(label: string): HTMLElement {
-  const tree = screen.getByRole('tree', { name: 'Sessions' })
-  const row = within(tree).getByText(label).closest<HTMLElement>('[role="treeitem"]')
-  if (row === null) throw new Error(`session row "${label}" missing`)
-  return row
-}
-
-/** Open the row's ... menu and click one action. The anchor button is
- *  CSS-hover-revealed (real stylesheets are injected in this assembled run,
- *  so role queries filter it as hidden); target it directly. */
-function pickRowAction(label: string, action: string): void {
-  const anchor = rowOf(label).querySelector<HTMLElement>(`button[aria-label="Session actions for ${label}"]`)
-  if (anchor === null) throw new Error(`row menu anchor for "${label}" missing`)
-  fireEvent.click(anchor)
-  fireEvent.click(screen.getByRole('menuitem', { name: action, hidden: true }))
-}
-
-it('renames a session through the row-menu dialog; the row settles from the unary response', async () => {
-  await bootApp()
-  const sourceLabel = 'Fixture 历史会话'
-  await screen.findByText(sourceLabel)
 
-  pickRowAction(sourceLabel, 'Rename')
-  const input = await screen.findByLabelText('Session name')
-  expect((input as HTMLInputElement).value).toBe(sourceLabel)
-  fireEvent.change(input, { target: { value: '  分叉  实验记录  ' } })
-  fireEvent.click(screen.getByRole('button', { name: 'Rename' }))
+  // The sidebar renders from the boot graph: every inject layer activated.
+  const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
+  await within(tree).findByText('4 sessions')
 
-  // Host-side normalization collapses whitespace; the dialog closes on
-  // acceptance and the row re-labels without any push-frame wait.
-  const renamed = '分叉 实验记录'
-  await waitFor(() => { expect(screen.queryByLabelText('Session name')).toBeNull() })
-  await screen.findByText(renamed)
-  const tree = screen.getByRole('tree', { name: 'Sessions' })
-  expect(within(tree).queryByText(sourceLabel)).toBeNull()
+  // Opening a session reaches chat content through the fixture transport.
+  fireEvent.click(await within(tree).findByText('Fixture 历史会话'))
+  await waitFor(() => {
+    expect(document.querySelector('[data-sample="bash-global"]')).not.toBeNull()
+  }, { timeout: 10_000 })
 
-  const rows = [...tree.querySelectorAll('[role="treeitem"]')].map(row => ({
-    label: row.textContent?.replace(/\s+/g, ' ').trim() ?? '',
-  }))
-  await expect(`${JSON.stringify(rows, null, 2)}\n`)
-    .toMatchFileSnapshot('./snapshots/session-actions/rename-rows.json')
+  // Every bundle injected its plugin-owned style tag (the loader's CSS path).
+  const styleOwners = [...document.head.querySelectorAll('style[data-plugin]')]
+    .map(style => style.getAttribute('data-plugin'))
+  for (const plugin of ['@deepseek-ai/dsh-client-ui-layout', '@deepseek-ai/dsh-client-ui-sidebar', '@deepseek-ai/dsh-client-ui-conversation']) {
+    expect(styleOwners).toContain(plugin)
+  }
 })

+ 0 - 239
apps/web/tests/code-mode-fixture.snapshot.ts

@@ -1,239 +0,0 @@
-// @vitest-environment jsdom
-// Code Mode fixture snapshot over the BUILT client graph (the workspace-flow
-// idiom: real bundles via AppWebEntry, keyless FixtureApiClient transport).
-// Opens the fixture history session and pins the run_code turn's rendering:
-// the code-variant parent row titled by the model-authored description, its
-// three always-visible nested sub-rows (bash through the sample registration,
-// read through GenericToolCard, the failing read wearing the error state),
-// the expanded program body, inert bash / file-link sub-row gestures,
-// details-panel resolution of a sub-callId, and the Trajectory tab's sub-call
-// cells and timing overview.
-import { readFileSync } from 'node:fs'
-import { join } from 'node:path'
-import { act, cleanup, fireEvent, screen, waitFor, within } from '@testing-library/react'
-import { afterEach, beforeEach, expect, it, vi } from 'vitest'
-import type { WebBootEntry } from '@deepseek-ai/dsh-client-modules/client'
-import { AppWebEntry } from '@deepseek-ai/dsh-client-web'
-
-const PLUGINS: readonly (WebBootEntry & { dir: string })[] = [
-  { id: '@deepseek-ai/dsh-client-connection', dir: 'connection', url: '/plugins/connection.js', rev: 'fx', inject: [], immediately: true },
-  { id: '@deepseek-ai/dsh-client-runtime', dir: 'runtime', url: '/plugins/runtime.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-connection'], immediately: true },
-  { id: '@deepseek-ai/dsh-client-ui-theme', dir: 'ui-theme', url: '/plugins/ui-theme.js', rev: 'fx', inject: [], immediately: true },
-  { id: '@deepseek-ai/dsh-client-locale', dir: 'locale', url: '/plugins/locale.js', rev: 'fx', inject: [], immediately: true },
-  { id: '@deepseek-ai/dsh-client-ui-layout', dir: 'ui-layout', url: '/plugins/ui-layout.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime'] },
-  { id: '@deepseek-ai/dsh-client-ui-sidebar', dir: 'ui-sidebar', url: '/plugins/ui-sidebar.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
-  { id: '@deepseek-ai/dsh-client-ui-conversation', dir: 'ui-conversation', url: '/plugins/ui-conversation.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
-  {
-    id: '@deepseek-ai/dsh-client-ui-workspace',
-    dir: 'ui-workspace',
-    url: '/plugins/ui-workspace.js',
-    rev: 'fx',
-    inject: [
-      '@deepseek-ai/dsh-client-runtime',
-      '@deepseek-ai/dsh-client-ui-conversation',
-      '@deepseek-ai/dsh-client-ui-sidebar',
-    ],
-  },
-  { id: '@deepseek-ai/dsh-client-ui-trajectory', dir: 'ui-trajectory', url: '/plugins/ui-trajectory.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-conversation'] },
-]
-
-const bundles = new Map(PLUGINS.map(plugin => [
-  plugin.url,
-  readFileSync(join(process.cwd(), 'packages/client', plugin.dir, 'lib/client.js'), 'utf8'),
-]))
-
-interface FixtureWindow extends Window {
-  __DSH_BOOT__?: { rev: string; entries: WebBootEntry[] }
-  __ModuleLoader__?: unknown
-}
-
-class ResizeObserverStub {
-  observe(): void {}
-  disconnect(): void {}
-  unobserve(): void {}
-}
-
-const win = window as FixtureWindow
-let unmount: (() => void) | undefined
-
-beforeEach(() => {
-  localStorage.clear()
-  document.title = 'DeepSeek Harness'
-  vi.stubGlobal('ResizeObserver', ResizeObserverStub)
-  vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
-    setTimeout(() => { callback(0) }, 0) as unknown as number)
-  vi.stubGlobal('cancelAnimationFrame', (id: number) => { clearTimeout(id) })
-})
-
-afterEach(() => {
-  act(() => { unmount?.() })
-  unmount = undefined
-  cleanup()
-  delete win.__DSH_BOOT__
-  delete win.__ModuleLoader__
-  delete (globalThis as Record<string, unknown>).__fxTiming
-  document.body.innerHTML = ''
-  document.head.querySelectorAll('style[data-plugin]').forEach((style) => { style.remove() })
-  document.title = ''
-  history.replaceState(null, '', '/')
-  vi.unstubAllGlobals()
-})
-
-/** Boot the complete built client graph against the populated fixture branch. */
-function boot(): void {
-  history.replaceState(null, '', '/?fixture')
-  const root = document.createElement('div')
-  root.id = 'root'
-  document.body.appendChild(root)
-  win.__DSH_BOOT__ = { rev: 'fx', entries: PLUGINS.map(({ dir: _dir, ...plugin }) => plugin) }
-  act(() => {
-    const entry = new AppWebEntry(root, {
-      fetchBundle: (url) => {
-        const code = bundles.get(url)
-        return code === undefined ? Promise.reject(new Error(`missing built bundle ${url}`)) : Promise.resolve(code)
-      },
-      executeBundle: (code) => { (0, eval)(code) },
-    })
-    void entry.run()
-    unmount = () => { entry.dispose() }
-  })
-}
-
-/** Collapse decorative whitespace while preserving the text a user sees. */
-function visibleText(element: Element): string {
-  return (element.textContent ?? '').replace(/\s+/g, ' ').trim()
-}
-
-/** Open the fixture history session (the alpha log carrying the run_code turn) and scroll to its tail. */
-async function openFixtureSession(): Promise<void> {
-  const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
-  const group = within(tree).getByText('4 sessions').closest<HTMLElement>('[role="treeitem"]')
-  if (group === null) throw new Error('fixture Workspace group missing')
-  if (group.getAttribute('aria-expanded') === 'false') {
-    fireEvent.click(within(group).getByText('fixture'))
-    await waitFor(() => {
-      expect(within(tree).getByText('4 sessions').closest('[role="treeitem"]')?.getAttribute('aria-expanded')).toBe('true')
-    })
-  }
-  const session = await within(tree).findByText('Fixture 历史会话')
-  fireEvent.click(session)
-  await waitFor(() => {
-    expect(document.querySelector('[data-variant="code"]')).not.toBeNull()
-  }, { timeout: 10_000 })
-}
-
-it('renders the fixture run_code turn: code parent row, nested sub-rows, error state', async () => {
-  boot()
-  await openFixtureSession()
-
-  const codeRoot = document.querySelector('[data-variant="code"]')
-  if (codeRoot === null) throw new Error('code-variant row missing')
-  const nest = codeRoot.closest('[class*="callRow"]')?.querySelector('[data-subcalls]')
-  if (nest === undefined || nest === null) throw new Error('sub-call nest missing under the code row')
-
-  expect({
-    parentRow: visibleText(codeRoot),
-    // The three sub-rows in dispatch order: bash rides the sample plugin's
-    // keyed registration (the same one a native top-level bash row uses),
-    // both reads ride GenericToolCard.
-    bashSample: nest.querySelector('[data-sample="bash-global"]') !== null,
-    subRows: [...nest.querySelectorAll(':scope > *')].map(visibleText),
-    errorSubRow: nest.querySelector('[data-state="error"]') !== null,
-  }).toMatchInlineSnapshot(`
-    {
-      "bashSample": true,
-      "errorSubRow": true,
-      "parentRow": "CodeRead the notes files and summarize",
-      "subRows": [
-        "BashList notes",
-        "Readnotes/demo.txt",
-        "Readnotes/missing.txt",
-      ],
-    }
-  `)
-})
-
-it('expands the code row into the program body; sub-row clicks do not open details', async () => {
-  boot()
-  await openFixtureSession()
-
-  // Expand: the leading control reveals the program (shiki-tokenized: the
-  // text splits into styled spans inside one <pre class="shiki"> tree).
-  const codeRoot = document.querySelector('[data-variant="code"]')
-  if (codeRoot === null) throw new Error('code-variant row missing')
-  const toggle = codeRoot.querySelector('button[aria-expanded]')
-  if (toggle === null) throw new Error('code row expand control missing')
-  fireEvent.click(toggle)
-  await waitFor(() => {
-    // Scope to THIS row: the markdown fixture turn also renders shiki pres.
-    const pre = codeRoot.querySelector('pre.shiki')
-    if (pre === null || !(pre.textContent ?? '').includes('const listing = await tools.bash')) {
-      throw new Error('highlighted program body missing under the code row')
-    }
-  })
-
-  // Tool rows no longer drive the details panel: bash is inert, file paths
-  // are host-open links (fixture openPath is a no-op success).
-  const nest = document.querySelector('[data-subcalls]')
-  if (nest === null) throw new Error('sub-call nest missing')
-  const bashRow = nest.querySelector('[data-sample="bash-global"]')
-  if (bashRow === null) throw new Error('bash sample sub-row missing')
-  const fileLink = nest.querySelector('button')
-  if (fileLink === null) throw new Error('file-path summary link missing on a read sub-row')
-  const frame = document.querySelector('[data-details-collapsed]')
-  if (frame === null) throw new Error('app frame missing')
-  expect(frame.getAttribute('data-details-collapsed')).toBe('true')
-  fireEvent.click(bashRow)
-  expect(frame.getAttribute('data-details-collapsed')).toBe('true')
-  fireEvent.click(fileLink)
-  expect(frame.getAttribute('data-details-collapsed')).toBe('true')
-  expect({
-    fileLink: visibleText(fileLink),
-    detailsCollapsed: frame.getAttribute('data-details-collapsed'),
-  }).toMatchInlineSnapshot(`
-    {
-      "detailsCollapsed": "true",
-      "fileLink": "notes/demo.txt",
-    }
-  `)
-})
-
-it('trajectory surfaces run_code sub-calls in the ledger and timing overview', async () => {
-  boot()
-  await openFixtureSession()
-
-  // Switch to the trajectory tab (same slot ring the chat view registers in).
-  fireEvent.click(await screen.findByRole('tab', { name: 'Trajectory' }))
-  await waitFor(() => {
-    expect(document.querySelector('[data-kind="subtool"]')).not.toBeNull()
-  }, { timeout: 10_000 })
-  const subCells = [...document.querySelectorAll('[data-kind="subtool"]')]
-  expect({
-    // Three Subtool cells nested under the run_code Tool cell in dispatch
-    // order, each paired with its result preview.
-    subCells: subCells.map(cell => visibleText(cell)),
-  }).toMatchInlineSnapshot(`
-    {
-      "subCells": [
-        "SUBTOOLbash{"command":"ls notes","description":"List notes"}→demo.txt new-demo.txt",
-        "SUBTOOLread{"path":"notes/demo.txt"}→hello fixture",
-        "SUBTOOLread{"path":"notes/missing.txt"}→error",
-      ],
-    }
-  `)
-
-  const timelineSubCalls = [...document.querySelectorAll('[data-timeline-span="subtool"]')]
-  expect({
-    count: timelineSubCalls.length,
-    measured: timelineSubCalls.map(span => span.getAttribute('title')?.endsWith(' · 800 ms')),
-  }).toMatchInlineSnapshot(`
-    {
-      "count": 3,
-      "measured": [
-        true,
-        true,
-        true,
-      ],
-    }
-  `)
-})

+ 0 - 153
apps/web/tests/session-title.snapshot.ts

@@ -1,153 +0,0 @@
-// @vitest-environment jsdom
-import { readFileSync } from 'node:fs'
-import { join } from 'node:path'
-import { act, cleanup, fireEvent, screen, waitFor, within } from '@testing-library/react'
-import { afterEach, beforeEach, expect, it, vi } from 'vitest'
-import type { WebBootEntry } from '@deepseek-ai/dsh-client-modules/client'
-import { AppWebEntry } from '@deepseek-ai/dsh-client-web'
-
-const PLUGINS: readonly (WebBootEntry & { dir: string })[] = [
-  { id: '@deepseek-ai/dsh-client-connection', dir: 'connection', url: '/plugins/connection.js', rev: 'fx', inject: [], immediately: true },
-  { id: '@deepseek-ai/dsh-client-runtime', dir: 'runtime', url: '/plugins/runtime.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-connection'], immediately: true },
-  { id: '@deepseek-ai/dsh-client-ui-theme', dir: 'ui-theme', url: '/plugins/ui-theme.js', rev: 'fx', inject: [], immediately: true },
-  { id: '@deepseek-ai/dsh-client-locale', dir: 'locale', url: '/plugins/locale.js', rev: 'fx', inject: [], immediately: true },
-  { id: '@deepseek-ai/dsh-client-ui-layout', dir: 'ui-layout', url: '/plugins/ui-layout.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime'] },
-  { id: '@deepseek-ai/dsh-client-ui-sidebar', dir: 'ui-sidebar', url: '/plugins/ui-sidebar.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
-  { id: '@deepseek-ai/dsh-client-ui-settings', dir: 'ui-settings', url: '/plugins/ui-settings.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-sidebar'] },
-  { id: '@deepseek-ai/dsh-client-ui-settings-general', dir: 'ui-settings-general', url: '/plugins/ui-settings-general.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-settings', '@deepseek-ai/dsh-client-locale'] },
-  { id: '@deepseek-ai/dsh-client-ui-models', dir: 'ui-models', url: '/plugins/ui-models.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-settings'] },
-  { id: '@deepseek-ai/dsh-client-ui-conversation', dir: 'ui-conversation', url: '/plugins/ui-conversation.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
-  { id: '@deepseek-ai/dsh-client-ui-slash', dir: 'ui-slash', url: '/plugins/ui-slash.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-ui-conversation'] },
-  { id: '@deepseek-ai/dsh-client-ui-command', dir: 'ui-command', url: '/plugins/ui-command.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-slash', '@deepseek-ai/dsh-client-ui-conversation'] },
-  { id: '@deepseek-ai/dsh-client-ui-model', dir: 'ui-model', url: '/plugins/ui-model.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-ui-command'] },
-  { id: '@deepseek-ai/dsh-client-ui-workspace', dir: 'ui-workspace', url: '/plugins/ui-workspace.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-ui-conversation', '@deepseek-ai/dsh-client-ui-sidebar'] },
-  { id: '@deepseek-ai/dsh-client-ui-trajectory', dir: 'ui-trajectory', url: '/plugins/ui-trajectory.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-conversation'] },
-]
-
-const bundles = new Map(PLUGINS.map(plugin => [
-  plugin.url,
-  readFileSync(join(process.cwd(), 'packages/client', plugin.dir, 'lib/client.js'), 'utf8'),
-]))
-
-interface FixtureTiming {
-  appendTitle(id: string, title: string): void
-}
-
-interface FixtureWindow extends Window {
-  __DSH_BOOT__?: { rev: string; entries: WebBootEntry[] }
-  __ModuleLoader__?: unknown
-}
-
-class ResizeObserverStub {
-  observe(): void {}
-  disconnect(): void {}
-  unobserve(): void {}
-}
-
-const win = window as FixtureWindow
-let unmount: (() => void) | undefined
-
-beforeEach(() => {
-  localStorage.clear()
-  history.replaceState(null, '', '/?fixture')
-  document.title = 'DeepSeek Harness'
-  const root = document.createElement('div')
-  root.id = 'root'
-  document.body.appendChild(root)
-  vi.stubGlobal('ResizeObserver', ResizeObserverStub)
-  vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
-    setTimeout(() => { callback(0) }, 0) as unknown as number)
-  vi.stubGlobal('cancelAnimationFrame', (id: number) => { clearTimeout(id) })
-  win.__DSH_BOOT__ = { rev: 'fx', entries: PLUGINS.map(({ dir: _dir, ...plugin }) => plugin) }
-})
-
-afterEach(() => {
-  act(() => { unmount?.() })
-  unmount = undefined
-  cleanup()
-  delete win.__DSH_BOOT__
-  delete win.__ModuleLoader__
-  delete (globalThis as Record<string, unknown>).__fxTiming
-  document.body.innerHTML = ''
-  document.head.querySelectorAll('style[data-plugin]').forEach((style) => { style.remove() })
-  document.title = ''
-  history.replaceState(null, '', '/')
-  vi.unstubAllGlobals()
-})
-
-/** Read only the stable, user-facing title surfaces from the assembled app. */
-function titleSurfaces(label: string): { sidebar: string; breadcrumb: string; documentTitle: string } {
-  const tree = screen.getByRole('tree', { name: 'Sessions' })
-  const sidebar = within(tree).getByText(label).textContent ?? ''
-  const breadcrumb = within(screen.getByRole('navigation', { name: 'Session hierarchy' }))
-    .getByRole('button', { name: label }).textContent ?? ''
-  return { sidebar, breadcrumb, documentTitle: document.title }
-}
-
-it('projects titles and routes the next turn through the selected model in the built fixture app', async () => {
-  const root = document.querySelector<HTMLElement>('#root')
-  if (root === null) throw new Error('snapshot root missing')
-  act(() => {
-    const entry = new AppWebEntry(root, {
-      fetchBundle: (url) => {
-        const code = bundles.get(url)
-        return code === undefined ? Promise.reject(new Error(`missing built bundle ${url}`)) : Promise.resolve(code)
-      },
-      executeBundle: (code) => { (0, eval)(code) },
-    })
-    void entry.run()
-    unmount = () => { entry.dispose() }
-  })
-
-  const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
-  // The fixture Intent selects the workspace, so the current-group effect
-  // already expanded it; clicking the header would now collapse (the twist
-  // stays live since intent stopped forcing expansion).
-  await within(tree).findByText('4 sessions')
-
-  const initialLabel = 'Fixture 历史会话'
-  const initialRowLabel = await screen.findByText(initialLabel)
-  const initialRow = initialRowLabel.closest<HTMLElement>('[role="treeitem"]')
-  if (initialRow === null) throw new Error('fixture session row missing')
-  fireEvent.click(initialRow)
-  await waitFor(() => { expect(document.title).toBe(`${initialLabel} — DeepSeek Harness`) })
-  const initial = titleSurfaces(initialLabel)
-
-  const revisedLabel = 'Fixture 修订标题'
-  const timing = (globalThis as Record<string, unknown>).__fxTiming as FixtureTiming
-  act(() => { timing.appendTitle('fx-alpha', revisedLabel) })
-  await waitFor(() => { expect(document.title).toBe(`${revisedLabel} — DeepSeek Harness`) })
-  const revised = titleSurfaces(revisedLabel)
-
-  // fx-alpha carries the fixture's resident answerable approval, so the
-  // approval panel has taken over the composer (the real takeover behavior);
-  // answer it to restore the composer chrome before asserting the model seat.
-  fireEvent.click(await screen.findByRole('button', { name: '允许一次' }))
-  const modelTrigger = await screen.findByRole('button', {
-    name: '选择模型,当前 DeepSeek-V4-Flash,推理等级 High',
-  })
-  fireEvent.click(modelTrigger)
-  fireEvent.click(screen.getByRole('menuitem', { name: /Model/ }))
-  fireEvent.click(screen.getByRole('menuitemradio', { name: /GPT-5/ }))
-  await waitFor(() => {
-    expect(modelTrigger.getAttribute('aria-label')).toBe('选择模型,当前 GPT-5,推理等级 Medium')
-  })
-  fireEvent.click(modelTrigger)
-  fireEvent.click(screen.getByRole('menuitem', { name: /Effort/ }))
-  fireEvent.click(screen.getByRole('menuitemradio', { name: 'Max' }))
-  await waitFor(() => {
-    expect(modelTrigger.getAttribute('aria-label')).toBe('选择模型,当前 GPT-5,推理等级 Max')
-  })
-
-  // fx-alpha starts in the running state. Selecting above is intentionally
-  // allowed for the next turn; stop the fixture's resident run before sending
-  // the route-report prompt.
-  fireEvent.click(screen.getByRole('button', { name: 'Stop generating' }))
-  const composer = await screen.findByPlaceholderText('给智能体发消息')
-  fireEvent.change(composer, { target: { value: 'report model' } })
-  fireEvent.keyDown(composer, { key: 'Enter' })
-  await screen.findByText('当前模型:openai/gpt-5 · 推理等级:max', {}, { timeout: 10_000 })
-
-  await expect(`${JSON.stringify({ initial, revised }, null, 2)}\n`)
-    .toMatchFileSnapshot('./snapshots/session-title.json')
-})

+ 0 - 195
apps/web/tests/slash-flow.snapshot.ts

@@ -1,195 +0,0 @@
-// @vitest-environment jsdom
-// Assembled keyless snapshot of the slash/input/session convergence under the
-// agent-parity model: the New Session view state locks the composer until a
-// Workspace is picked (connectWorkspace materializes the full Session+Agent),
-// the '/' menu serves the session's wire command catalog (sessions are always
-// agent-backed — no draft/materialized split), a leadingInput command claims,
-// submits over the wire, and notices its result, and the SAME composer
-// textarea then carries the first plain send, whose ACCEPTANCE (not attempt)
-// flips blank and surfaces the session in lists. This is the user-visible
-// acceptance anchor — package mocks do not substitute for the assembled
-// application transcript.
-import { readFileSync } from 'node:fs'
-import { join } from 'node:path'
-import { act, cleanup, fireEvent, screen, waitFor, within } from '@testing-library/react'
-import { afterEach, beforeEach, expect, it, vi } from 'vitest'
-import type { WebBootEntry } from '@deepseek-ai/dsh-client-modules/client'
-import { AppWebEntry } from '@deepseek-ai/dsh-client-web'
-
-const PLUGINS: readonly (WebBootEntry & { dir: string })[] = [
-  { id: '@deepseek-ai/dsh-client-connection', dir: 'connection', url: '/plugins/connection.js', rev: 'fx', inject: [], immediately: true },
-  { id: '@deepseek-ai/dsh-client-runtime', dir: 'runtime', url: '/plugins/runtime.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-connection'], immediately: true },
-  { id: '@deepseek-ai/dsh-client-ui-theme', dir: 'ui-theme', url: '/plugins/ui-theme.js', rev: 'fx', inject: [], immediately: true },
-  { id: '@deepseek-ai/dsh-client-locale', dir: 'locale', url: '/plugins/locale.js', rev: 'fx', inject: [], immediately: true },
-  { id: '@deepseek-ai/dsh-client-ui-layout', dir: 'ui-layout', url: '/plugins/ui-layout.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime'] },
-  { id: '@deepseek-ai/dsh-client-ui-sidebar', dir: 'ui-sidebar', url: '/plugins/ui-sidebar.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
-  { id: '@deepseek-ai/dsh-client-ui-slash', dir: 'ui-slash', url: '/plugins/ui-slash.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime'] },
-  { id: '@deepseek-ai/dsh-client-ui-conversation', dir: 'ui-conversation', url: '/plugins/ui-conversation.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout', '@deepseek-ai/dsh-client-ui-slash'] },
-  { id: '@deepseek-ai/dsh-client-ui-command', dir: 'ui-command', url: '/plugins/ui-command.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-slash', '@deepseek-ai/dsh-client-ui-conversation'] },
-  { id: '@deepseek-ai/dsh-client-ui-skill', dir: 'ui-skill', url: '/plugins/ui-skill.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-slash'] },
-  { id: '@deepseek-ai/dsh-client-ui-subagent', dir: 'ui-subagent', url: '/plugins/ui-subagent.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-slash'] },
-  {
-    id: '@deepseek-ai/dsh-client-ui-workspace',
-    dir: 'ui-workspace',
-    url: '/plugins/ui-workspace.js',
-    rev: 'fx',
-    inject: [
-      '@deepseek-ai/dsh-client-runtime',
-      '@deepseek-ai/dsh-client-ui-conversation',
-      '@deepseek-ai/dsh-client-ui-sidebar',
-    ],
-  },
-]
-
-const bundles = new Map(PLUGINS.map(plugin => [
-  plugin.url,
-  readFileSync(join(process.cwd(), 'packages/client', plugin.dir, 'lib/client.js'), 'utf8'),
-]))
-
-interface FixtureWindow extends Window {
-  __DSH_BOOT__?: { rev: string; entries: WebBootEntry[] }
-  __ModuleLoader__?: unknown
-}
-
-class ResizeObserverStub {
-  observe(): void {}
-  disconnect(): void {}
-  unobserve(): void {}
-}
-
-// jsdom has no scrollIntoView; the slash menu follows its highlighted option.
-const scrollIntoView = vi.fn()
-const win = window as FixtureWindow
-let unmount: (() => void) | undefined
-
-beforeEach(() => {
-  localStorage.clear()
-  document.title = 'DeepSeek Harness'
-  Element.prototype.scrollIntoView = scrollIntoView
-  scrollIntoView.mockClear()
-  vi.stubGlobal('ResizeObserver', ResizeObserverStub)
-  vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
-    setTimeout(() => { callback(0) }, 0) as unknown as number)
-  vi.stubGlobal('cancelAnimationFrame', (id: number) => { clearTimeout(id) })
-})
-
-afterEach(() => {
-  act(() => { unmount?.() })
-  unmount = undefined
-  cleanup()
-  delete win.__DSH_BOOT__
-  delete win.__ModuleLoader__
-  delete (globalThis as Record<string, unknown>).__fxTiming
-  document.body.innerHTML = ''
-  document.head.querySelectorAll('style[data-plugin]').forEach((style) => { style.remove() })
-  document.title = ''
-  history.replaceState(null, '', '/')
-  Reflect.deleteProperty(Element.prototype, 'scrollIntoView')
-  vi.unstubAllGlobals()
-})
-
-/** Boot the complete built client graph against one keyless fixture branch. */
-function boot(search: string): void {
-  history.replaceState(null, '', `/${search}`)
-  const root = document.createElement('div')
-  root.id = 'root'
-  document.body.appendChild(root)
-  win.__DSH_BOOT__ = { rev: 'fx', entries: PLUGINS.map(({ dir: _dir, ...plugin }) => plugin) }
-  act(() => {
-    const entry = new AppWebEntry(root, {
-      fetchBundle: (url) => {
-        const code = bundles.get(url)
-        return code === undefined ? Promise.reject(new Error(`missing built bundle ${url}`)) : Promise.resolve(code)
-      },
-      executeBundle: (code) => { (0, eval)(code) },
-    })
-    void entry.run()
-    unmount = () => { entry.dispose() }
-  })
-}
-
-/** Collapse decorative whitespace while preserving the text a user sees. */
-function visibleText(element: Element): string {
-  return (element.textContent ?? '').replace(/\s+/g, ' ').trim()
-}
-
-/** Type into the machine-driven composer and let the change echo back. */
-async function typeComposer(composer: HTMLTextAreaElement, value: string): Promise<void> {
-  fireEvent.change(composer, { target: { value } })
-  await waitFor(() => { expect(composer.value).toBe(value) })
-}
-
-it('locked view state, connectWorkspace unlock, /echo claim chain, and blank-on-acceptance ride one resident composer', async () => {
-  boot('?fixture=empty')
-
-  // View state: no session entity — the composer renders locked; only the
-  // workspace picker is live.
-  const locked = await screen.findByPlaceholderText<HTMLTextAreaElement>(
-    'Choose a workspace to start', {}, { timeout: 10_000 },
-  )
-  expect(locked.disabled).toBe(true)
-
-  // Pick (create) a Workspace: connectWorkspace materializes the full
-  // Session+Agent and the provider swaps in the live blank-session hero.
-  fireEvent.click(screen.getAllByRole('button', { name: 'Choose workspace' })
-    .find(el => el.getAttribute('aria-haspopup') === 'menu')!)
-  fireEvent.click(await screen.findByRole('menuitem', { name: 'Create a new workspace' }))
-  const dialog = await screen.findByRole('dialog', { name: 'Create a new workspace' })
-  fireEvent.change(within(dialog).getByRole('textbox', { name: 'New workspace name' }), {
-    target: { value: 'nova' },
-  })
-  fireEvent.click(within(dialog).getByRole('button', { name: 'Create workspace' }))
-
-  const composer = await screen.findByPlaceholderText<HTMLTextAreaElement>(
-    'Describe what you want to build', {}, { timeout: 10_000 },
-  )
-  expect(composer.disabled).toBe(false)
-
-  // '/' opens the menu with the session's wire command catalog (the session
-  // is agent-backed from birth — the catalog is the single-address list).
-  await typeComposer(composer, '/')
-  const menu = await screen.findByRole('listbox', { name: 'Trigger suggestions' })
-  await waitFor(() => { expect(visibleText(menu)).toContain('echo') })
-  const menuText = visibleText(menu)
-
-  // Pick /echo (leadingInput): the claim token lands in the same textarea.
-  fireEvent.mouseDown(screen.getByRole('option', { name: /echo/ }))
-  await waitFor(() => { expect(composer.value).toBe('/echo ') })
-
-  // Type args and submit: the claim executes over the wire and notices its
-  // result; the token is consumed and the draft returns to plain text.
-  await typeComposer(composer, '/echo hello parser')
-  fireEvent.keyDown(composer, { key: 'Enter' })
-  await screen.findByText('hello parser', {}, { timeout: 10_000 })
-  await waitFor(() => { expect(composer.value).toBe('') })
-
-  // Slash execution does not flip blank: the selected row remains New Session.
-  const tree = screen.getByRole('tree', { name: 'Sessions' })
-  expect(within(tree).getByText('1 session')).toBeDefined()
-  expect(within(tree).getByText('New Session')).toBeDefined()
-
-  // First plain send through the SAME textarea: acceptance logs the user
-  // message and converts the existing sidebar row out of blank.
-  const before = composer
-  await typeComposer(composer, 'build me a parser')
-  fireEvent.keyDown(composer, { key: 'Enter' })
-  await waitFor(() => {
-    expect(screen.queryByText("Let's start building")).toBeNull()
-  }, { timeout: 10_000 })
-  await waitFor(() => { expect(within(tree).getByText('1 session')).toBeDefined() }, { timeout: 10_000 })
-  const after = document.querySelector('textarea')
-
-  expect({
-    menuHadEcho: menuText.includes('echo'),
-    menuHadCompact: menuText.includes('compact'),
-    composerSurvivedConversion: after === before,
-    sessionListed: visibleText(within(tree).getByText('1 session').closest('[role="treeitem"]')!),
-  }).toMatchInlineSnapshot(`
-    {
-      "composerSurvivedConversion": true,
-      "menuHadCompact": true,
-      "menuHadEcho": true,
-      "sessionListed": "nova1 session",
-    }
-  `)
-})

+ 0 - 14
apps/web/tests/snapshots/session-actions/rename-rows.json

@@ -1,14 +0,0 @@
-[
-  {
-    "label": "fixture4 sessions"
-  },
-  {
-    "label": "New Sessionnow"
-  },
-  {
-    "label": "分叉 实验记录now"
-  },
-  {
-    "label": "fixture2min"
-  }
-]

+ 0 - 12
apps/web/tests/snapshots/session-title.json

@@ -1,12 +0,0 @@
-{
-  "initial": {
-    "sidebar": "Fixture 历史会话",
-    "breadcrumb": "Fixture 历史会话",
-    "documentTitle": "Fixture 历史会话 — DeepSeek Harness"
-  },
-  "revised": {
-    "sidebar": "Fixture 修订标题",
-    "breadcrumb": "Fixture 修订标题",
-    "documentTitle": "Fixture 修订标题 — DeepSeek Harness"
-  }
-}

+ 0 - 306
apps/web/tests/terminal-card.snapshot.ts

@@ -1,306 +0,0 @@
-// @vitest-environment jsdom
-// Terminal card snapshot over the BUILT client graph (the code-mode-fixture
-// idiom: real bundles via AppWebEntry, keyless FixtureApiClient transport).
-// Opens the fixture history session and pins the `card: 'terminal'` render
-// intent at both of its conversation render sites, for both chat-row shapes:
-// turn 60's `fx-bash` on the render-site fallback row (expand-gated body) and
-// turn 65's `bash` on the keyed BashRow registration (resident body). Turn 65
-// carries what turn 60's two clean prompt rows cannot — SGR runs resolved to
-// --dsw-* tokens, output past the chat cap, a nested cwd, and a non-zero exit
-// pill; turn 60 carries the multi-line command's per-line prompt rows.
-//
-// The details panel's Output section is NOT covered here: tool rows stopped
-// being details-panel click targets, and nothing else in the assembled
-// application opens that panel, so the surface cannot be driven end to end.
-// Its terminal rendering stays pinned in ui-conversation's
-// tests/terminal-card.spec.tsx, which mounts DetailsPanel with a selection
-// directly.
-import { readFileSync } from 'node:fs'
-import { join } from 'node:path'
-import { act, cleanup, fireEvent, screen, waitFor, within } from '@testing-library/react'
-import { afterEach, beforeEach, expect, it, vi } from 'vitest'
-import type { WebBootEntry } from '@deepseek-ai/dsh-client-modules/client'
-import { AppWebEntry } from '@deepseek-ai/dsh-client-web'
-
-const PLUGINS: readonly (WebBootEntry & { dir: string })[] = [
-  { id: '@deepseek-ai/dsh-client-connection', dir: 'connection', url: '/plugins/connection.js', rev: 'fx', inject: [], immediately: true },
-  { id: '@deepseek-ai/dsh-client-runtime', dir: 'runtime', url: '/plugins/runtime.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-connection'], immediately: true },
-  { id: '@deepseek-ai/dsh-client-ui-theme', dir: 'ui-theme', url: '/plugins/ui-theme.js', rev: 'fx', inject: [], immediately: true },
-  { id: '@deepseek-ai/dsh-client-locale', dir: 'locale', url: '/plugins/locale.js', rev: 'fx', inject: [], immediately: true },
-  { id: '@deepseek-ai/dsh-client-ui-layout', dir: 'ui-layout', url: '/plugins/ui-layout.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime'] },
-  { id: '@deepseek-ai/dsh-client-ui-sidebar', dir: 'ui-sidebar', url: '/plugins/ui-sidebar.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
-  { id: '@deepseek-ai/dsh-client-ui-conversation', dir: 'ui-conversation', url: '/plugins/ui-conversation.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
-  {
-    id: '@deepseek-ai/dsh-client-ui-workspace',
-    dir: 'ui-workspace',
-    url: '/plugins/ui-workspace.js',
-    rev: 'fx',
-    inject: [
-      '@deepseek-ai/dsh-client-runtime',
-      '@deepseek-ai/dsh-client-ui-conversation',
-      '@deepseek-ai/dsh-client-ui-sidebar',
-    ],
-  },
-]
-
-const bundles = new Map(PLUGINS.map(plugin => [
-  plugin.url,
-  readFileSync(join(process.cwd(), 'packages/client', plugin.dir, 'lib/client.js'), 'utf8'),
-]))
-
-interface FixtureWindow extends Window {
-  __DSH_BOOT__?: { rev: string; entries: WebBootEntry[] }
-  __ModuleLoader__?: unknown
-}
-
-class ResizeObserverStub {
-  observe(): void {}
-  disconnect(): void {}
-  unobserve(): void {}
-}
-
-const win = window as FixtureWindow
-let unmount: (() => void) | undefined
-
-beforeEach(() => {
-  localStorage.clear()
-  document.title = 'DeepSeek Harness'
-  vi.stubGlobal('ResizeObserver', ResizeObserverStub)
-  vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
-    setTimeout(() => { callback(0) }, 0) as unknown as number)
-  vi.stubGlobal('cancelAnimationFrame', (id: number) => { clearTimeout(id) })
-})
-
-afterEach(() => {
-  act(() => { unmount?.() })
-  unmount = undefined
-  cleanup()
-  delete win.__DSH_BOOT__
-  delete win.__ModuleLoader__
-  document.body.innerHTML = ''
-  document.head.querySelectorAll('style[data-plugin]').forEach((style) => { style.remove() })
-  document.title = ''
-  history.replaceState(null, '', '/')
-  vi.unstubAllGlobals()
-})
-
-/** Boot the complete built client graph against the populated fixture branch. */
-function boot(): void {
-  history.replaceState(null, '', '/?fixture')
-  const root = document.createElement('div')
-  root.id = 'root'
-  document.body.appendChild(root)
-  win.__DSH_BOOT__ = { rev: 'fx', entries: PLUGINS.map(({ dir: _dir, ...plugin }) => plugin) }
-  act(() => {
-    const entry = new AppWebEntry(root, {
-      fetchBundle: (url) => {
-        const code = bundles.get(url)
-        return code === undefined ? Promise.reject(new Error(`missing built bundle ${url}`)) : Promise.resolve(code)
-      },
-      executeBundle: (code) => { (0, eval)(code) },
-    })
-    void entry.run()
-    unmount = () => { entry.dispose() }
-  })
-}
-
-/** Collapse decorative whitespace while preserving the text a user sees. */
-function visibleText(element: Element): string {
-  return (element.textContent ?? '').replace(/\s+/g, ' ').trim()
-}
-
-/**
- * Read one terminal card's user-visible state. Output lines keep their interior
- * whitespace: holding column alignment is what this card exists for, so
- * collapsing runs of spaces would hide the behavior under test.
- */
-function readCard(card: Element) {
-  const status = card.querySelector('[class*="_status_"]')
-  const expander = card.querySelector('button[aria-expanded]')
-  return {
-    // One entry per command line: a multi-line command is one row per line.
-    prompt: [...card.querySelectorAll('[class*="_promptLine_"]')].map(row =>
-      `${row.querySelector('[class*="_cwd_"]')?.textContent ?? ''} ${row.querySelector('[class*="_command_"]')?.textContent ?? ''}`),
-    // Dots per prompt row: exactly one, on the first row — the exit status the
-    // view carries is the whole call's, so a dot per line would assert a
-    // per-line outcome bash does not report.
-    dotsPerPromptRow: [...card.querySelectorAll('[class*="_promptLine_"]')].map(row =>
-      row.querySelectorAll('[data-state]').length),
-    status: status === null ? null : status.textContent,
-    copy: card.querySelector('[class*="_copyButton_"]')?.textContent ?? null,
-    lines: [...card.querySelectorAll('[class*="_line_"]')].map(line => line.textContent),
-    expander: expander === null ? null : {
-      label: expander.getAttribute('aria-label'),
-      text: expander.textContent,
-      expanded: expander.getAttribute('aria-expanded'),
-    },
-    // The run-state dot at the head of the prompt line, by its StateDot state.
-    runState: card.querySelector('[class*="_runState_"][data-state]')?.getAttribute('data-state') ?? null,
-    runStateLabel: card.querySelector('[class*="_runStateLabel_"]')?.textContent ?? null,
-    // Every color the ANSI parser emits resolves through a --dsw-* token, so
-    // the card follows the theme instead of painting literal terminal rgb.
-    // Scoped to the output lines: the run-state dot is an inline-styled span
-    // too, and its geometry is not an ANSI-resolved color.
-    colors: [...new Set([...card.querySelectorAll('[class*="_line_"] span[style]')]
-      .map(span => span.getAttribute('style')))],
-  }
-}
-
-/** Open the fixture history session (the alpha log carrying both bash turns) and wait for its tail. */
-async function openFixtureSession(): Promise<void> {
-  const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
-  // Anchor on the expandable Workspace group row: the title and the blank
-  // session row can both read "fixture".
-  const group = (await within(tree).findAllByText('fixture'))
-    .map(el => el.closest<HTMLElement>('[role="treeitem"]'))
-    .find(el => el?.getAttribute('aria-expanded') !== null)
-  if (group === null || group === undefined) throw new Error('fixture Workspace group missing')
-  if (group.getAttribute('aria-expanded') === 'false') {
-    fireEvent.click(within(group).getByText('fixture'))
-    await waitFor(() => {
-      expect(group.getAttribute('aria-expanded')).toBe('true')
-    })
-  }
-  fireEvent.click(await within(tree).findByText('Fixture 历史会话'))
-  await waitFor(() => {
-    expect(document.querySelector('[data-sample="bash-global"]')).not.toBeNull()
-  }, { timeout: 10_000 })
-}
-
-/** The keyed BashRow of fixture turn 65 (the one carrying the ANSI sample). */
-function keyedBashRow(): Element {
-  // Anchored on the BashRow wrapper (summary row + resident card), not on the
-  // summary row itself: the summary now shows the presenter's description (the
-  // contract's above-card text), so the command lives only in the card below it.
-  const row = [...document.querySelectorAll('[data-sample="bash-global"]')]
-    .map(node => node.parentElement)
-    .find((node): node is HTMLElement => node !== null && visibleText(node).includes('pnpm run check'))
-  if (row === undefined) throw new Error('keyed bash row for turn 65 missing')
-  return row
-}
-
-/** The turn-60 fallback row, which reaches the terminal card through GenericToolCard/ToolRow. */
-function fallbackBashRow(): Element {
-  const row = document.querySelector('[data-tool="fx-bash"]')
-  if (row === null) throw new Error('fx-bash fallback row missing')
-  return row
-}
-
-it('renders the keyed bash row with a resident terminal card', async () => {
-  boot()
-  await openFixtureSession()
-
-  const row = keyedBashRow()
-  const card = row.parentElement?.querySelector('[data-terminal]')
-  if (card === null || card === undefined) throw new Error('keyed bash row has no resident terminal card')
-  // The prompt shortens the nested cwd to its last segment, the exit pill comes
-  // from the sample's authored exit status (its body deliberately carries no
-  // `[exit code: N]` marker, since the real presenter consumes that one), ANSI
-  // runs land on theme tokens, and the chat cap (8) collapses the middle into a
-  // head/tail split with an expander between them.
-  expect(readCard(card)).toMatchInlineSnapshot(`
-    {
-      "colors": [
-        "font-weight: 700;",
-        "color: var(--dsw-alias-state-success-primary);",
-        "color: var(--dsw-alias-state-error-primary);",
-      ],
-      "copy": "复制",
-      "dotsPerPromptRow": [
-        1,
-      ],
-      "expander": {
-        "expanded": "false",
-        "label": "展开其余 13 行输出",
-        "text": "… 其余 13 行",
-      },
-      "lines": [
-        "Running 4 checks",
-        "✓ typecheck                                          1.82s",
-        "✓ lint                                               0.94s",
-        "✓ duplication                                        2.10s",
-        "StateDot.tsx                100%     100%        100%         -",
-        "markdown/Markdown.tsx       100%     100%        100%         -",
-        "",
-        "1 of 4 checks failed",
-      ],
-      "prompt": [
-        "nested pnpm run check",
-      ],
-      "runState": "error",
-      "runStateLabel": "失败",
-      "status": "退出码 1",
-    }
-  `)
-})
-
-it('the fallback row reaches the same card through its expand control', async () => {
-  boot()
-  await openFixtureSession()
-
-  const row = fallbackBashRow()
-  expect(row.querySelector('[data-terminal]')).toBeNull()
-  const toggle = row.querySelector('button[aria-expanded]')
-  if (toggle === null) throw new Error('fallback row expand control missing')
-  fireEvent.click(toggle)
-  const card = await waitFor(() => {
-    const found = row.querySelector('[data-terminal]')
-    if (found === null) throw new Error('terminal card missing after expanding the fallback row')
-    return found
-  })
-  // Three plain lines under the cap: no ANSI spans, no exit pill, no expander.
-  expect(readCard(card)).toMatchInlineSnapshot(`
-    {
-      "colors": [],
-      "copy": "复制",
-      "dotsPerPromptRow": [
-        1,
-        0,
-      ],
-      "expander": null,
-      "lines": [
-        "total 2",
-        "drwxr-xr-x fixture",
-        "-rw-r--r-- demo.txt",
-      ],
-      "prompt": [
-        "fixture ls -la",
-        "$ echo done",
-      ],
-      "runState": "done",
-      "runStateLabel": "已完成",
-      "status": null,
-    }
-  `)
-})
-
-it('the chat card expands the collapsed middle in place, without opening the details panel', async () => {
-  boot()
-  await openFixtureSession()
-
-  const card = keyedBashRow().parentElement?.querySelector('[data-terminal]')
-  if (card === null || card === undefined) throw new Error('resident terminal card missing')
-  const expander = card.querySelector('button[aria-expanded]')
-  if (expander === null) throw new Error('height-cap expander missing')
-  const capped = card.querySelectorAll('[class*="_line_"]').length
-
-  fireEvent.click(expander)
-  await waitFor(() => {
-    expect(card.querySelector('button[aria-expanded]')?.getAttribute('aria-expanded')).toBe('true')
-  })
-  expect({
-    cappedLines: capped,
-    expandedLines: card.querySelectorAll('[class*="_line_"]').length,
-    expanderLabel: card.querySelector('button[aria-expanded]')?.getAttribute('aria-label'),
-    // The card sits outside the summary row's click target, so toggling it
-    // left the details panel shut.
-    detailsOpen: screen.queryByText('Input') !== null,
-  }).toMatchInlineSnapshot(`
-    {
-      "cappedLines": 8,
-      "detailsOpen": false,
-      "expandedLines": 21,
-      "expanderLabel": "收起输出",
-    }
-  `)
-})

+ 0 - 213
apps/web/tests/todo-display.snapshot.ts

@@ -1,213 +0,0 @@
-// @vitest-environment jsdom
-// Todo display snapshot over the BUILT client graph (the code-mode-fixture
-// idiom: real bundles via AppWebEntry, keyless FixtureApiClient transport).
-// Opens the fixture history session and pins the todo_write turn's two
-// surfaces: the dedicated TodoRow in the chat flow (keyed toolview, summary
-// derived from the call args) and the TodoPanel plan strip riding the
-// 'conversation.input.dock' slot (fed by the host `todos` projection via
-// useProjection, seeded by the tail history page), including the collapse
-// interaction and the next-turn clearance of the standing plan.
-import { readFileSync } from 'node:fs'
-import { join } from 'node:path'
-import { act, cleanup, fireEvent, screen, waitFor, within } from '@testing-library/react'
-import { afterEach, beforeEach, expect, it, vi } from 'vitest'
-import type { WebBootEntry } from '@deepseek-ai/dsh-client-modules/client'
-import { AppWebEntry } from '@deepseek-ai/dsh-client-web'
-
-const PLUGINS: readonly (WebBootEntry & { dir: string })[] = [
-  { id: '@deepseek-ai/dsh-client-connection', dir: 'connection', url: '/plugins/connection.js', rev: 'fx', inject: [], immediately: true },
-  { id: '@deepseek-ai/dsh-client-runtime', dir: 'runtime', url: '/plugins/runtime.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-connection'], immediately: true },
-  { id: '@deepseek-ai/dsh-client-ui-theme', dir: 'ui-theme', url: '/plugins/ui-theme.js', rev: 'fx', inject: [], immediately: true },
-  { id: '@deepseek-ai/dsh-client-locale', dir: 'locale', url: '/plugins/locale.js', rev: 'fx', inject: [], immediately: true },
-  { id: '@deepseek-ai/dsh-client-ui-layout', dir: 'ui-layout', url: '/plugins/ui-layout.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime'] },
-  { id: '@deepseek-ai/dsh-client-ui-sidebar', dir: 'ui-sidebar', url: '/plugins/ui-sidebar.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
-  { id: '@deepseek-ai/dsh-client-ui-conversation', dir: 'ui-conversation', url: '/plugins/ui-conversation.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
-  {
-    id: '@deepseek-ai/dsh-client-ui-workspace',
-    dir: 'ui-workspace',
-    url: '/plugins/ui-workspace.js',
-    rev: 'fx',
-    inject: [
-      '@deepseek-ai/dsh-client-runtime',
-      '@deepseek-ai/dsh-client-ui-conversation',
-      '@deepseek-ai/dsh-client-ui-sidebar',
-    ],
-  },
-]
-
-const bundles = new Map(PLUGINS.map(plugin => [
-  plugin.url,
-  readFileSync(join(process.cwd(), 'packages/client', plugin.dir, 'lib/client.js'), 'utf8'),
-]))
-
-interface FixtureWindow extends Window {
-  __DSH_BOOT__?: { rev: string; entries: WebBootEntry[] }
-  __ModuleLoader__?: unknown
-}
-
-class ResizeObserverStub {
-  observe(): void {}
-  disconnect(): void {}
-  unobserve(): void {}
-}
-
-const win = window as FixtureWindow
-let unmount: (() => void) | undefined
-
-beforeEach(() => {
-  localStorage.clear()
-  document.title = 'DeepSeek Harness'
-  vi.stubGlobal('ResizeObserver', ResizeObserverStub)
-  vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
-    setTimeout(() => { callback(0) }, 0) as unknown as number)
-  vi.stubGlobal('cancelAnimationFrame', (id: number) => { clearTimeout(id) })
-})
-
-afterEach(() => {
-  act(() => { unmount?.() })
-  unmount = undefined
-  cleanup()
-  delete win.__DSH_BOOT__
-  delete win.__ModuleLoader__
-  delete (globalThis as Record<string, unknown>).__fxTiming
-  document.body.innerHTML = ''
-  document.head.querySelectorAll('style[data-plugin]').forEach((style) => { style.remove() })
-  document.title = ''
-  history.replaceState(null, '', '/')
-  vi.unstubAllGlobals()
-})
-
-/** Boot the complete built client graph against the populated fixture branch. */
-function boot(): void {
-  history.replaceState(null, '', '/?fixture')
-  const root = document.createElement('div')
-  root.id = 'root'
-  document.body.appendChild(root)
-  win.__DSH_BOOT__ = { rev: 'fx', entries: PLUGINS.map(({ dir: _dir, ...plugin }) => plugin) }
-  act(() => {
-    const entry = new AppWebEntry(root, {
-      fetchBundle: (url) => {
-        const code = bundles.get(url)
-        return code === undefined ? Promise.reject(new Error(`missing built bundle ${url}`)) : Promise.resolve(code)
-      },
-      executeBundle: (code) => { (0, eval)(code) },
-    })
-    void entry.run()
-    unmount = () => { entry.dispose() }
-  })
-}
-
-/** Collapse decorative whitespace while preserving the text a user sees. */
-function visibleText(element: Element): string {
-  return (element.textContent ?? '').replace(/\s+/g, ' ').trim()
-}
-
-/** Open the fixture history session (the alpha log carrying the todo_write turn) and wait for its tail. */
-async function openFixtureSession(): Promise<void> {
-  const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
-  // Anchor on the expandable Workspace group row: the title and the blank
-  // session row can both read "fixture", and the session-count meta shifts
-  // when a blank session joins the group.
-  const group = (await within(tree).findAllByText('fixture'))
-    .map(el => el.closest<HTMLElement>('[role="treeitem"]'))
-    .find(el => el?.getAttribute('aria-expanded') !== null)
-  if (group === null || group === undefined) throw new Error('fixture Workspace group missing')
-  if (group.getAttribute('aria-expanded') === 'false') {
-    fireEvent.click(within(group).getByText('fixture'))
-    await waitFor(() => {
-      expect(group.getAttribute('aria-expanded')).toBe('true')
-    })
-  }
-  const session = await within(tree).findByText('Fixture 历史会话')
-  fireEvent.click(session)
-  await waitFor(() => {
-    expect(document.querySelector('[data-sample="todo-row"]')).not.toBeNull()
-  }, { timeout: 10_000 })
-}
-
-it('renders the todo_write turn: dedicated tool row + the dock plan strip', async () => {
-  boot()
-  await openFixtureSession()
-
-  const row = document.querySelector('[data-sample="todo-row"]')
-  if (row === null) throw new Error('todo row missing')
-  const panel = document.querySelector('[data-testid="todo-panel"]')
-  if (panel === null) throw new Error('todo panel missing from the input dock')
-
-  // Header spans are adjacent inline nodes; textContent joins "To-dos" +
-  // "1/3…" with no space (visual gap is CSS gap: 10px, not a text node).
-  expect({
-    row: visibleText(row),
-    rowState: row.getAttribute('data-state'),
-    panelHeader: visibleText(panel.querySelector('button') ?? panel),
-    panelItems: [...panel.querySelectorAll('li')].map(item => ({
-      status: item.getAttribute('data-status'),
-      text: visibleText(item),
-    })),
-  }).toMatchInlineSnapshot(`
-    {
-      "panelHeader": "To-dos1/3 tasks · 1 in progress",
-      "panelItems": [],
-      "row": "更新任务清单1/3 已完成 · 实现 fixture 样本",
-      "rowState": "ok",
-    }
-  `)
-})
-
-it('expands the default-collapsed plan strip and restores its folded state', async () => {
-  boot()
-  await openFixtureSession()
-
-  const panel = document.querySelector('[data-testid="todo-panel"]')
-  if (panel === null) throw new Error('todo panel missing from the input dock')
-  const header = panel.querySelector('button')
-  if (header === null) throw new Error('todo panel header missing')
-
-  expect({
-    collapsedHeader: visibleText(header),
-    expanded: header.getAttribute('aria-expanded'),
-    listGone: panel.querySelector('ul') === null,
-  }).toMatchInlineSnapshot(`
-    {
-      "collapsedHeader": "To-dos1/3 tasks · 1 in progress",
-      "expanded": "false",
-      "listGone": true,
-    }
-  `)
-
-  fireEvent.click(header)
-  expect(panel.querySelectorAll('li')).toHaveLength(3)
-  expect(header.getAttribute('aria-expanded')).toBe('true')
-
-  fireEvent.click(header)
-  expect(panel.querySelector('ul')).toBeNull()
-  expect(header.getAttribute('aria-expanded')).toBe('false')
-})
-
-it('hides the plan strip when the next turn starts', async () => {
-  boot()
-  await openFixtureSession()
-  expect(document.querySelector('[data-testid="todo-panel"]')).not.toBeNull()
-
-  const composer = await screen.findByPlaceholderText('给智能体发消息', {}, { timeout: 10_000 })
-  fireEvent.change(composer, { target: { value: '下一轮清空计划' } })
-  fireEvent.keyDown(composer, { key: 'Enter' })
-
-  await screen.findByText('下一轮清空计划', { exact: true }, { timeout: 10_000 })
-  await waitFor(() => {
-    expect(document.querySelector('[data-testid="todo-panel"]')).toBeNull()
-  }, { timeout: 10_000 })
-
-  expect({
-    promptVisible: screen.getByText('下一轮清空计划', { exact: true }).textContent,
-    panelGone: document.querySelector('[data-testid="todo-panel"]') === null,
-    // Historical todo_write row stays in the flow; only the dock strip clears.
-    rowStillPresent: document.querySelector('[data-sample="todo-row"]') !== null,
-  }).toMatchInlineSnapshot(`
-    {
-      "panelGone": true,
-      "promptVisible": "下一轮清空计划",
-      "rowStillPresent": true,
-    }
-  `)
-})

+ 0 - 397
apps/web/tests/workspace-flow.snapshot.ts

@@ -1,397 +0,0 @@
-// @vitest-environment jsdom
-// Assembled keyless snapshots of the New Session flow under the agent-parity
-// model: startup auto-connects the recent Workspace's blank session when one
-// exists; without any Workspace the composer is locked in the pure view
-// state until one is chosen. Picking one materializes the full Session+Agent
-// (reuse-or-create of the workspace's blank session), the first ACCEPTED
-// prompt flips blank and surfaces the session in lists, and failures leave
-// no client-side transaction state: a failed attach keeps the view state
-// locked, a rejected prompt keeps the session blank with the draft restored.
-import { readFileSync } from 'node:fs'
-import { join } from 'node:path'
-import { act, cleanup, fireEvent, screen, waitFor, within } from '@testing-library/react'
-import { afterEach, beforeEach, expect, it, vi } from 'vitest'
-import type { WebBootEntry } from '@deepseek-ai/dsh-client-modules/client'
-import { AppWebEntry } from '@deepseek-ai/dsh-client-web'
-
-const PLUGINS: readonly (WebBootEntry & { dir: string })[] = [
-  { id: '@deepseek-ai/dsh-client-connection', dir: 'connection', url: '/plugins/connection.js', rev: 'fx', inject: [], immediately: true },
-  { id: '@deepseek-ai/dsh-client-runtime', dir: 'runtime', url: '/plugins/runtime.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-connection'], immediately: true },
-  { id: '@deepseek-ai/dsh-client-ui-theme', dir: 'ui-theme', url: '/plugins/ui-theme.js', rev: 'fx', inject: [], immediately: true },
-  { id: '@deepseek-ai/dsh-client-locale', dir: 'locale', url: '/plugins/locale.js', rev: 'fx', inject: [], immediately: true },
-  { id: '@deepseek-ai/dsh-client-ui-layout', dir: 'ui-layout', url: '/plugins/ui-layout.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-runtime'] },
-  { id: '@deepseek-ai/dsh-client-ui-sidebar', dir: 'ui-sidebar', url: '/plugins/ui-sidebar.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
-  { id: '@deepseek-ai/dsh-client-ui-settings', dir: 'ui-settings', url: '/plugins/ui-settings.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-sidebar'] },
-  { id: '@deepseek-ai/dsh-client-ui-settings-general', dir: 'ui-settings-general', url: '/plugins/ui-settings-general.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-settings', '@deepseek-ai/dsh-client-locale'] },
-  { id: '@deepseek-ai/dsh-client-ui-models', dir: 'ui-models', url: '/plugins/ui-models.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-settings'] },
-  { id: '@deepseek-ai/dsh-client-ui-conversation', dir: 'ui-conversation', url: '/plugins/ui-conversation.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
-  {
-    id: '@deepseek-ai/dsh-client-ui-workspace',
-    dir: 'ui-workspace',
-    url: '/plugins/ui-workspace.js',
-    rev: 'fx',
-    inject: [
-      '@deepseek-ai/dsh-client-runtime',
-      '@deepseek-ai/dsh-client-ui-conversation',
-      '@deepseek-ai/dsh-client-ui-sidebar',
-    ],
-  },
-  { id: '@deepseek-ai/dsh-client-ui-trajectory', dir: 'ui-trajectory', url: '/plugins/ui-trajectory.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-conversation'] },
-  // Dual-face host package: its browser half fills the directory-flow holes
-  // (the same composition row apps/cli mounts for the node-side backend).
-  {
-    id: '@deepseek-ai/dsh-host-directory-picker-browse',
-    dir: '../host/directory-picker-browse',
-    url: '/plugins/directory-picker-browse.js',
-    rev: 'fx',
-    inject: ['@deepseek-ai/dsh-client-runtime', '@deepseek-ai/dsh-client-ui-workspace', '@deepseek-ai/dsh-client-locale'],
-  },
-]
-
-const bundles = new Map(PLUGINS.map(plugin => [
-  plugin.url,
-  readFileSync(join(process.cwd(), 'packages/client', plugin.dir, 'lib/client.js'), 'utf8'),
-]))
-
-interface FixtureWindow extends Window {
-  __DSH_BOOT__?: { rev: string; entries: WebBootEntry[] }
-  __ModuleLoader__?: unknown
-}
-
-class ResizeObserverStub {
-  observe(): void {}
-  disconnect(): void {}
-  unobserve(): void {}
-}
-
-const win = window as FixtureWindow
-let unmount: (() => void) | undefined
-
-beforeEach(() => {
-  localStorage.clear()
-  document.title = 'DeepSeek Harness'
-  vi.stubGlobal('ResizeObserver', ResizeObserverStub)
-  vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) =>
-    setTimeout(() => { callback(0) }, 0) as unknown as number)
-  vi.stubGlobal('cancelAnimationFrame', (id: number) => { clearTimeout(id) })
-})
-
-afterEach(() => {
-  act(() => { unmount?.() })
-  unmount = undefined
-  cleanup()
-  delete win.__DSH_BOOT__
-  delete win.__ModuleLoader__
-  delete (globalThis as Record<string, unknown>).__fxTiming
-  document.body.innerHTML = ''
-  document.head.querySelectorAll('style[data-plugin]').forEach((style) => { style.remove() })
-  document.title = ''
-  history.replaceState(null, '', '/')
-  vi.unstubAllGlobals()
-})
-
-/** Boot the complete built client graph against one keyless fixture branch. */
-function boot(search: string): void {
-  history.replaceState(null, '', `/${search}`)
-  const root = document.createElement('div')
-  root.id = 'root'
-  document.body.appendChild(root)
-  win.__DSH_BOOT__ = { rev: 'fx', entries: PLUGINS.map(({ dir: _dir, ...plugin }) => plugin) }
-  act(() => {
-    const entry = new AppWebEntry(root, {
-      fetchBundle: (url) => {
-        const code = bundles.get(url)
-        return code === undefined ? Promise.reject(new Error(`missing built bundle ${url}`)) : Promise.resolve(code)
-      },
-      executeBundle: (code) => { (0, eval)(code) },
-    })
-    void entry.run()
-    unmount = () => { entry.dispose() }
-  })
-}
-
-/** Collapse decorative whitespace while preserving the text a user sees. */
-function visibleText(element: Element): string {
-  return (element.textContent ?? '').replace(/\s+/g, ' ').trim()
-}
-
-/** Identify the interactive Workspace chip (view state or blank-session hero) by its menu contract. */
-function workspaceChip(): HTMLElement {
-  const chip = screen.getAllByRole('button', { name: 'Choose workspace' })
-    .find(element => element.getAttribute('aria-haspopup') === 'menu')
-  if (chip === undefined) throw new Error('Workspace chip missing')
-  return chip
-}
-
-/** The locked view-state composer (no session yet). */
-async function findLockedComposer(): Promise<HTMLTextAreaElement> {
-  return await screen.findByPlaceholderText(
-    'Choose a workspace to start', {}, { timeout: 10_000 },
-  )
-}
-
-/** The live blank-session hero composer (session materialized). */
-async function findHeroComposer(): Promise<HTMLTextAreaElement> {
-  return await screen.findByPlaceholderText(
-    'Describe what you want to build', {}, { timeout: 10_000 },
-  )
-}
-
-/** Edit the machine-owned controlled input and assert the same-tick echo. */
-function setComposerText(composer: HTMLElement, value: string): void {
-  fireEvent.change(composer, { target: { value } })
-  expect((composer as HTMLTextAreaElement).value).toBe(value)
-}
-
-/** Drive the picker's create flow: chip → Create a new workspace → name dialog. */
-async function createWorkspaceViaPicker(name: string): Promise<void> {
-  fireEvent.click(workspaceChip())
-  fireEvent.click(await screen.findByRole('menuitem', { name: 'Create a new workspace' }))
-  const dialog = await screen.findByRole('dialog', { name: 'Create a new workspace' })
-  fireEvent.change(within(dialog).getByRole('textbox', { name: 'New workspace name' }), {
-    target: { value: name },
-  })
-  fireEvent.click(within(dialog).getByRole('button', { name: 'Create workspace' }))
-}
-
-/** Pick an existing Workspace row from the chip menu. */
-async function pickWorkspace(title: string): Promise<void> {
-  fireEvent.click(workspaceChip())
-  fireEvent.click(await screen.findByRole('menuitem', { name: title }))
-}
-
-it('locks the composer in the New Session view state until a Workspace is chosen', async () => {
-  boot('?fixture=empty')
-
-  const composer = await findLockedComposer()
-  const tree = screen.getByRole('tree', { name: 'Sessions' })
-
-  expect({
-    headline: visibleText(screen.getByText("Let's start building")),
-    chip: visibleText(workspaceChip()),
-    composerDisabled: composer.disabled,
-    sendDisabled: screen.getByRole<HTMLButtonElement>('button', { name: 'Send message' }).disabled,
-    sidebar: visibleText(tree),
-  }).toMatchInlineSnapshot(`
-    {
-      "chip": "Choose workspace",
-      "composerDisabled": true,
-      "headline": "Let's start building",
-      "sendDisabled": true,
-      "sidebar": "No sessions yet",
-    }
-  `)
-})
-
-it('adopts a directory through the composed in-app browse flow and lands in its blank session', async () => {
-  boot('?fixture=empty')
-
-  await findLockedComposer()
-  fireEvent.click(workspaceChip())
-  const menu = await screen.findByRole('menu')
-  // The composed flow package occupies the directory-flow hole, so the
-  // picking affordance is present (no advertised-kind read exists anymore).
-  expect(within(menu).getAllByRole('menuitem').map(item => visibleText(item)))
-    .toEqual(['Open local folder…', 'Create a new workspace'])
-  fireEvent.click(within(menu).getByRole('menuitem', { name: 'Open local folder…' }))
-  // The browse occupant renders the Select Workspace Directory dialog at the
-  // fixture home; select Documents, advance into project, and adopt it.
-  const dialog = await screen.findByRole('dialog', { name: '选择工作区目录' }, { timeout: 10_000 })
-  // Row targeting goes through the visible label text: listitem accessible-name
-  // computation differs across dom-accessibility-api environments, while the
-  // row's name span is stable (clicks bubble to the row button).
-  fireEvent.click(await within(dialog).findByText('Documents', {}, { timeout: 10_000 }))
-  fireEvent.click(await within(dialog).findByText('project', {}, { timeout: 10_000 }))
-  // Open disables while the selection's child listing is in flight; wait for
-  // the enabled state or the click lands on a dead button on slow runners.
-  await waitFor(() => {
-    expect(within(dialog).getByRole<HTMLButtonElement>('button', { name: '打开' }).disabled).toBe(false)
-  }, { timeout: 10_000 })
-  fireEvent.click(within(dialog).getByRole('button', { name: '打开' }))
-  await findHeroComposer()
-  await waitFor(() => {
-    expect(visibleText(screen.getByRole('tree', { name: 'Sessions' }))).toContain('project')
-  })
-})
-
-it('selects the recent Workspace and opens its blank Session on first load', async () => {
-  boot('?fixture')
-
-  const composer = await findHeroComposer()
-  const tree = screen.getByRole('tree', { name: 'Sessions' })
-  await waitFor(() => { expect(within(tree).getByText('4 sessions')).toBeDefined() }, { timeout: 10_000 })
-
-  expect({
-    chip: visibleText(workspaceChip()),
-    composerDisabled: composer.disabled,
-    blankRow: within(tree).getByText('New Session').textContent,
-  }).toMatchInlineSnapshot(`
-    {
-      "blankRow": "New Session",
-      "chip": "fixture",
-      "composerDisabled": false,
-    }
-  `)
-})
-
-it('creating a Workspace materializes and lists its selected blank Session', async () => {
-  boot('?fixture=empty')
-
-  await findLockedComposer()
-  await createWorkspaceViaPicker('nova')
-
-  // The pick connected the workspace: full Session+Agent exists, composer live.
-  const composer = await findHeroComposer()
-  const tree = screen.getByRole('tree', { name: 'Sessions' })
-  await waitFor(() => { expect(within(tree).getByText('1 session')).toBeDefined() })
-  expect(within(tree).getByText('New Session')).toBeDefined()
-  const group = within(tree).getByText('1 session').closest('[role="treeitem"]')
-  if (group === null) throw new Error('created Workspace projection missing')
-
-  expect({
-    composerDisabled: composer.disabled,
-    chip: visibleText(workspaceChip()),
-    workspace: visibleText(group),
-  }).toMatchInlineSnapshot(`
-    {
-      "chip": "nova",
-      "composerDisabled": false,
-      "workspace": "nova1 session",
-    }
-  `)
-})
-
-it('New Session reuses the Workspace blank session and converts the single visible row', async () => {
-  boot('?fixture=empty')
-
-  await findLockedComposer()
-  await createWorkspaceViaPicker('nova')
-  await findHeroComposer()
-  const tree = screen.getByRole('tree', { name: 'Sessions' })
-  await waitFor(() => { expect(within(tree).getByText('1 session')).toBeDefined() }, { timeout: 10_000 })
-
-  // New Session resolves through the recent Workspace and reuses its blank
-  // session in place: no locked interlude, no second entity.
-  const newSessionButton = screen.getAllByRole('button', { name: 'New session' })
-    .find(button => visibleText(button) === 'New Session')
-  if (newSessionButton === undefined) throw new Error('New Session button missing')
-  fireEvent.click(newSessionButton)
-  const composer = await findHeroComposer()
-  await waitFor(() => { expect(within(tree).getByText('1 session')).toBeDefined() }, { timeout: 10_000 })
-
-  setComposerText(composer, 'first light')
-  fireEvent.keyDown(composer, { key: 'Enter' })
-
-  // Conversion: the accepted prompt flips blank without adding a second row.
-  await screen.findByText('first light', { exact: true }, { timeout: 10_000 })
-  await waitFor(() => { expect(within(tree).getByText('1 session')).toBeDefined() }, { timeout: 10_000 })
-  const group = within(tree).getByText('1 session').closest('[role="treeitem"]')
-  if (group === null) throw new Error('converted Session projection missing')
-
-  expect({
-    workspace: visibleText(group),
-    promptVisible: screen.getByText('first light', { exact: true }).textContent,
-  }).toMatchInlineSnapshot(`
-    {
-      "promptVisible": "first light",
-      "workspace": "nova1 session",
-    }
-  `)
-})
-
-it('a failed Workspace attach recovers by reusing the published blank session', async () => {
-  boot('?fixture&fixtureAttach=fail')
-
-  // The rejected startup connect surfaces the locked view state first: the
-  // failure leaves no client-side transaction state to unwind.
-  await findLockedComposer()
-
-  // The host published the session before rejecting attachment (blank, with
-  // the workspace cwd), so the next connect — retry or manual pick — reuses
-  // it instead of minting a duplicate, and the hero opens on it.
-  await pickWorkspace('fixture')
-  const composer = await findHeroComposer()
-  const tree = screen.getByRole('tree', { name: 'Sessions' })
-  const group = within(tree).getByText('3 sessions').closest('[role="treeitem"]')
-  if (group === null) throw new Error('fixture Workspace projection missing')
-
-  expect({
-    headline: visibleText(screen.getByText("Let's start building")),
-    composerDisabled: composer.disabled,
-    chip: visibleText(workspaceChip()),
-    workspace: visibleText(group),
-  }).toMatchInlineSnapshot(`
-    {
-      "chip": "fixture",
-      "composerDisabled": false,
-      "headline": "Let's start building",
-      "workspace": "fixture3 sessions",
-    }
-  `)
-})
-
-it('a rejected first prompt keeps the session blank and the draft in the machine', async () => {
-  boot('?fixture=empty&fixturePrompt=reject')
-
-  await findLockedComposer()
-  await createWorkspaceViaPicker('nova')
-  const composer = await findHeroComposer()
-
-  setComposerText(composer, 'do not lose this')
-  fireEvent.click(screen.getByRole('button', { name: 'Send message' }))
-
-  const alert = await screen.findByRole('alert', {}, { timeout: 10_000 })
-  // Failure restore rides the machine (no pendingPrompt transaction): the
-  // draft returns to the same resident textarea one render later. The
-  // attempt flips the composer out of the hero (engaging = retry chrome),
-  // but acceptance never happened: the session row stays New Session.
-  const retained = await screen.findByDisplayValue('do not lose this')
-  const tree = screen.getByRole('tree', { name: 'Sessions' })
-  const group = within(tree).getByText('1 session').closest('[role="treeitem"]')
-  if (group === null) throw new Error('rejected-send Workspace projection missing')
-
-  expect({
-    error: visibleText(alert),
-    prompt: (retained as HTMLTextAreaElement).value,
-    blankRow: within(tree).getByText('New Session').textContent,
-    workspace: visibleText(group),
-  }).toMatchInlineSnapshot(`
-    {
-      "blankRow": "New Session",
-      "error": "fixture: prompt rejected before acceptance (agent-busy)",
-      "prompt": "do not lose this",
-      "workspace": "nova1 session",
-    }
-  `)
-})
-
-it('switching Workspace before the first message carries the draft to the new blank session', async () => {
-  boot('?fixture')
-
-  const composer = await findHeroComposer()
-  setComposerText(composer, 'carry me')
-
-  // Switch = session switch: the new workspace's blank session takes over,
-  // the typed draft moves machine-to-machine, the old blank stays hidden.
-  await createWorkspaceViaPicker('nova')
-  await waitFor(() => { expect(visibleText(workspaceChip())).toBe('nova') }, { timeout: 10_000 })
-  const carried = await screen.findByDisplayValue('carry me')
-  const tree = screen.getByRole('tree', { name: 'Sessions' })
-  const fixtureGroup = within(tree).getByText('3 sessions').closest('[role="treeitem"]')
-  const novaGroup = within(tree).getByText('1 session').closest('[role="treeitem"]')
-  if (fixtureGroup === null || novaGroup === null) throw new Error('Workspace projections missing after switch')
-
-  expect({
-    chip: visibleText(workspaceChip()),
-    prompt: (carried as HTMLTextAreaElement).value,
-    fixtureWorkspace: visibleText(fixtureGroup),
-    novaWorkspace: visibleText(novaGroup),
-  }).toMatchInlineSnapshot(`
-    {
-      "chip": "nova",
-      "fixtureWorkspace": "fixture3 sessions",
-      "novaWorkspace": "nova1 session",
-      "prompt": "carry me",
-    }
-  `)
-})