test(web): re-anchor snapshot suites to the startup-selection boot flow

The startup Workspace auto-selection (wired in cd8ed43e4) changed the
boot landing: with any Workspace present the client connects its blank
session directly instead of resting in the locked view state.

- workspace-flow: New Session now reuses the blank session in place
  (no locked interlude); the failed-attach scenario asserts the actual
  recovery semantics — the host publishes the session before rejecting
  attachment, so the next connect reuses it into the hero (connect
  failures log to console, there is no view-state alert surface); the
  rejected-prompt scenario anchors on the sidebar New Session row since
  a send attempt leaves the hero for the engaging retry chrome.
- slash-flow: drop the stale i18n PLUGINS row (the package is locale).
This commit is contained in:
imccyu
2026-07-27 07:48:07 +08:00
parent cbe8735d7c
commit 250e2415ae
2 changed files with 30 additions and 22 deletions
-1
View File
@@ -20,7 +20,6 @@ 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-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-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-ui-theme', dir: 'ui-theme', url: '/plugins/ui-theme.js', rev: 'fx', inject: [], immediately: true },
{ id: '@deepseek-ai/dsh-client-i18n', dir: 'i18n', url: '/plugins/i18n.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-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-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-sidebar', dir: 'ui-sidebar', url: '/plugins/ui-sidebar.js', rev: 'fx', inject: ['@deepseek-ai/dsh-client-ui-layout'] },
+30 -21
View File
@@ -1,11 +1,12 @@
// @vitest-environment jsdom // @vitest-environment jsdom
// Assembled keyless snapshots of the New Session flow under the agent-parity // Assembled keyless snapshots of the New Session flow under the agent-parity
// model: no session exists before a Workspace is chosen (the composer is // model: startup auto-connects the recent Workspace's blank session when one
// locked in the pure view state), picking one materializes the full // exists; without any Workspace the composer is locked in the pure view
// Session+Agent (reuse-or-create of the workspace's blank session), the // state until one is chosen. Picking one materializes the full Session+Agent
// first accepted prompt flips blank and surfaces the session in lists, and // (reuse-or-create of the workspace's blank session), the first ACCEPTED
// failures (attach rejection, prompt rejection) are ordinary error strips // prompt flips blank and surfaces the session in lists, and failures leave
// with no client-side transaction state. // 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 { readFileSync } from 'node:fs'
import { join } from 'node:path' import { join } from 'node:path'
import { act, cleanup, fireEvent, screen, waitFor, within } from '@testing-library/react' import { act, cleanup, fireEvent, screen, waitFor, within } from '@testing-library/react'
@@ -227,20 +228,20 @@ it('New Session reuses the Workspace blank session and converts the single visib
await findLockedComposer() await findLockedComposer()
await createWorkspaceViaPicker('nova') await createWorkspaceViaPicker('nova')
await findHeroComposer() await findHeroComposer()
const tree = screen.getByRole('tree', { name: 'Sessions' })
await waitFor(() => { expect(within(tree).getByText('1 session')).toBeDefined() }, { timeout: 10_000 })
// Back out to the view state and choose the same workspace again: the // New Session resolves through the recent Workspace and reuses its blank
// existing blank session is reused — no second entity. // session in place: no locked interlude, no second entity.
fireEvent.click(screen.getByRole('button', { name: 'New session' })) fireEvent.click(screen.getByRole('button', { name: 'New session' }))
await findLockedComposer()
await pickWorkspace('nova')
const composer = await findHeroComposer() const composer = await findHeroComposer()
await waitFor(() => { expect(within(tree).getByText('1 session')).toBeDefined() }, { timeout: 10_000 })
setComposerText(composer, 'first light') setComposerText(composer, 'first light')
fireEvent.keyDown(composer, { key: 'Enter' }) fireEvent.keyDown(composer, { key: 'Enter' })
// Conversion: the accepted prompt flips blank without adding a second row. // Conversion: the accepted prompt flips blank without adding a second row.
await screen.findByText('first light', { exact: true }, { timeout: 10_000 }) await screen.findByText('first light', { exact: true }, { timeout: 10_000 })
const tree = screen.getByRole('tree', { name: 'Sessions' })
await waitFor(() => { expect(within(tree).getByText('1 session')).toBeDefined() }, { 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"]') const group = within(tree).getByText('1 session').closest('[role="treeitem"]')
if (group === null) throw new Error('converted Session projection missing') if (group === null) throw new Error('converted Session projection missing')
@@ -256,26 +257,32 @@ it('New Session reuses the Workspace blank session and converts the single visib
`) `)
}) })
it('a failed Workspace attach surfaces in the view state and keeps the composer locked', async () => { it('a failed Workspace attach recovers by reusing the published blank session', async () => {
boot('?fixture&fixtureAttach=fail') 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() await findLockedComposer()
await pickWorkspace('fixture')
const alert = await screen.findByRole('alert', {}, { timeout: 10_000 }) // The host published the session before rejecting attachment (blank, with
const composer = await findLockedComposer() // 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 tree = screen.getByRole('tree', { name: 'Sessions' })
const group = within(tree).getByText('3 sessions').closest('[role="treeitem"]') const group = within(tree).getByText('3 sessions').closest('[role="treeitem"]')
if (group === null) throw new Error('fixture Workspace projection missing') if (group === null) throw new Error('fixture Workspace projection missing')
expect({ expect({
error: visibleText(alert), headline: visibleText(screen.getByText("Let's start building")),
composerDisabled: composer.disabled, composerDisabled: composer.disabled,
chip: visibleText(workspaceChip()),
workspace: visibleText(group), workspace: visibleText(group),
}).toMatchInlineSnapshot(` }).toMatchInlineSnapshot(`
{ {
"composerDisabled": true, "chip": "fixture",
"error": "session create failed: workspace-attach-failed: fixture rejected Workspace attachment for fx-1", "composerDisabled": false,
"headline": "Let's start building",
"workspace": "fixture3 sessions", "workspace": "fixture3 sessions",
} }
`) `)
@@ -293,7 +300,9 @@ it('a rejected first prompt keeps the session blank and the draft in the machine
const alert = await screen.findByRole('alert', {}, { timeout: 10_000 }) const alert = await screen.findByRole('alert', {}, { timeout: 10_000 })
// Failure restore rides the machine (no pendingPrompt transaction): the // Failure restore rides the machine (no pendingPrompt transaction): the
// draft returns to the same resident textarea one render later. // 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 retained = await screen.findByDisplayValue('do not lose this')
const tree = screen.getByRole('tree', { name: 'Sessions' }) const tree = screen.getByRole('tree', { name: 'Sessions' })
const group = within(tree).getByText('1 session').closest('[role="treeitem"]') const group = within(tree).getByText('1 session').closest('[role="treeitem"]')
@@ -302,13 +311,13 @@ it('a rejected first prompt keeps the session blank and the draft in the machine
expect({ expect({
error: visibleText(alert), error: visibleText(alert),
prompt: (retained as HTMLTextAreaElement).value, prompt: (retained as HTMLTextAreaElement).value,
stillHero: screen.getByText("Let's start building").textContent, blankRow: within(tree).getByText('New Session').textContent,
workspace: visibleText(group), workspace: visibleText(group),
}).toMatchInlineSnapshot(` }).toMatchInlineSnapshot(`
{ {
"blankRow": "New Session",
"error": "fixture: prompt rejected before acceptance (agent-busy)", "error": "fixture: prompt rejected before acceptance (agent-busy)",
"prompt": "do not lose this", "prompt": "do not lose this",
"stillHero": "Let's start building",
"workspace": "nova1 session", "workspace": "nova1 session",
} }
`) `)