browser-plugin.spec.ts 3.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091
  1. /**
  2. * ui-task plugin halves: the browser entry's dictionary and header-slot
  3. * registrations against the real SlotsService (with fiber teardown proving
  4. * removal — HMR safety), the inert node entry, and the invariant companion's
  5. * ownership reservation.
  6. */
  7. import { Context } from '@deepseek-ai/cordis'
  8. import { describe, expect, it } from 'vitest'
  9. import InvariantService from '@deepseek-ai/dsh-invariants'
  10. import { SlotsService } from '@deepseek-ai/dsh-client-runtime/client'
  11. import { apply as applyLocale, inject as localeInject } from '@deepseek-ai/dsh-client-locale/client'
  12. import { apply, inject } from '../src/client/index.ts'
  13. import { apply as applyNode } from '../src/index.ts'
  14. import * as TaskInvariant from '../src/invariant.ts'
  15. import { en, NS, zh } from '../src/client/locales.ts'
  16. /** Slot ledger reader: entry ids currently registered in the header list. */
  17. function headerEntryIds(ctx: Context): (string | undefined)[] {
  18. return ctx.slots
  19. .entries('conversation.session.header.actions')
  20. .map(entry => entry.options.id)
  21. }
  22. /** Boot the browser half over a real slot tree that declares the header list. */
  23. async function bench(): Promise<{ ctx: Context; fiber: ReturnType<Context['plugin']> }> {
  24. const ctx = new Context()
  25. await ctx.plugin(SlotsService).await()
  26. ctx.slots.register({
  27. name: 'root',
  28. children: {
  29. 'conversation.session.header.actions': { kind: 'list', scope: 'session' },
  30. },
  31. } as never, () => null)
  32. ctx.provide('sessions', {})
  33. // The locale plugin binds a settings scope, which reads the connection handle.
  34. ctx.provide('connection', { api: { settings: {} }, isLoopback: false } as never)
  35. await ctx.plugin({ inject: localeInject, apply: applyLocale }).await()
  36. const fiber = ctx.plugin({ inject: [...inject], apply })
  37. await fiber.await()
  38. return { ctx, fiber }
  39. }
  40. describe('ui-task browser half', () => {
  41. it('declares the services it binds', () => {
  42. expect(inject).toEqual(['sessions', 'slots', 'locale'])
  43. })
  44. it('registers the header action, and fiber teardown removes it (HMR safety)', async () => {
  45. const { ctx, fiber } = await bench()
  46. expect(headerEntryIds(ctx)).toContain('task-list')
  47. await fiber.dispose()
  48. expect(headerEntryIds(ctx)).not.toContain('task-list')
  49. })
  50. it('registers both dictionaries under its own namespace and releases them with the fiber', async () => {
  51. const { ctx, fiber } = await bench()
  52. const translate = ctx.locale.bind(NS)
  53. expect(translate('list.aria')).toBe(zh['list.aria'])
  54. ctx.locale.setLocale('en')
  55. expect(translate('list.aria')).toBe(en['list.aria'])
  56. // Withdrawn dictionaries leave the key unresolved rather than translated.
  57. await fiber.dispose()
  58. expect(translate('list.aria')).not.toBe(en['list.aria'])
  59. })
  60. it('keeps the English dictionary key-identical to the Chinese source of truth', () => {
  61. expect(Object.keys(en).sort()).toEqual(Object.keys(zh).sort())
  62. })
  63. })
  64. describe('ui-task node half', () => {
  65. it('contributes no host behavior', () => {
  66. // The node half exists only so the plugin appears in the Loader tree.
  67. expect(applyNode).not.toThrow()
  68. })
  69. })
  70. describe('ui-task invariant companion', () => {
  71. it('reserves package ownership under its declared companion name', async () => {
  72. const ctx = new Context()
  73. await ctx.plugin(InvariantService, { enabled: true })
  74. const fiber = ctx.plugin(TaskInvariant)
  75. await fiber.await()
  76. expect(TaskInvariant.name).toBe('client-ui-task-invariant')
  77. expect(TaskInvariant.inject).toEqual(['invariants'])
  78. // Emitting an unrelated event proves the companion installed no audit.
  79. expect(() => { (ctx.emit as (event: string) => void)('slots/changed') }).not.toThrow()
  80. await fiber.dispose()
  81. })
  82. })