// @vitest-environment jsdom // apply wiring: the conversation service provided, the chat view registered // as the first 'conversation.view' ring entry declaring the keyed toolview // hole, the three slot registrations land against a root entry's children // declarations (the AppFrame role), the shared store handle rides all session // entries, and the bash sample mounts through the load-order seam as a keyed // entry. Full-chain rendering belongs to the machinery spec // (chat-toolview-slot.spec.tsx) and the shell e2e; this spec stops at the // assembly surface. import { Context } from 'cordis' import { describe, expect, it, vi } from 'vitest' import { createSnapshotStore } from '@deepseek-ai/dsh-client-runtime/client' import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client' import type { SessionId, SessionListState } from '@deepseek-ai/dsh-client-runtime/client' import { apply, inject } from '@deepseek-ai/dsh-client-ui-conversation/client' const ROOT = 'root-1' as SessionId const CHILD = 'child-1' as SessionId async function bench() { const ctx = new Context() const slotsFiber = ctx.plugin(SlotsService) await slotsFiber.await() const listStore = createSnapshotStore({ ids: [ROOT, CHILD], byId: { [ROOT]: { id: ROOT, title: 'R', displayTitle: 'R', running: false, blank: false, updatedAt: 1 }, [CHILD]: { id: CHILD, title: 'C', displayTitle: 'C', parentId: ROOT, running: false, blank: false, updatedAt: 2 }, }, current: undefined, phase: 'ready', } as SessionListState) const sessionsFake = { list: listStore, binding: vi.fn(), scope: () => undefined, provideInfo: () => undefined, maybeProvideInfo: () => ({ hooks: {}, props: {} }), provide: vi.fn(() => () => {}), create: vi.fn(), open: vi.fn(), updateIntent: vi.fn(), } ctx.provide('sessions', sessionsFake) ctx.provide('workspaces', { startSession: vi.fn(), sendSession: vi.fn(), }) ctx.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() }) ctx.provide('locale', { bind: () => (key: string) => key }) // Declared by ui-layout's root entry in production; a stand-in root // occupant declares them here so the contributions land (it consumes // renderSlot to satisfy the declare-means-render check). const slots = ctx.get('slots') as SlotsService slots.register({ name: 'root', children: { 'conversation': { kind: 'single', scope: 'session-maybe' }, 'details': { kind: 'single', scope: 'session' }, }, }, (_p: { renderSlot?: unknown }) => null) const fiber = ctx.plugin({ inject: [...inject], apply }) return { ctx, fiber, slots } } /** First stored entry for a key (inject/store live directly on StoredEntry). */ function renderEntryOf(slots: SlotsService, key: 'conversation' | 'conversation.session' | 'conversation.view' | 'details') { return slots.entries(key)[0] as undefined | { inject?: unknown; store?: unknown } } describe('apply wiring', () => { it('provides the conversation service', async () => { const b = await bench() await b.fiber.await() expect(b.ctx.get('conversation')).toBeDefined() }) it('registers the chat view as the first ring entry, declaring the keyed toolview hole', async () => { const b = await bench() await b.fiber.await() const entries = b.slots.entries('conversation.view') expect(entries.map((e) => e.options.id)).toEqual(['chat']) expect(entries[0]?.options.label).toBe('Chat') expect(entries[0]?.options.order).toBe(0) // Declaring is claiming: the chat entry's registration put the hole on // the ledger with the contract's kind/scope. expect(b.slots.spec('conversation.chat.toolview')).toEqual({ kind: 'keyed', scope: 'session' }) }) it('occupies the slots + the ring; session entries share one store handle', async () => { const b = await bench() await b.fiber.await() const conversation = renderEntryOf(b.slots, 'conversation') const conversationSession = renderEntryOf(b.slots, 'conversation.session') const chatView = renderEntryOf(b.slots, 'conversation.view') const details = renderEntryOf(b.slots, 'details') expect(conversation?.inject).toBeTypeOf('function') expect(chatView?.inject).toBeTypeOf('function') expect(details?.inject).toBeTypeOf('function') // The shared handle: one apply-built store value on ALL session entries // (the session-maybe 'conversation' shell carries no store by design). expect(conversationSession?.store).toBeDefined() expect(details?.store).toBe(conversationSession?.store) expect(chatView?.store).toBe(conversationSession?.store) // The hero workspace picker hole rides the conversation entry's children // declaration (the empty-state occupant is gone). expect(b.slots.spec('conversation.hero.workspace')).toEqual({ kind: 'single', scope: 'root' }) }) it('mounts the bash sample and the todo row as keyed entries through the load-order seam', async () => { const b = await bench() await b.fiber.await() // Both registrant plugins' inject: ['slots', 'conversation'] resolved — the // service being present implies the chat entry declared the hole first. const entries = b.slots.entries('conversation.chat.toolview') expect(entries.map((e) => e.options.key)).toEqual(['bash', 'todo_write']) }) it('plugin fiber disposal collects every registration (unload cascade, ring and hole included)', async () => { const b = await bench() await b.fiber.await() await b.fiber.dispose() expect(b.slots.entries('conversation')).toHaveLength(0) // The declared ring collapses with its declaring entry, and the chat // entry's keyed hole (with the sample's registration) collapses with it. expect(b.slots.entries('conversation.view')).toHaveLength(0) expect(b.slots.entries('conversation.chat.toolview')).toHaveLength(0) expect(b.slots.spec('conversation.chat.toolview')).toBeUndefined() expect(b.slots.entries('details')).toHaveLength(0) expect(b.ctx.get('conversation')).toBeUndefined() }) })