/** * ui-task plugin halves: the browser entry's dictionary and header-slot * registrations against the real SlotsService (with fiber teardown proving * removal — HMR safety), the inert node entry, and the invariant companion's * ownership reservation. */ import { Context } from '@deepseek-ai/cordis' import { describe, expect, it } from 'vitest' import InvariantService from '@deepseek-ai/dsh-invariants' import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client' import { stubSettingsScope } from '@deepseek-ai/dsh-client-test-runtime' import { apply as applyLocale, inject as localeInject } from '@deepseek-ai/dsh-client-locale/client' import { apply, inject } from '../src/client/index.ts' import { apply as applyNode } from '../src/index.ts' import * as TaskInvariant from '../src/invariant.ts' import { en, NS, zh } from '../src/client/locales.ts' /** Slot ledger reader: entry ids currently registered in the header list. */ function headerEntryIds(ctx: Context): (string | undefined)[] { return ctx.slots .entries('conversation.session.header.actions') .map(entry => entry.options.id) } /** Boot the browser half over a real slot tree that declares the header list. */ async function bench(): Promise<{ ctx: Context; fiber: ReturnType }> { const ctx = new Context() await ctx.plugin(SlotsService).await() ctx.slots.register({ name: 'root', children: { 'conversation.session.header.actions': { kind: 'list', scope: 'session' }, }, } as never, () => null) ctx.provide('sessions', {}) // The locale plugin binds a settings scope, which reads the connection handle // and the forwarded-event port. ctx.provide('connection', { api: { settings: {} }, isLoopback: false } as never) ctx.provide('remote', { $on: () => () => {} } as never) ctx.provide('settingsScope', { bind: () => stubSettingsScope().scope } as never) await ctx.plugin({ inject: localeInject, apply: applyLocale }).await() const fiber = ctx.plugin({ inject: [...inject], apply }) await fiber.await() return { ctx, fiber } } describe('ui-task browser half', () => { it('declares the services it binds', () => { expect(inject).toEqual(['sessions', 'slots', 'locale']) }) it('registers the header action, and fiber teardown removes it (HMR safety)', async () => { const { ctx, fiber } = await bench() expect(headerEntryIds(ctx)).toContain('task-list') await fiber.dispose() expect(headerEntryIds(ctx)).not.toContain('task-list') }) it('registers both dictionaries under its own namespace and releases them with the fiber', async () => { const { ctx, fiber } = await bench() const translate = ctx.locale.bind(NS) expect(translate('list.aria')).toBe(zh['list.aria']) ctx.locale.setLocale('en') expect(translate('list.aria')).toBe(en['list.aria']) // Withdrawn dictionaries leave the key unresolved rather than translated. await fiber.dispose() expect(translate('list.aria')).not.toBe(en['list.aria']) }) it('keeps the English dictionary key-identical to the Chinese source of truth', () => { expect(Object.keys(en).sort()).toEqual(Object.keys(zh).sort()) }) }) describe('ui-task node half', () => { it('contributes no host behavior', () => { // The node half exists only so the plugin appears in the Loader tree. expect(applyNode).not.toThrow() }) }) describe('ui-task invariant companion', () => { it('reserves package ownership under its declared companion name', async () => { const ctx = new Context() await ctx.plugin(InvariantService, { enabled: true }) const fiber = ctx.plugin(TaskInvariant) await fiber.await() expect(TaskInvariant.name).toBe('client-ui-task-invariant') expect(TaskInvariant.inject).toEqual(['invariants']) // Emitting an unrelated event proves the companion installed no audit. expect(() => { (ctx.emit as (event: string) => void)('slots/changed') }).not.toThrow() await fiber.dispose() }) })