assembly-modules.client.spec.ts 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. /**
  2. * In-process module arrival: dynamic `/client` imports resolve through the
  3. * repository path aliases, provided rows replace them, the bootstrap row is the
  4. * static namespace, and the resulting module system serves the vendored Loader
  5. * without ever loading a bundle.
  6. */
  7. import { Context } from '@deepseek-ai/cordis'
  8. import Loader from '@deepseek-ai/cordis-plugin-loader'
  9. import * as modulesClient from '@deepseek-ai/dsh-client-modules/client'
  10. import { parseBootManifest } from '@deepseek-ai/dsh-client-modules/client'
  11. import type { WebBootGraph } from '@deepseek-ai/dsh-client-modules/client'
  12. import * as uiRenderer from '@deepseek-ai/dsh-client-ui-renderer/client'
  13. import * as typertRegistry from '@deepseek-ai/dsh-typert-registry/client'
  14. import { describe, expect, it, onTestFinished } from 'vitest'
  15. import { ClientRoster } from '../src/assembly/index.ts'
  16. import { MODULES_PACKAGE, createInProcessModules, loadPluginModules } from '../src/assembly/modules.ts'
  17. const RENDERER = '@deepseek-ai/dsh-client-ui-renderer'
  18. const TYPERT = '@deepseek-ai/dsh-typert-registry'
  19. const BRAND = '@deepseek-ai/dsh-client-ui-brand-official'
  20. const MISSING = '@deepseek-ai/dsh-client-does-not-exist'
  21. const row = (name: string, immediately = false) => ({ name, inject: [], immediately })
  22. function graph(names: readonly string[]): WebBootGraph {
  23. return {
  24. rev: 'local',
  25. entries: names.map(id => ({ id, url: `/plugins/${id}/client.js`, rev: 'local' })),
  26. batches: [{ phase: 'application', url: '/plugins/all.js', rev: 'local', entries: [...names] }],
  27. }
  28. }
  29. describe('loadPluginModules', () => {
  30. it('imports real rows dynamically, takes provided rows from the plan, and pins the bootstrap row', async () => {
  31. const provided = { apply: () => {} }
  32. const roster = ClientRoster.of([row(TYPERT), row(MODULES_PACKAGE), row(BRAND), row(RENDERER)])
  33. const modules = await loadPluginModules({ roster, provide: { [BRAND]: provided } })
  34. expect([...modules.keys()]).toEqual([TYPERT, MODULES_PACKAGE, BRAND, RENDERER])
  35. expect(modules.get(TYPERT)).toBe(typertRegistry)
  36. expect(modules.get(RENDERER)).toBe(uiRenderer)
  37. expect(modules.get(MODULES_PACKAGE)).toBe(modulesClient)
  38. expect(modules.get(BRAND)).toBe(provided)
  39. })
  40. it('prefixes the package name when a /client import fails', async () => {
  41. const roster = ClientRoster.of([row(MISSING)])
  42. await expect(loadPluginModules({ roster })).rejects.toThrow(
  43. new RegExp(`^client-test-runtime: cannot import ${MISSING}/client: .+`),
  44. )
  45. })
  46. it('refuses a provided bootstrap row', async () => {
  47. const roster = ClientRoster.of([row(MODULES_PACKAGE)])
  48. await expect(loadPluginModules({ roster, provide: { [MODULES_PACKAGE]: { apply: () => {} } } }))
  49. .rejects.toThrow(`${MODULES_PACKAGE} is the bootstrap module and cannot be provided`)
  50. })
  51. })
  52. describe('createInProcessModules', () => {
  53. it('serves import() and prefetch() from registered factories and never loads a bundle', async () => {
  54. const roster = ClientRoster.of([row(MODULES_PACKAGE, true), row(TYPERT, true), row(RENDERER)])
  55. const modules = await loadPluginModules({ roster })
  56. const system = createInProcessModules(graph(roster.rows.map(r => r.name)), modules)
  57. await expect(system.prefetch(TYPERT)).resolves.toBeUndefined()
  58. await expect(system.prefetch(RENDERER)).resolves.toBeUndefined()
  59. await expect(system.prefetch(MODULES_PACKAGE)).resolves.toBeUndefined()
  60. expect(system.loadCache.has(TYPERT)).toBe(false)
  61. expect(system.loadCache.has(RENDERER)).toBe(false)
  62. await expect(system.import(RENDERER, '', {})).resolves.toBe(uiRenderer)
  63. await expect(system.import(`${TYPERT}/client`, '', {})).resolves.toBe(typertRegistry)
  64. await expect(system.import(MODULES_PACKAGE, '', {})).resolves.toBe(modulesClient)
  65. expect(system.loadCache.get(TYPERT)?.exports).toBe(typertRegistry)
  66. expect(system.loadCache.get(RENDERER)?.exports).toBe(uiRenderer)
  67. await expect(system.import('@deepseek-ai/dsh-client-unknown', '', {})).rejects.toThrow('cannot resolve')
  68. })
  69. it('rejects loudly instead of fetching when a graph row has no loaded module', async () => {
  70. const system = createInProcessModules(graph(['ghost']), new Map())
  71. await expect(system.prefetch('ghost')).rejects.toThrow('in-process modules never load bundles (/plugins/all.js)')
  72. await expect(system.import('ghost', '', {})).rejects.toThrow('in-process modules never load bundles (/plugins/all.js)')
  73. })
  74. it('exposes the graph parsed by the production validator as its manifest', () => {
  75. const raw = graph(['a', 'b'])
  76. expect(createInProcessModules(raw, new Map()).manifest).toEqual(parseBootManifest(raw))
  77. expect(() => createInProcessModules({ ...raw, batches: [] }, new Map())).toThrow('belongs to no initial-load batch')
  78. })
  79. it('boots the vendored Loader over the in-process table with every entry active', async () => {
  80. const roster = ClientRoster.of([row(TYPERT, true), row(RENDERER, true), row(MODULES_PACKAGE, true)])
  81. const modules = await loadPluginModules({ roster })
  82. const system = createInProcessModules(graph(roster.rows.map(r => r.name)), modules)
  83. const ctx = new Context()
  84. onTestFinished(() => ctx.fiber.dispose())
  85. await ctx.plugin(Loader)
  86. ctx.loader.internal = system as never
  87. await Promise.all(roster.rows.map(r => ctx.loader.create({ name: r.name })))
  88. await ctx.loader.await()
  89. expect(ctx.get('typert')).toBeDefined()
  90. expect(ctx.get('slots')).toBeDefined()
  91. expect(ctx.get('modules')).toBe(system)
  92. expect([...ctx.loader.entries()].every(entry => entry.fiber !== undefined)).toBe(true)
  93. })
  94. })