web-plugins.e2e.ts 2.7 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
  1. /**
  2. * Web UI plugin assembly: the in-memory Loader tree mounts all eight 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 { afterEach, describe, expect, it } from 'vitest'
  14. import { createHostWebPluginRegistry } from '@deepseek-ai/dsh-host-webserver'
  15. import { WEB_UI_PLUGINS, mountWebPlugins } from '../src/web-plugins.ts'
  16. const nodeRequire = createRequire(import.meta.url)
  17. const built = WEB_UI_PLUGINS.every((name) => {
  18. try {
  19. return existsSync(nodeRequire.resolve(name))
  20. } catch {
  21. return false
  22. }
  23. })
  24. let root: Context | undefined
  25. afterEach(async () => {
  26. await root?.fiber.dispose()
  27. root = undefined
  28. })
  29. describe.skipIf(!built)('mountWebPlugins + registry', () => {
  30. it('mounts the eight-package in-memory Loader tree and projects the boot manifest', async () => {
  31. root = new Context()
  32. const mounted = await mountWebPlugins(root)
  33. const registry = createHostWebPluginRegistry({
  34. ctx: root,
  35. loader: mounted.loader,
  36. resolvePkgJson: mounted.resolvePkgJson,
  37. onError: (err) => { throw err },
  38. })
  39. const rows = registry.snapshot()
  40. expect(rows.map(r => r.id)).toEqual([...WEB_UI_PLUGINS])
  41. // The infra four are the early-load group; the UI four are not.
  42. const immediate = rows.filter(r => r.immediately === true).map(r => r.id)
  43. expect(immediate).toEqual([
  44. '@deepseek-ai/dsh-client-connection',
  45. '@deepseek-ai/dsh-client-runtime',
  46. '@deepseek-ai/dsh-client-ui-theme',
  47. '@deepseek-ai/dsh-client-i18n',
  48. ])
  49. // Every row resolves a client path under its own package lib/.
  50. for (const row of rows) {
  51. expect(registry.clientPath(row.id)).toMatch(/lib[/\\]client\.js$/)
  52. expect(row.url).toBe(`/plugins/${row.id}/client.js`)
  53. }
  54. registry.dispose()
  55. })
  56. it('is idempotent: a second mount reuses the loader and creates no duplicate entries', async () => {
  57. root = new Context()
  58. await mountWebPlugins(root)
  59. const second = await mountWebPlugins(root)
  60. // ctx.loader hands out a fresh traced proxy per access, so loader identity
  61. // is not assertable; the observable contract is a single entry per package.
  62. const names = [...second.loader.entries()].map(e => e.options.name)
  63. .filter(n => (WEB_UI_PLUGINS as readonly string[]).includes(n))
  64. expect(names.length).toBe(WEB_UI_PLUGINS.length)
  65. })
  66. })