web-plugins.spec.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. /**
  2. * mountWebPlugins unit coverage (keyless). The Loader-facing behavior —
  3. * baseUrl anchoring, entry creation with idempotent reuse, the fiber-less
  4. * fail-loud sweep, and the resolver seam — is exercised against a stubbed
  5. * loader service so it runs without built lib/ artifacts. The roster is
  6. * caller-supplied now (composition moved to apps/cli), so these tests pass
  7. * their own lists.
  8. */
  9. import { Context } from 'cordis'
  10. import { afterEach, describe, expect, it } from 'vitest'
  11. import { mountWebPlugins } from '../src/web-plugins.ts'
  12. const ROSTER = [
  13. '@deepseek-ai/dsh-plugin-a',
  14. '@deepseek-ai/dsh-plugin-b',
  15. '@deepseek-ai/dsh-plugin-c',
  16. ] as const
  17. interface FakeEntry {
  18. options: { name: string }
  19. fiber?: unknown
  20. disabled: boolean
  21. }
  22. /** Loader stub provided under the real service name (mountWebPlugins skips ctx.plugin(Loader) when present). */
  23. class FakeLoader {
  24. readonly created: string[] = []
  25. awaited = 0
  26. constructor(private readonly entriesList: FakeEntry[], private readonly onCreate?: (name: string) => void) {}
  27. entries(): Iterable<FakeEntry> {
  28. return this.entriesList
  29. }
  30. async create(options: { name: string }): Promise<void> {
  31. this.created.push(options.name)
  32. this.onCreate?.(options.name)
  33. }
  34. async await(): Promise<void> {
  35. this.awaited += 1
  36. }
  37. }
  38. let root: Context | undefined
  39. afterEach(async () => {
  40. await root?.fiber.dispose()
  41. root = undefined
  42. })
  43. function withLoader(entriesList: FakeEntry[], onCreate?: (name: string) => void): { ctx: Context; loader: FakeLoader } {
  44. root = new Context()
  45. const loader = new FakeLoader(entriesList, onCreate)
  46. root.reflect.provide('loader', loader)
  47. return { ctx: root, loader }
  48. }
  49. describe('mountWebPlugins (stubbed loader)', () => {
  50. it('creates one entry per roster package, awaits the tree, and returns the loader view + resolver', async () => {
  51. const entriesList: FakeEntry[] = []
  52. const { ctx, loader } = withLoader(entriesList, (name) => {
  53. entriesList.push({ options: { name }, fiber: {}, disabled: false })
  54. })
  55. const mounted = await mountWebPlugins(ctx, ROSTER, import.meta.url)
  56. expect(loader.created).toEqual([...ROSTER])
  57. expect(loader.awaited).toBe(1)
  58. expect([...mounted.loader.entries()].map(e => e.options.name)).toEqual([...ROSTER])
  59. // The resolver resolves a real package manifest through real module resolution, anchored at this test file.
  60. expect(mounted.resolvePkgJson('@deepseek-ai/dsh-host-runtime')).toMatch(/package\.json$/)
  61. expect(ctx.baseUrl).toBeDefined()
  62. })
  63. it('reuses existing entries (idempotent mount creates no duplicates)', async () => {
  64. const preexisting: FakeEntry[] = ROSTER.map(name => ({ options: { name }, fiber: {}, disabled: false }))
  65. const { ctx, loader } = withLoader(preexisting)
  66. await mountWebPlugins(ctx, ROSTER, import.meta.url)
  67. expect(loader.created).toEqual([])
  68. })
  69. it('throws listing every fiber-less entry (silent import failure must not drop a client plugin)', async () => {
  70. const entriesList: FakeEntry[] = []
  71. const { ctx } = withLoader(entriesList, (name) => {
  72. // First one loads; the rest stay fiber-less (import failed silently).
  73. entriesList.push({ options: { name }, fiber: entriesList.length < 1 ? {} : undefined, disabled: false })
  74. })
  75. await expect(mountWebPlugins(ctx, ROSTER, import.meta.url))
  76. .rejects.toThrow(/client plugin\(s\) failed to load: .*dsh-plugin-c/)
  77. })
  78. it('skips disabled entries in the fail-loud sweep (disabled is the one valid fiber-less state)', async () => {
  79. const entriesList: FakeEntry[] = ROSTER.map(name => ({ options: { name }, fiber: undefined, disabled: true }))
  80. const { ctx } = withLoader(entriesList)
  81. await expect(mountWebPlugins(ctx, ROSTER, import.meta.url)).resolves.toBeDefined()
  82. })
  83. it('mounts the real Loader when none is present (the ctx.plugin(Loader) branch)', async () => {
  84. root = new Context()
  85. // An empty roster keeps this keyless and artifact-free: the branch under
  86. // test is only the Loader auto-mount.
  87. await mountWebPlugins(root, [], import.meta.url)
  88. expect(root.get('loader') !== undefined).toBe(true)
  89. }, 30_000) // cold-cache import of the real vendored Loader crosses the network-disk 5s default
  90. it('keeps a caller-set baseUrl (anchors only when absent)', async () => {
  91. const entriesList: FakeEntry[] = []
  92. const { ctx } = withLoader(entriesList, (name) => {
  93. entriesList.push({ options: { name }, fiber: {}, disabled: false })
  94. })
  95. ctx.baseUrl = 'file:///caller/anchor/'
  96. await mountWebPlugins(ctx, ROSTER, import.meta.url)
  97. expect(ctx.baseUrl).toBe('file:///caller/anchor/')
  98. })
  99. })