browser-plugin.spec.ts 3.9 KB

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