browser-plugin.client.spec.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. // @vitest-environment jsdom
  2. import { Context, Service } from '@deepseek-ai/cordis'
  3. import { afterEach, describe, expect, it, vi } from 'vitest'
  4. import { cleanup } from '@testing-library/react'
  5. import { LocaleRuntime } from '@deepseek-ai/dsh-client-locale/client'
  6. import { SlotRegistry } from '@deepseek-ai/dsh-client-ui-renderer/client'
  7. import { resolveSlotLabel } from '@deepseek-ai/dsh-client-ui-slots'
  8. import { TestRemote, usePinnedBrowserLanguages } from '@deepseek-ai/dsh-client-test-runtime'
  9. import { apply, inject, NS } from '../src/client/index.ts'
  10. import { PluginManagerSettingsTab } from '../src/client/PluginManagerSettingsTab.tsx'
  11. import { PresetPluginsSection } from '../src/client/PresetPluginsSection.tsx'
  12. import type { PluginManagerFace } from '../src/client/manager-store.ts'
  13. import { apply as hostApply } from '../src/index.ts'
  14. usePinnedBrowserLanguages('zh-CN')
  15. afterEach(cleanup)
  16. async function bench() {
  17. const ctx = new Context()
  18. await ctx.plugin(SlotRegistry).await()
  19. const locale = new LocaleRuntime(ctx)
  20. ctx.provide('locale', locale)
  21. class LocaleHolder extends Service {
  22. constructor(serviceCtx: Context) {
  23. super(serviceCtx, 'localeHolder')
  24. }
  25. }
  26. new LocaleHolder(ctx)
  27. const list = vi.fn(() => Promise.resolve({ ok: true as const, value: [] }))
  28. const inventory = vi.fn(() => Promise.resolve({ ok: true as const, value: { entries: [] } }))
  29. const remote = new TestRemote(ctx, {
  30. plugins: { list },
  31. pluginInventory: { list: inventory },
  32. })
  33. return { ctx, slots: ctx.get('slots') as SlotRegistry, locale, list, inventory, remote }
  34. }
  35. function declare(slots: SlotRegistry): () => void {
  36. return slots.register({
  37. name: 'root',
  38. children: {
  39. 'settings.plugins.tab': { kind: 'list', scope: 'root' },
  40. 'settings.agentPreset.detail': { kind: 'list', scope: 'root' },
  41. },
  42. } as never, () => null)
  43. }
  44. describe('ui-settings-plugin-manager browser plugin', () => {
  45. it('keeps the host Loader entry inert', () => {
  46. expect(hostApply).not.toThrow()
  47. })
  48. it('declares only the services the tab and its two Remote faces use', () => {
  49. expect(inject).toEqual(['slots', 'locale', 'remote', 'remote.plugins', 'remote.pluginInventory'])
  50. })
  51. it('registers a localized tab that reads the Host only once rendered, and follows Host changes', async () => {
  52. const b = await bench()
  53. declare(b.slots)
  54. const fiber = b.ctx.plugin({ inject: [...inject], apply })
  55. await fiber.await()
  56. const entry = b.slots.entries('settings.plugins.tab')[0]!
  57. expect(entry.component).toBe(PluginManagerSettingsTab)
  58. expect(entry.options).toMatchObject({ id: 'manage', order: -10 })
  59. expect(entry.locale).toBe(NS)
  60. expect(resolveSlotLabel(entry.options.label)).toBe('插件管理')
  61. // The same store feeds the capabilities section of every preset's detail page.
  62. const section = b.slots.entries('settings.agentPreset.detail')[0]!
  63. expect(section.component).toBe(PresetPluginsSection)
  64. expect(section.options).toMatchObject({ id: 'plugins', order: 0 })
  65. expect(section.locale).toBe(NS)
  66. const sectionFace = (section.inject as unknown as () => PluginManagerFace)()
  67. expect(b.list).not.toHaveBeenCalled()
  68. const face = (entry.inject as unknown as () => PluginManagerFace)()
  69. expect(sectionFace.hooks.pluginManager).toBe(face.hooks.pluginManager)
  70. // A Host change before the first render is not a reason to read.
  71. b.remote.emit('plugins/changed', [{ reason: 'install' }])
  72. b.ctx.emit('connection/reset')
  73. await Promise.resolve()
  74. expect(b.list).not.toHaveBeenCalled()
  75. face.ensure()
  76. await vi.waitFor(() => { expect(face.hooks.pluginManager.getSnapshot().status).toBe('ready') })
  77. expect(b.list).toHaveBeenCalledTimes(1)
  78. b.remote.emit('plugins/changed', [{ reason: 'enable', packageName: 'x' }])
  79. await vi.waitFor(() => { expect(b.list).toHaveBeenCalledTimes(2) })
  80. b.ctx.emit('connection/reset')
  81. await vi.waitFor(() => { expect(b.list).toHaveBeenCalledTimes(3) })
  82. // Install output folds into an open run only.
  83. face.openInstall()
  84. face.editInstallSpec('pkg')
  85. b.remote.emit('plugins/install-log', [{ jobId: 'j', argv: ['pnpm', 'add', 'pkg'], cwd: '/p', spec: 'pkg', stream: 'stdout', text: 'early' }])
  86. expect(face.hooks.pluginManager.getSnapshot().install.runs).toEqual([])
  87. // Shipped preset names resolve over the agent-preset dictionaries the
  88. // real plugin registers; user-authored metadata stays untranslated.
  89. b.locale.register('settings.agentPreset', 'zh', { presetStandardName: '标准模式' } as never)
  90. expect(face.presetName({ id: 'standard', trust: 'system', isDefault: true, rows: [] })).toBe('标准模式')
  91. expect(face.presetName({ id: 'mine', trust: 'user', name: '我自己的', isDefault: false, rows: [] })).toBe('我自己的')
  92. await fiber.dispose()
  93. expect(b.slots.entries('settings.plugins.tab')).toHaveLength(0)
  94. expect(b.slots.entries('settings.agentPreset.detail')).toHaveLength(0)
  95. b.remote.emit('plugins/changed', [{ reason: 'install' }])
  96. await Promise.resolve()
  97. expect(b.list).toHaveBeenCalledTimes(3)
  98. })
  99. })