test(client): pin assembled-app journeys at package level on SlotTestRuntime
Sink the behavior previously held only by the apps/web jsdom snapshots into the owning packages, each bench mounting the real apply on the production slot machinery with fixture-fed data: - ui-conversation/assembly-surfaces: the todo_write turn reaches both product surfaces (keyed toolview row + dock plan strip via the todos projection) and the strip follows projection retirement; the keyed bash row carries its resident terminal card while the fallback row reaches one through expand; the locked no-session view state; the composer textarea surviving the blank→active conversion as the same DOM node; the promptError alert strip with the machine-restored draft; one summary update re-labeling the breadcrumb. - ui-workspace/rename-assembly: the session-rename chain (row menu → dialog → the injected renameSession hop → ISession.rename with the edge-trimmed draft → dialog close and row re-label from the list), plus the rejected arm keeping the dialog open with the error. - runtime/workspaces-service: startInitialSelection — connects the recent Workspace once both baselines are ready and opens the session, stays idle with a current session or no recent target (double start fails loud), and a failed connect returns to waiting and retries on the next list change. Component-level arms stay in the existing package suites; these files prove only the assembled wiring.
This commit is contained in:
118
packages/client/ui-workspace/tests/rename-assembly.spec.tsx
Normal file
118
packages/client/ui-workspace/tests/rename-assembly.spec.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
// @vitest-environment jsdom
|
||||
/**
|
||||
* The session-rename assembly chain on SlotTestRuntime (real apply, real
|
||||
* WorkspaceBrowser occupying the sidebar hole): row menu → rename dialog →
|
||||
* the injected renameSession hop (sessions.binding → ISession.rename) → on
|
||||
* the accepted unary response the dialog closes and the row re-labels from
|
||||
* the list state — no push-frame wait. Previously pinned only by the
|
||||
* assembled-app snapshot (apps/web/tests/session-actions.snapshot.ts); the
|
||||
* verb's wire behavior stays with the runtime package
|
||||
* (session.spec.ts#rename), the dialog's own arms with rows.spec /
|
||||
* workspace-browser.spec.
|
||||
*/
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { cleanup, fireEvent, waitFor, within } from '@testing-library/react'
|
||||
import type { ISession, SessionId, WorkspaceId } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { PropsRenderSlots } from '@deepseek-ai/dsh-client-ui-slots'
|
||||
import { SlotTestRuntime } from '@deepseek-ai/dsh-client-test-runtime'
|
||||
import { apply, inject } from '@deepseek-ai/dsh-client-ui-workspace/client'
|
||||
|
||||
const SID = 's1' as SessionId
|
||||
|
||||
afterEach(cleanup)
|
||||
beforeEach(() => { localStorage.clear() })
|
||||
|
||||
/** Test-owned sidebar shell role: declares and renders the browsing region. */
|
||||
type FrameProps = PropsRenderSlots<'sidebar.workspaces'>
|
||||
function SidebarFrame({ renderSlot }: FrameProps) {
|
||||
return <>{renderSlot('sidebar.workspaces', { wide: true, expandSidebar: () => {} })}</>
|
||||
}
|
||||
|
||||
describe('session rename through the assembled browser', () => {
|
||||
it('renames via the row menu: binding.session.rename fires, the dialog closes, the row re-labels from the list', async () => {
|
||||
const runtime = await SlotTestRuntime.create()
|
||||
const rename = vi.fn<ISession['rename']>(async title => ({
|
||||
ok: true, value: { title: title.trim().replace(/\s+/g, ' '), seq: 7 },
|
||||
}))
|
||||
await runtime.sessions.add({
|
||||
id: SID,
|
||||
summary: { title: '旧标题', displayTitle: '旧标题', cwd: '/w/alpha' },
|
||||
session: { rename },
|
||||
})
|
||||
await runtime.workspaces.update((draft) => {
|
||||
draft.items = [{
|
||||
workspaceId: 'w1' as WorkspaceId, title: 'alpha', path: '/w/alpha',
|
||||
sessionIds: [SID], createdAt: '2026-01-01T00:00:00.000Z', updatedAt: '2026-01-01T00:00:00.000Z',
|
||||
}] as never
|
||||
})
|
||||
await runtime.root.declare(
|
||||
{ 'sidebar.workspaces': { kind: 'single', scope: 'root' } } as never,
|
||||
SidebarFrame as never,
|
||||
)
|
||||
await runtime.mount({ inject: [...inject], apply })
|
||||
const view = runtime.renderRoot()
|
||||
|
||||
// The current session's group auto-expands; open the row's action menu.
|
||||
const row = (await view.findByText('旧标题')).closest('[role="treeitem"]')!
|
||||
fireEvent.click(within(row as HTMLElement).getByLabelText('Session actions for 旧标题'))
|
||||
fireEvent.click(view.getByRole('menuitem', { name: 'Rename', hidden: true }))
|
||||
|
||||
// The dialog seeds from the current title; submit a padded value.
|
||||
const input = await view.findByLabelText('Session name') as HTMLInputElement
|
||||
expect(input.value).toBe('旧标题')
|
||||
fireEvent.change(input, { target: { value: ' 分叉 实验记录 ' } })
|
||||
fireEvent.click(view.getByRole('button', { name: 'Rename' }))
|
||||
|
||||
// The injected hop reached the session face with the edge-trimmed draft
|
||||
// (the dialog trims edges; interior normalization is host-side).
|
||||
await waitFor(() => { expect(rename).toHaveBeenCalledWith('分叉 实验记录') })
|
||||
// Acceptance closes the dialog without any push-frame wait.
|
||||
await waitFor(() => { expect(view.queryByLabelText('Session name')).toBeNull() })
|
||||
// The manager lands the unary echo in the list store (its own package
|
||||
// tests own that hop); the row re-labels from list state alone.
|
||||
await runtime.sessions.updateSummary(SID, { displayTitle: '分叉 实验记录', title: '分叉 实验记录' })
|
||||
await view.findByText('分叉 实验记录')
|
||||
expect(view.queryByText('旧标题')).toBeNull()
|
||||
await runtime.dispose()
|
||||
})
|
||||
|
||||
it('a rejected rename keeps the dialog open with the error surfaced', async () => {
|
||||
const runtime = await SlotTestRuntime.create()
|
||||
const rename = vi.fn<ISession['rename']>(async () => ({
|
||||
ok: false, error: { code: 'internal', message: 'title write failed', details: {} },
|
||||
}))
|
||||
await runtime.sessions.add({
|
||||
id: SID,
|
||||
summary: { title: '旧标题', displayTitle: '旧标题', cwd: '/w/alpha' },
|
||||
session: { rename },
|
||||
})
|
||||
await runtime.workspaces.update((draft) => {
|
||||
draft.items = [{
|
||||
workspaceId: 'w1' as WorkspaceId, title: 'alpha', path: '/w/alpha',
|
||||
sessionIds: [SID], createdAt: '2026-01-01T00:00:00.000Z', updatedAt: '2026-01-01T00:00:00.000Z',
|
||||
}] as never
|
||||
})
|
||||
await runtime.root.declare(
|
||||
{ 'sidebar.workspaces': { kind: 'single', scope: 'root' } } as never,
|
||||
SidebarFrame as never,
|
||||
)
|
||||
await runtime.mount({ inject: [...inject], apply })
|
||||
const view = runtime.renderRoot()
|
||||
await runtime.flush()
|
||||
|
||||
const row = (await view.findByText('旧标题')).closest('[role="treeitem"]')!
|
||||
fireEvent.click(within(row as HTMLElement).getByLabelText('Session actions for 旧标题'))
|
||||
fireEvent.click(view.getByRole('menuitem', { name: 'Rename', hidden: true }))
|
||||
const input = await view.findByLabelText('Session name')
|
||||
fireEvent.change(input, { target: { value: '新名' } })
|
||||
fireEvent.click(view.getByRole('button', { name: 'Rename' }))
|
||||
|
||||
// Failure: the injected hop rethrows the business error; the dialog
|
||||
// stays open with the alert and the row keeps its title.
|
||||
const alert = await view.findByRole('alert')
|
||||
expect(alert.textContent).toContain('title write failed')
|
||||
expect(view.getByLabelText('Session name')).toBeTruthy()
|
||||
expect(view.getByText('旧标题')).toBeTruthy()
|
||||
await runtime.dispose()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user