web-plugins.e2e.ts 3.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. /**
  2. * Web UI plugin assembly: the in-memory Loader tree mounts all nine UI
  3. * packages (node halves), and the webserver registry built over it yields the
  4. * full __DSH_BOOT__ manifest — the P-I config-source bar end to end.
  5. *
  6. * The Loader imports plugin packages through their exports maps (lib/), so
  7. * this is a built-artifact e2e: it skips until the workspace build has run
  8. * (`pnpm run build`), like the other built-* e2e suites.
  9. */
  10. import { existsSync } from 'node:fs'
  11. import { createRequire } from 'node:module'
  12. import { Context } from 'cordis'
  13. import ToolRegistry from '@deepseek-ai/dsh-tools'
  14. import SystemPrompt from '@deepseek-ai/dsh-system-prompt'
  15. import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
  16. import { afterEach, describe, expect, it } from 'vitest'
  17. import { createHostWebPluginRegistry } from '@deepseek-ai/dsh-host-webserver'
  18. import { WEB_UI_PLUGINS, mountWebPlugins } from '../src/web-plugins.ts'
  19. const nodeRequire = createRequire(import.meta.url)
  20. const built = WEB_UI_PLUGINS.every((name) => {
  21. try {
  22. return existsSync(nodeRequire.resolve(name))
  23. } catch {
  24. return false
  25. }
  26. })
  27. let root: Context | undefined
  28. afterEach(async () => {
  29. await root?.fiber.dispose()
  30. root = undefined
  31. })
  32. describe.skipIf(!built)('mountWebPlugins + registry', () => {
  33. async function rootWithHostServices(): Promise<Context> {
  34. root = new Context()
  35. await root.plugin(SystemPrompt)
  36. await root.plugin(ToolRegistry)
  37. await root.plugin(UserInteractionService)
  38. return root
  39. }
  40. it('mounts the nine-package in-memory Loader tree and projects the boot manifest', async () => {
  41. root = await rootWithHostServices()
  42. const mounted = await mountWebPlugins(root)
  43. const registry = createHostWebPluginRegistry({
  44. ctx: root,
  45. loader: mounted.loader,
  46. resolvePkgJson: mounted.resolvePkgJson,
  47. onError: (err) => { throw err },
  48. })
  49. const rows = registry.snapshot()
  50. expect(rows.map(r => r.id)).toEqual([...WEB_UI_PLUGINS])
  51. // The infra four are the early-load group; the UI four are not.
  52. const immediate = rows.filter(r => r.immediately === true).map(r => r.id)
  53. expect(immediate).toEqual([
  54. '@deepseek-ai/dsh-client-connection',
  55. '@deepseek-ai/dsh-client-runtime',
  56. '@deepseek-ai/dsh-client-ui-theme',
  57. '@deepseek-ai/dsh-client-i18n',
  58. ])
  59. // Every row resolves a client path under its own package lib/.
  60. for (const row of rows) {
  61. expect(registry.clientPath(row.id)).toMatch(/lib[/\\]client\.js$/)
  62. expect(row.url).toBe(`/plugins/${row.id}/client.js`)
  63. }
  64. registry.dispose()
  65. })
  66. it('is idempotent: a second mount reuses the loader and creates no duplicate entries', async () => {
  67. root = await rootWithHostServices()
  68. await mountWebPlugins(root)
  69. const second = await mountWebPlugins(root)
  70. // ctx.loader hands out a fresh traced proxy per access, so loader identity
  71. // is not assertable; the observable contract is a single entry per package.
  72. const names = [...second.loader.entries()].map(e => e.options.name)
  73. .filter(n => (WEB_UI_PLUGINS as readonly string[]).includes(n))
  74. expect(names.length).toBe(WEB_UI_PLUGINS.length)
  75. })
  76. })