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)