fix(client,host): review round — hooks-compartment occupancy, StrictMode re-arm, internal flow module, honest swap comment
- Directory-flow occupancy moves onto the inject face's reserved hooks compartment: apply publishes a stable observable per surface and the renderer binds useDirectoryFlow — no hand-rolled component subscriptions (the client contract's channel for registrant-private reactive facts). - The native flow's alive guard re-arms in effect setup: StrictMode's development replay ran the cleanup once and every later outcome was discarded. - NativeDirectoryFlow moves to a package-internal module; ./client exports only the Loader surface, tests import the internal module directly. - The composition swap comment no longer advertises -browse as a complete swap before its dialog lands (stacked follow-up).
This commit is contained in:
@@ -88,18 +88,18 @@ describe('ui-workspace apply', () => {
|
||||
|
||||
const browser = (b.slots.entries('sidebar.workspaces')[0]!.inject as () => WorkspaceBrowserInjected)()
|
||||
const picker = (b.slots.entries('conversation.hero.workspace')[0]!.inject as () => WorkspacePickerInjected)()
|
||||
expect(browser.hasDirectoryFlow()).toBe(false)
|
||||
expect(picker.hasDirectoryFlow()).toBe(false)
|
||||
// A flow occupant flips exactly its own surface.
|
||||
expect(browser.hooks.directoryFlow.getSnapshot()).toBe(false)
|
||||
expect(picker.hooks.directoryFlow.getSnapshot()).toBe(false)
|
||||
// A flow occupant flips exactly its own surface, and the source notifies.
|
||||
const notified = vi.fn()
|
||||
const unsubscribe = browser.subscribeDirectoryFlow(notified)
|
||||
const unsubscribe = browser.hooks.directoryFlow.subscribe(notified)
|
||||
const dispose = b.slots.register({ name: 'sidebar.workspaces.directoryFlow' } as never, () => null)
|
||||
expect(browser.hasDirectoryFlow()).toBe(true)
|
||||
expect(picker.hasDirectoryFlow()).toBe(false)
|
||||
expect(browser.hooks.directoryFlow.getSnapshot()).toBe(true)
|
||||
expect(picker.hooks.directoryFlow.getSnapshot()).toBe(false)
|
||||
await Promise.resolve()
|
||||
expect(notified).toHaveBeenCalled()
|
||||
dispose()
|
||||
expect(browser.hasDirectoryFlow()).toBe(false)
|
||||
expect(browser.hooks.directoryFlow.getSnapshot()).toBe(false)
|
||||
unsubscribe()
|
||||
})
|
||||
|
||||
|
||||
@@ -59,8 +59,7 @@ function mount(overrides: Partial<WorkspaceBrowserProps> = {}) {
|
||||
deleteWorkspace: vi.fn(async () => {}),
|
||||
insertSessionBefore: vi.fn(async () => {}),
|
||||
createWorkspace: vi.fn(async () => workspace('created', [])),
|
||||
hasDirectoryFlow: () => true,
|
||||
subscribeDirectoryFlow: () => () => {},
|
||||
useDirectoryFlow: bindSnapshotSelector({ getSnapshot: () => true, subscribe: () => () => {} }),
|
||||
renderSlot: ((_name: string, owner: { open: boolean }) => (owner.open ? <div data-testid="directory-flow" /> : null)) as never,
|
||||
...overrides,
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
} from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import { WorkspaceCreateError } from '@deepseek-ai/dsh-client-runtime/client'
|
||||
import type { DirectoryFlowOwnerProps } from '../src/client/contract/slots.ts'
|
||||
import { bindSnapshotSelector } from '@deepseek-ai/dsh-client-web-react'
|
||||
import { WorkspacePicker } from '../src/client/WorkspacePicker.tsx'
|
||||
|
||||
afterEach(cleanup)
|
||||
@@ -50,16 +51,19 @@ function flowProbe() {
|
||||
return { probe, renderSlot }
|
||||
}
|
||||
|
||||
/** Manual occupancy source: flip() drives the uSES subscription like a real registration change. */
|
||||
/** Manual occupancy source bound like the renderer would: flip() drives the hook like a real registration change. */
|
||||
function occupancySource(initial = true) {
|
||||
let occupied = initial
|
||||
const listeners = new Set<() => void>()
|
||||
return {
|
||||
hasDirectoryFlow: () => occupied,
|
||||
subscribeDirectoryFlow: (listener: () => void) => {
|
||||
const useDirectoryFlow = bindSnapshotSelector({
|
||||
getSnapshot: () => occupied,
|
||||
subscribe: (listener: () => void) => {
|
||||
listeners.add(listener)
|
||||
return () => { listeners.delete(listener) }
|
||||
},
|
||||
})
|
||||
return {
|
||||
useDirectoryFlow,
|
||||
flip: (next: boolean) => {
|
||||
occupied = next
|
||||
for (const listener of [...listeners]) listener()
|
||||
@@ -85,8 +89,7 @@ function mount(
|
||||
onPick={onPick}
|
||||
onClose={onClose}
|
||||
createWorkspace={createWorkspace}
|
||||
hasDirectoryFlow={occupancy.hasDirectoryFlow}
|
||||
subscribeDirectoryFlow={occupancy.subscribeDirectoryFlow}
|
||||
useDirectoryFlow={occupancy.useDirectoryFlow}
|
||||
renderSlot={renderSlot}
|
||||
/>
|
||||
)
|
||||
@@ -265,7 +268,7 @@ describe('WorkspacePicker', () => {
|
||||
<WorkspacePicker
|
||||
open useSessions={hook(sessions)} useWorkspaces={hook(workspaceState([]))}
|
||||
onPick={vi.fn()} onClose={vi.fn()} createWorkspace={vi.fn()}
|
||||
hasDirectoryFlow={() => true} subscribeDirectoryFlow={() => () => {}} renderSlot={renderSlot}
|
||||
useDirectoryFlow={occupancySource().useDirectoryFlow} renderSlot={renderSlot}
|
||||
/>,
|
||||
)
|
||||
expect(screen.queryByRole('menu')).toBeNull()
|
||||
@@ -280,7 +283,7 @@ describe('WorkspacePicker', () => {
|
||||
<WorkspacePicker
|
||||
open anchorRef={anchor()} useSessions={hook(sessions)} useWorkspaces={hook(state)}
|
||||
onPick={vi.fn()} onClose={vi.fn()} createWorkspace={vi.fn()}
|
||||
hasDirectoryFlow={() => true} subscribeDirectoryFlow={() => () => {}} renderSlot={renderSlot}
|
||||
useDirectoryFlow={occupancySource().useDirectoryFlow} renderSlot={renderSlot}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByRole('status').textContent).toBe('Loading workspaces…')
|
||||
|
||||
Reference in New Issue
Block a user