From 1a9c1596b8dd1ddaa0e0b199b77bc6f4c7c3bf88 Mon Sep 17 00:00:00 2001
From: imccyu <276526105+imccyu@users.noreply.github.com>
Date: Sat, 25 Jul 2026 16:38:32 +0800
Subject: [PATCH] test(web): cover workspace UI branches
---
.../client/connection/tests/fixture.spec.ts | 36 ++++
.../client/ui-sidebar/tests/rows.spec.tsx | 76 ++++++++
.../ui-sidebar/tests/sidebar-root.spec.tsx | 183 +++++++++++++++++-
packages/client/ui-sidebar/tests/tree.spec.ts | 86 +++++++-
.../tests/workspace-picker.spec.tsx | 46 ++++-
.../storage-domain/tests/domain.spec.ts | 21 +-
6 files changed, 440 insertions(+), 8 deletions(-)
create mode 100644 packages/client/ui-sidebar/tests/rows.spec.tsx
diff --git a/packages/client/connection/tests/fixture.spec.ts b/packages/client/connection/tests/fixture.spec.ts
index d860c69234..16fa4b4ed6 100644
--- a/packages/client/connection/tests/fixture.spec.ts
+++ b/packages/client/connection/tests/fixture.spec.ts
@@ -377,6 +377,42 @@ describe('createFixtureApi', () => {
})
})
+ it('attaches an existing ungrouped Session to a matching Workspace', async () => {
+ const api = createFixtureApi()
+ const sessionId = sid('fx-existing-ungrouped')
+ await expect(api.sessions.create(req({ sessionId, cwd: '/tmp/fixture' }))).resolves.toMatchObject({
+ result: { ok: true, value: { sessionId } },
+ })
+
+ await expect(api.sessions.create(req({
+ sessionId,
+ workspaceId: 'fx-ws-fixture' as WorkspaceId,
+ }))).resolves.toMatchObject({ result: { ok: true, value: { sessionId } } })
+
+ const workspaces = await api.workspace.list(req({}))
+ if (!workspaces.result.ok) throw new Error('workspace list failed')
+ expect(workspaces.result.value.items[0]?.sessionIds).toContain(sessionId)
+ })
+
+ it('reports a conflict without an existing cwd detail for an unrecorded cwd', async () => {
+ const api = createFixtureApi()
+ const listed = await api.sessions.list(req({}))
+ if (!listed.result.ok) throw new Error('session list failed')
+ const existing = listed.result.value.items.find(item => item.sessionId === sid('fx-alpha'))
+ if (existing === undefined) throw new Error('fixture Session missing')
+ delete existing.cwd
+
+ const conflict = await api.sessions.create(req({ sessionId: existing.sessionId }))
+ expect(conflict.result).toEqual({
+ ok: false,
+ error: {
+ code: 'session-conflict',
+ message: `session ${existing.sessionId} already uses no cwd`,
+ details: { sessionId: existing.sessionId, requestedCwd: '/tmp/fixture' },
+ },
+ })
+ })
+
it('publishes an ungrouped Session when Workspace attachment fails', async () => {
const api = createFixtureApi({ failWorkspaceAttach: true })
const sessionId = sid('fx-partial')
diff --git a/packages/client/ui-sidebar/tests/rows.spec.tsx b/packages/client/ui-sidebar/tests/rows.spec.tsx
new file mode 100644
index 0000000000..468ce550d9
--- /dev/null
+++ b/packages/client/ui-sidebar/tests/rows.spec.tsx
@@ -0,0 +1,76 @@
+// @vitest-environment jsdom
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { cleanup, fireEvent, render, screen } from '@testing-library/react'
+import type { SessionId, WorkspaceId } from '@deepseek-ai/dsh-client-runtime/client'
+import { IntentRowItem, ProjectRowItem, SessionNodeItem } from '../src/client/Rows.tsx'
+import type { GroupNode, SessionNode } from '../src/client/tree.ts'
+
+afterEach(cleanup)
+
+const sid = (id: string) => id as SessionId
+const wid = (id: string) => id as WorkspaceId
+
+describe('sidebar rows', () => {
+ it('renders an active Workspace and keeps its create action separate from toggling', () => {
+ const onToggle = vi.fn()
+ const onCreate = vi.fn()
+ const group: GroupNode = {
+ key: 'project', workspaceId: wid('project'), cwd: '/projects/project', label: 'Project',
+ sessionCount: 1, expanded: true, containsCurrent: true, intentHere: false, sessions: [],
+ }
+ render()
+
+ expect(screen.getByText('1 session')).toBeTruthy()
+ expect(screen.getByRole('treeitem').getAttribute('aria-expanded')).toBe('true')
+ fireEvent.click(screen.getByRole('button', { name: 'New session in Project' }))
+ expect(onCreate).toHaveBeenCalledOnce()
+ expect(onToggle).not.toHaveBeenCalled()
+ fireEvent.click(screen.getByText('Project'))
+ expect(onToggle).toHaveBeenCalledOnce()
+ })
+
+ it('renders the frontend Intent placeholder as selected', () => {
+ render()
+ expect(screen.getByRole('treeitem').getAttribute('aria-selected')).toBe('true')
+ })
+
+ it('renders and operates selected, running, recursive Session nodes', () => {
+ const child: SessionNode = {
+ id: sid('child'), title: 'Child', children: [], hasChildren: false,
+ expanded: false, running: false, updatedAt: 0,
+ }
+ const parent: SessionNode = {
+ id: sid('parent'), title: 'Parent', children: [child], hasChildren: true,
+ expanded: true, running: true, updatedAt: 0,
+ }
+ const onOpen = vi.fn()
+ const onToggle = vi.fn()
+ const view = render(
+ ,
+ )
+
+ const parentRow = screen.getByText('Parent').closest('[role="treeitem"]')!
+ const childRow = screen.getByText('Child').closest('[role="treeitem"]')!
+ expect(parentRow.getAttribute('aria-selected')).toBe('true')
+ expect(parentRow.getAttribute('aria-expanded')).toBe('true')
+ expect(childRow.getAttribute('aria-selected')).toBe('false')
+ expect(childRow.hasAttribute('aria-expanded')).toBe(false)
+
+ fireEvent.click(screen.getByRole('button', { name: 'Collapse' }))
+ expect(onToggle).toHaveBeenCalledWith(parent.id)
+ expect(onOpen).not.toHaveBeenCalled()
+ fireEvent.click(parentRow)
+ fireEvent.click(childRow)
+ expect(onOpen.mock.calls).toEqual([[parent.id], [child.id]])
+
+ view.rerender(
+ ,
+ )
+ expect(screen.getByRole('button', { name: 'Expand' })).toBeTruthy()
+ expect(screen.getByRole('treeitem').getAttribute('aria-selected')).toBe('false')
+ expect(screen.getByRole('treeitem').style.paddingLeft).toBe('24px')
+ })
+})
diff --git a/packages/client/ui-sidebar/tests/sidebar-root.spec.tsx b/packages/client/ui-sidebar/tests/sidebar-root.spec.tsx
index bae2ed69ee..26adafd793 100644
--- a/packages/client/ui-sidebar/tests/sidebar-root.spec.tsx
+++ b/packages/client/ui-sidebar/tests/sidebar-root.spec.tsx
@@ -1,13 +1,16 @@
// @vitest-environment jsdom
import { afterEach, describe, expect, it, vi } from 'vitest'
-import { cleanup, fireEvent, render, screen } from '@testing-library/react'
+import { act, cleanup, fireEvent, render, screen } from '@testing-library/react'
import type {
SessionId, SessionListState, WorkspaceId, WorkspaceListState, WorkspaceView,
} from '@deepseek-ai/dsh-client-runtime/client'
import type { SidebarRootComponentProps } from '../src/client/contract/slots.ts'
import { SidebarRoot } from '../src/client/SidebarRoot.tsx'
-afterEach(cleanup)
+afterEach(() => {
+ cleanup()
+ vi.useRealTimers()
+})
const sid = (id: string) => id as SessionId
const wid = (id: string) => id as WorkspaceId
const hook = (snapshot: T) => (selector: (state: T) => S): S => selector(snapshot)
@@ -41,6 +44,43 @@ function mount(sessionState: SessionListState = sessions) {
return { view, startSession, open, pickerOwner: () => pickerOwner }
}
+function mountSidebar({
+ sessionState = sessions,
+ workspaceState = workspaces,
+ collapsed = false,
+ width = 300,
+}: {
+ sessionState?: SessionListState
+ workspaceState?: WorkspaceListState
+ collapsed?: boolean
+ width?: number
+} = {}) {
+ const startSession = vi.fn()
+ const open = vi.fn()
+ const toggleSidebar = vi.fn()
+ let pickerOwner: unknown
+ let current = { sessionState, workspaceState, collapsed, width }
+ const root = () => (
+ { pickerOwner = owner; return null }) as SidebarRootComponentProps['renderSlot']}
+ />
+ )
+ const view = render(root())
+ return {
+ startSession,
+ open,
+ toggleSidebar,
+ pickerOwner: () => pickerOwner,
+ rerender(next: Partial) {
+ current = { ...current, ...next }
+ view.rerender(root())
+ },
+ }
+}
+
describe('SidebarRoot', () => {
it('renders real Workspaces from useWorkspaces and routes New Session', () => {
const b = mount()
@@ -79,4 +119,143 @@ describe('SidebarRoot', () => {
fireEvent.click(screen.getByText('First session'))
expect(b.open).toHaveBeenCalledWith(sid('s1'))
})
+
+ it('opens, selects, dismisses, and toggles the group-by menu', () => {
+ mount()
+ const button = screen.getByRole('button', { name: 'Group by' })
+
+ fireEvent.click(button)
+ fireEvent.click(screen.getByRole('menuitem', { name: 'Workspace' }))
+ expect(screen.queryByRole('menu')).toBeNull()
+
+ fireEvent.click(button)
+ fireEvent.keyDown(document, { key: 'Escape' })
+ expect(screen.queryByRole('menu')).toBeNull()
+
+ fireEvent.click(button)
+ fireEvent.click(button)
+ expect(screen.queryByRole('menu')).toBeNull()
+ })
+
+ it('routes every Workspace picker close path', () => {
+ const b = mount()
+ fireEvent.click(screen.getByRole('button', { name: 'Create workspace' }))
+ const owner = b.pickerOwner() as { open: boolean; onClose(): void }
+ expect(owner.open).toBe(true)
+ act(() => { owner.onClose() })
+ expect((b.pickerOwner() as { open: boolean }).open).toBe(false)
+ })
+
+ it('focuses, filters, and clears search while distinguishing both empty states', () => {
+ mount()
+ const input = screen.getByPlaceholderText('Search name, keywords...')
+ fireEvent.click(input.parentElement!)
+ expect(document.activeElement).toBe(input)
+ fireEvent.click(screen.getByRole('button', { name: 'Search sessions' }))
+
+ fireEvent.change(input, { target: { value: 'missing' } })
+ expect(screen.getByText('No matches')).toBeTruthy()
+ fireEvent.click(screen.getByRole('button', { name: 'Clear search' }))
+ expect(screen.queryByText('No matches')).toBeNull()
+
+ cleanup()
+ const emptySessions = listState()
+ const emptyWorkspaces: WorkspaceListState = { ...workspaces, items: [], recentWorkspaceId: undefined }
+ mountSidebar({ sessionState: emptySessions, workspaceState: emptyWorkspaces })
+ expect(screen.getByText('No sessions yet')).toBeTruthy()
+ })
+
+ it('toggles Workspace and nested Session expansion in both directions', () => {
+ const parent = sid('parent')
+ const child = sid('child')
+ const nestedSessions: SessionListState = {
+ ...sessions,
+ ids: [parent, child],
+ byId: {
+ [parent]: { id: parent, displayTitle: 'Parent', running: false, updatedAt: 2 },
+ [child]: { id: child, displayTitle: 'Child', running: false, updatedAt: 1, parentId: parent },
+ },
+ }
+ const nestedWorkspace: WorkspaceListState = {
+ ...workspaces,
+ items: [{ ...workspace, sessionIds: [parent, child] }],
+ }
+ mountSidebar({ sessionState: nestedSessions, workspaceState: nestedWorkspace })
+
+ fireEvent.click(screen.getByText('Project'))
+ fireEvent.click(screen.getByRole('button', { name: 'Expand' }))
+ expect(screen.getByText('Child')).toBeTruthy()
+ fireEvent.click(screen.getByRole('button', { name: 'Collapse' }))
+ expect(screen.queryByText('Child')).toBeNull()
+ fireEvent.click(screen.getByText('Project'))
+ expect(screen.queryByText('Parent')).toBeNull()
+ })
+
+ it('does not start a Session from an Ungrouped row create action', () => {
+ const loose = sid('loose')
+ const looseSessions: SessionListState = {
+ ...listState(),
+ ids: [loose],
+ byId: { [loose]: { id: loose, displayTitle: 'Loose', running: false, updatedAt: 1 } },
+ current: loose,
+ }
+ const b = mountSidebar({
+ sessionState: looseSessions,
+ workspaceState: { ...workspaces, items: [], recentWorkspaceId: undefined },
+ })
+ fireEvent.click(screen.getByRole('button', { name: 'New session in Ungrouped' }))
+ expect(b.startSession).not.toHaveBeenCalled()
+ })
+
+ it('keeps an already expanded selected Workspace open and resolves later Workspace matches', () => {
+ const b = mountSidebar()
+ fireEvent.click(screen.getByText('Project'))
+ const other = { ...workspace, workspaceId: wid('other'), title: 'Other', sessionIds: [] }
+ b.rerender({
+ sessionState: { ...sessions, current: sid('s1') },
+ workspaceState: { ...workspaces, items: [other, workspace] },
+ })
+ expect(screen.getByText('First session')).toBeTruthy()
+
+ b.rerender({
+ sessionState: {
+ ...sessions,
+ current: sid('draft'),
+ intent: { sessionId: sid('draft'), target: { kind: 'workspace-intent' }, prompt: '', phase: 'ready' },
+ },
+ })
+ expect(screen.getByText('Project')).toBeTruthy()
+ })
+
+ it('renders the static collapsed rail and expands rail search into focused input', () => {
+ vi.useFakeTimers()
+ const b = mountSidebar({ collapsed: true })
+ expect(screen.getByRole('button', { name: 'Open sidebar' })).toBeTruthy()
+ expect(screen.queryByPlaceholderText('Search name, keywords...')).toBeNull()
+ fireEvent.click(screen.getByRole('button', { name: 'Open sidebar' }))
+ expect(b.toggleSidebar).toHaveBeenCalledOnce()
+
+ fireEvent.click(screen.getByRole('button', { name: 'Search sessions' }))
+ expect(b.toggleSidebar).toHaveBeenCalledTimes(2)
+ b.rerender({ collapsed: false })
+ const input = screen.getByPlaceholderText('Search name, keywords...')
+ act(() => { vi.advanceTimersByTime(300) })
+ expect(document.activeElement).toBe(input)
+ })
+
+ it('keeps wide content during live collapse, then settles to the rail', () => {
+ vi.useFakeTimers()
+ const b = mountSidebar({ width: 320 })
+ fireEvent.click(screen.getByRole('button', { name: 'Collapse sidebar' }))
+ expect(b.toggleSidebar).toHaveBeenCalledOnce()
+ b.rerender({ collapsed: true, width: 56 })
+ expect(screen.getByPlaceholderText('Search name, keywords...')).toBeTruthy()
+ act(() => { vi.advanceTimersByTime(150) })
+ expect(screen.queryByPlaceholderText('Search name, keywords...')).toBeNull()
+ expect(screen.getByRole('button', { name: 'Open sidebar' })).toBeTruthy()
+ })
})
+
+function listState(): SessionListState {
+ return { ids: [], byId: {}, current: undefined, phase: 'ready', intent: undefined }
+}
diff --git a/packages/client/ui-sidebar/tests/tree.spec.ts b/packages/client/ui-sidebar/tests/tree.spec.ts
index 76d68db4ee..d114d43dea 100644
--- a/packages/client/ui-sidebar/tests/tree.spec.ts
+++ b/packages/client/ui-sidebar/tests/tree.spec.ts
@@ -2,7 +2,7 @@ import { describe, expect, it } from 'vitest'
import type {
SessionId, SessionListState, SessionSummary, WorkspaceId, WorkspaceView,
} from '@deepseek-ai/dsh-client-runtime/client'
-import { deriveGroups, formatRelativeTime, UNGROUPED_KEY } from '../src/client/tree.ts'
+import { deriveGroups, formatRelativeTime, projectLabel, UNGROUPED_KEY, UNGROUPED_LABEL } from '../src/client/tree.ts'
const sid = (id: string) => id as SessionId
const wid = (id: string) => id as WorkspaceId
@@ -59,6 +59,90 @@ describe('deriveGroups', () => {
expect(groups[0]!.intentHere).toBe(false)
expect(groups[0]!.sessionCount).toBe(2)
})
+
+ it('builds, sorts, expands, and cycle-guards an ungrouped session tree', () => {
+ const parent = summary('parent', 1)
+ const oldChild = { ...summary('old-child', 10), parentId: parent.id }
+ const newChild = { ...summary('new-child', 20), parentId: parent.id }
+ const tieB = { ...summary('tie-b', 20), parentId: parent.id }
+ const tieA = { ...summary('tie-a', 20), parentId: parent.id }
+ const self = { ...summary('self', 2), parentId: sid('self') }
+ const orphan = { ...summary('orphan', 3), parentId: sid('missing') }
+ const cycleA = { ...summary('cycle-a', 4), parentId: sid('cycle-b') }
+ const cycleB = { ...summary('cycle-b', 5), parentId: sid('cycle-a') }
+ const groups = deriveGroups(
+ list(parent, oldChild, newChild, tieB, tieA, self, orphan, cycleA, cycleB),
+ [],
+ { expandedProjects: [UNGROUPED_KEY], expandedSessions: [parent.id, cycleA.id, cycleB.id], query: '' },
+ )
+
+ expect(groups).toHaveLength(1)
+ expect(groups[0]!.sessions.map(node => node.id)).toEqual([
+ sid('orphan'), sid('self'), parent.id, sid('cycle-a'),
+ ])
+ expect(groups[0]!.sessions[2]!.children.map(node => node.id)).toEqual([
+ newChild.id, tieA.id, tieB.id, oldChild.id,
+ ])
+
+ // Equal timestamps use ids as a deterministic tiebreak in either input order.
+ expect(deriveGroups(list(summary('tie-a', 1), summary('tie-b', 1)), [], view([UNGROUPED_KEY]))[0]!
+ .sessions.map(node => node.id)).toEqual([sid('tie-a'), sid('tie-b')])
+ })
+
+ it('tolerates Workspace membership arriving before its Session summary', () => {
+ const partial: SessionListState = {
+ ...list(),
+ ids: [sid('present')],
+ byId: { [sid('present')]: summary('present', 1) },
+ }
+ const groups = deriveGroups(partial, [workspace('project', ['missing', 'present'])], view(['project']))
+ expect(groups[0]!.sessions.map(node => node.id)).toEqual([sid('present')])
+ })
+
+ it('searches descendants with ancestors and handles cycles, self parents, and label-only hits', () => {
+ const root = { ...summary('root', 1), displayTitle: 'Ancestor' }
+ const match = { ...summary('match', 2), displayTitle: 'Needle child', parentId: root.id }
+ const sibling = { ...summary('sibling', 3), displayTitle: 'Other child', parentId: root.id }
+ const self = { ...summary('self', 4), displayTitle: 'Needle self', parentId: sid('self') }
+ const orphan = { ...summary('orphan', 5), displayTitle: 'Needle orphan', parentId: sid('absent') }
+ const cycleA = { ...summary('cycle-a', 6), displayTitle: 'Needle cycle A', parentId: sid('cycle-b') }
+ const cycleB = { ...summary('cycle-b', 7), displayTitle: 'Needle cycle B', parentId: sid('cycle-a') }
+ const sessions = list(root, match, sibling, self, orphan, cycleA, cycleB)
+ const groups = deriveGroups(sessions, [workspace('project', sessions.ids)], view([], 'needle'))
+
+ expect(groups[0]!.sessions.flatMap(node => [node.id, ...node.children.map(child => child.id)])).toEqual([
+ root.id, match.id, self.id, orphan.id, cycleA.id, cycleB.id,
+ ])
+
+ const labelOnly = deriveGroups(
+ list(summary('hidden', 1)),
+ [workspace('label-hit', ['hidden']), workspace('other', [])],
+ view([], 'label'),
+ )
+ expect(labelOnly).toEqual([
+ expect.objectContaining({ key: 'label-hit', expanded: false, sessions: [], sessionCount: 1 }),
+ ])
+ })
+
+ it('marks selected Workspace and Ungrouped sessions without relying on an Intent', () => {
+ const owned = summary('owned', 1)
+ const loose = summary('loose', 2)
+ const ws = workspace('project', ['owned'])
+ const ownedGroups = deriveGroups({ ...list(owned, loose), current: owned.id }, [ws], view())
+ expect(ownedGroups.find(group => group.key === 'project')!.containsCurrent).toBe(true)
+ const looseGroups = deriveGroups({ ...list(owned, loose), current: loose.id }, [ws], view())
+ expect(looseGroups.find(group => group.key === UNGROUPED_KEY)!.containsCurrent).toBe(true)
+ })
+})
+
+describe('projectLabel', () => {
+ it('uses the Ungrouped fallback and extracts POSIX and Windows basenames', () => {
+ expect(projectLabel(undefined)).toBe(UNGROUPED_LABEL)
+ expect(projectLabel('')).toBe(UNGROUPED_LABEL)
+ expect(projectLabel('/projects/demo/')).toBe('demo')
+ expect(projectLabel('C:\\projects\\demo\\')).toBe('demo')
+ expect(projectLabel('/')).toBe('/')
+ })
})
describe('formatRelativeTime', () => {
diff --git a/packages/client/ui-workspace/tests/workspace-picker.spec.tsx b/packages/client/ui-workspace/tests/workspace-picker.spec.tsx
index 32d5be145b..523e869961 100644
--- a/packages/client/ui-workspace/tests/workspace-picker.spec.tsx
+++ b/packages/client/ui-workspace/tests/workspace-picker.spec.tsx
@@ -79,12 +79,23 @@ describe('WorkspacePicker', () => {
const createWorkspace = vi.fn(async () => created)
const b = mount([], createWorkspace)
chooseCreateItem('Use an existing folder')
- fireEvent.change(screen.getByLabelText('Existing folder path'), { target: { value: ' /tmp/project ' } })
- fireEvent.click(screen.getByRole('button', { name: 'Use folder' }))
+ const input = screen.getByLabelText('Existing folder path')
+ fireEvent.keyDown(input, { key: 'ArrowRight' })
+ fireEvent.keyDown(input, { key: 'Enter' })
+ expect(createWorkspace).not.toHaveBeenCalled()
+ fireEvent.change(input, { target: { value: ' /tmp/project ' } })
+ fireEvent.keyDown(input, { key: 'Enter' })
expect(createWorkspace).toHaveBeenCalledWith({ path: '/tmp/project' })
await waitFor(() => { expect(b.onPick).toHaveBeenCalledWith(created.workspaceId) })
})
+ it('closes a creation modal when the user cancels', () => {
+ mount([])
+ chooseCreateItem('Create a new workspace')
+ fireEvent.click(screen.getByRole('button', { name: 'Cancel' }))
+ expect(screen.queryByRole('dialog')).toBeNull()
+ })
+
it('blocks a create-new name already present in the Workspace list', () => {
const b = mount([workspace('alpha', 'Alpha')])
chooseCreateItem('Create a new workspace')
@@ -98,16 +109,43 @@ describe('WorkspacePicker', () => {
it('exposes creation phase and error text while retaining the modal for retry', async () => {
let reject!: (reason: unknown) => void
const pending = new Promise((_resolve, rejectPromise) => { reject = rejectPromise })
- const b = mount([], vi.fn(() => pending))
+ const createWorkspace = vi.fn(() => pending)
+ const b = mount([], createWorkspace)
chooseCreateItem('Create a new workspace')
- fireEvent.change(screen.getByLabelText('New workspace name'), { target: { value: 'broken' } })
+ const input = screen.getByLabelText('New workspace name')
+ fireEvent.keyDown(input, { key: 'ArrowRight' })
+ fireEvent.change(input, { target: { value: 'broken' } })
fireEvent.click(screen.getByRole('button', { name: 'Create workspace' }))
expect(screen.getByRole('status').textContent).toBe('Creating workspace…')
+ fireEvent.keyDown(input, { key: 'Enter' })
+ expect(createWorkspace).toHaveBeenCalledTimes(1)
+ fireEvent.keyDown(document, { key: 'Escape' })
+ expect(screen.getByRole('dialog')).toBeTruthy()
await act(async () => { reject(new Error('disk unavailable')); await pending.catch(() => {}) })
expect(screen.getByRole('alert').textContent).toBe('Workspace creation failed: disk unavailable')
expect(b.view.getByRole('dialog')).toBeTruthy()
})
+ it('reports non-Error creation failures', async () => {
+ const b = mount([], vi.fn(async () => { throw 'permission denied' }))
+ chooseCreateItem('Create a new workspace')
+ fireEvent.click(screen.getByRole('button', { name: 'Create workspace' }))
+ await waitFor(() => {
+ expect(screen.getByRole('alert').textContent).toBe('Workspace creation failed: permission denied')
+ })
+ expect(b.onPick).not.toHaveBeenCalled()
+ })
+
+ it('waits to show its menu until an optional anchor is available', () => {
+ render(
+ ,
+ )
+ expect(screen.queryByRole('menu')).toBeNull()
+ })
+
it('shows list loading through a stable status surface', () => {
const state: WorkspaceListState = {
...workspaceState([]), phase: 'pending', state: 'loading', baselinesReady: false,
diff --git a/packages/storage/storage-domain/tests/domain.spec.ts b/packages/storage/storage-domain/tests/domain.spec.ts
index 761c1d7c1d..4a083b3f78 100644
--- a/packages/storage/storage-domain/tests/domain.spec.ts
+++ b/packages/storage/storage-domain/tests/domain.spec.ts
@@ -2,7 +2,7 @@ import { describe, expect, it, vi } from 'vitest'
import { Context } from 'cordis'
import { z } from 'zod'
import Storage, { storageBackendServiceKey } from '@deepseek-ai/dsh-storage'
-import { DomainFacility, defineDomain, domainTable } from '../src/index.ts'
+import { apply, DomainFacility, defineDomain, domainTable } from '../src/index.ts'
import type { Config } from '../src/index.ts'
import type { DomainChanged } from '../src/events.ts'
import { MemoryMediaPool, MemoryStorageBackend } from './helpers/memory-backend.ts'
@@ -151,6 +151,25 @@ describe('DomainFacility.open', () => {
})
describe('plugin apply', () => {
+ it('uses only the default backend when routes are omitted', async () => {
+ const ctx = new Context()
+ await ctx.plugin(Storage)
+ const backend = new MemoryStorageBackend()
+ ctx.storage.backend.register('memory', backend)
+ const disposeBackend = ctx.provide(storageBackendServiceKey('memory'), backend)
+
+ const fiber = await ctx.plugin({
+ name: 'storage-domain-routeless-test',
+ inject: ['storage'],
+ apply: (domainCtx: Context) => apply(domainCtx, { backend: 'memory' }),
+ })
+ await vi.waitFor(() => { expect(ctx.storageDomain).toBeInstanceOf(DomainFacility) })
+
+ disposeBackend()
+ await vi.waitFor(() => { expect(ctx.get('storageDomain')).toBeUndefined() })
+ await fiber.dispose()
+ })
+
it('waits for routed backends, then mounts one lifecycle-bound service and form', async () => {
const ctx = new Context()
await ctx.plugin(Storage)