# Conflicts: # .agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.i18n.yaml # .agents/notes/implemented/architecture/2026-07-30-client-locale-full-rollout.zh.md # .agents/notes/implemented/feature/2026-07-30-web-queue-steer-action.i18n.yaml # .agents/notes/implemented/feature/2026-07-30-web-queue-steer-action.zh.md # .agents/notes/implemented/feature/2026-07-31-browser-derived-initial-locale.i18n.yaml # .agents/notes/implemented/feature/2026-07-31-browser-derived-initial-locale.zh.md # .agents/notes/implemented/testing/2026-07-24-web-gui-browser-e2e-lane.i18n.yaml # apps/web/tests/scaffold.ts # docs/module-graph.md # packages/client/runtime/README.i18n.yaml # packages/client/runtime/package.json # packages/client/test-runtime/README.i18n.yaml # packages/client/test-runtime/README.zh.md # packages/client/ui-conversation/README.i18n.yaml # packages/client/ui-conversation/README.zh.md # packages/client/ui-conversation/package.json # packages/client/ui-conversation/src/client/apply.ts # packages/client/ui-theme/README.i18n.yaml # packages/client/ui-theme/README.md # packages/client/ui-theme/README.zh.md # packages/host/apiproxy/README.i18n.yaml # packages/host/apiproxy/README.zh.md # pnpm-lock.yaml
124 lines
6.5 KiB
TypeScript
124 lines
6.5 KiB
TypeScript
// @vitest-environment jsdom
|
|
// apply wiring: the conversation service provided, the chat view registered
|
|
// as the first 'conversation.view' ring entry declaring the whole-Tool seat,
|
|
// the slot registrations land against a root entry's children declarations
|
|
// (the AppFrame role), and the shared store handle rides all strict session
|
|
// entries. Tool composition belongs to ui-tool and its machinery spec.
|
|
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import { SlotTestRuntime, usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime'
|
|
import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots'
|
|
import { LocaleService } from '@deepseek-ai/dsh-client-locale/client'
|
|
import type { SessionId } from '@deepseek-ai/dsh-client-runtime/client'
|
|
import { apply, inject } from '@deepseek-ai/dsh-client-ui-conversation/client'
|
|
|
|
// The service reads its initial locale from the browser; these specs assert
|
|
// the shipped Chinese copy, so they state the browser they assume.
|
|
usePinnedBrowserLanguages('zh-CN')
|
|
|
|
const ROOT = 'root-1' as SessionId
|
|
const CHILD = 'child-1' as SessionId
|
|
|
|
async function bench() {
|
|
const runtime = await SlotTestRuntime.create()
|
|
runtime.provide('connection', { api: { settings: {} }, isLoopback: false } as never)
|
|
await runtime.sessions.add({ id: ROOT, summary: { title: 'R', displayTitle: 'R' } }, { current: false })
|
|
await runtime.sessions.add(
|
|
{ id: CHILD, summary: { title: 'C', displayTitle: 'C', parentId: ROOT } }, { current: false })
|
|
runtime.provide('layout', { openDetails: vi.fn(), closeDetails: vi.fn() })
|
|
const locale = new LocaleService(runtime.ctx)
|
|
runtime.provide('locale', locale)
|
|
runtime.slots.installLocale(locale)
|
|
|
|
// Declared by ui-layout's root entry in production; the test root declares
|
|
// them here so the contributions land.
|
|
await runtime.root.declare({
|
|
'conversation': { kind: 'single', scope: 'session-maybe' },
|
|
'details': { kind: 'single', scope: 'session' },
|
|
'settings.general.item': { kind: 'list', scope: 'root' },
|
|
}, (_p: { renderSlot?: unknown }) => null)
|
|
|
|
const feature = await runtime.mount({ inject: [...inject], apply })
|
|
return { runtime, feature, slots: runtime.slots }
|
|
}
|
|
|
|
/** First stored entry for a key (inject/store live directly on StoredEntry). */
|
|
function renderEntryOf(slots: Awaited<ReturnType<typeof bench>>['slots'], key: 'conversation' | 'conversation.session' | 'conversation.session.header' | '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()
|
|
expect(b.runtime.ctx.get('conversation')).toBeDefined()
|
|
await b.runtime.dispose()
|
|
})
|
|
|
|
it('registers the chat view and its keyed business-node seat', async () => {
|
|
const b = await bench()
|
|
const entries = b.slots.entries('conversation.view')
|
|
expect(entries.map(e => e.options.id)).toEqual(['chat'])
|
|
// Label is a locale thunk resolving through the zh dictionary.
|
|
expect(resolveSlotLabel(entries[0]?.options.label)).toBe('对话')
|
|
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.
|
|
const nodeSlot = b.slots.spec('conversation.chat.node')
|
|
expect(nodeSlot).toMatchObject({ kind: 'keyed', scope: 'session' })
|
|
expect(nodeSlot?.inject?.hooks?.turnData).toBeTypeOf('function')
|
|
await b.runtime.dispose()
|
|
})
|
|
|
|
it('occupies the slots + the ring; session entries share one store handle', async () => {
|
|
const b = await bench()
|
|
const conversation = renderEntryOf(b.slots, 'conversation')
|
|
const conversationSession = renderEntryOf(b.slots, 'conversation.session')
|
|
const conversationHeader = renderEntryOf(b.slots, 'conversation.session.header')
|
|
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(conversationHeader?.store).toBe(conversationSession?.store)
|
|
expect(details?.store).toBe(conversationSession?.store)
|
|
expect(chatView?.store).toBe(conversationSession?.store)
|
|
// The hero holes ride the conversation entry's children declaration (the
|
|
// empty-state occupant is gone). Both are root-scoped: the new-session
|
|
// screen precedes the session either would belong to.
|
|
expect(b.slots.spec('conversation.hero.workspace')).toEqual({ kind: 'single', scope: 'root' })
|
|
expect(b.slots.spec('conversation.hero.agentPreset')).toEqual({ kind: 'single', scope: 'root' })
|
|
expect(b.slots.entries('settings.general.item').map(entry => entry.options.id)).toEqual(['composer-enter'])
|
|
await b.runtime.dispose()
|
|
})
|
|
|
|
it('leaves per-Tool rows to the ui-tool plugin', async () => {
|
|
const b = await bench()
|
|
// The actual toolview declaration activates every registrant. The
|
|
// file-mutation registrant claims both write and edit for the diff card; the
|
|
// one search row registers under both grep and glob; the web rows register
|
|
// one component under both web tool names.
|
|
expect(b.slots.entries('conversation.chat.node').map(entry => entry.options.key)).not.toContain('tool-call')
|
|
// Stats stick with the composer (not inside ChatView).
|
|
expect(b.slots.entries('conversation.composer.dock').map(e => e.options.id)).toEqual(['stats'])
|
|
await b.runtime.dispose()
|
|
})
|
|
|
|
it('plugin fiber disposal collects every registration (unload cascade, ring and hole included)', async () => {
|
|
const b = await bench()
|
|
await b.feature.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.node')).toHaveLength(0)
|
|
expect(b.slots.spec('conversation.chat.node')).toBeUndefined()
|
|
expect(b.slots.entries('details')).toHaveLength(0)
|
|
expect(b.slots.entries('settings.general.item')).toHaveLength(0)
|
|
expect(b.runtime.ctx.get('conversation')).toBeUndefined()
|
|
await b.runtime.dispose()
|
|
})
|
|
})
|