node-half.spec.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. /** Node-half composition diagnostics for package metadata and built client bundles. */
  2. import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from 'node:fs'
  3. import { tmpdir } from 'node:os'
  4. import { join } from 'node:path'
  5. import { pathToFileURL } from 'node:url'
  6. import { Context } from 'cordis'
  7. import { afterEach, describe, expect, it } from 'vitest'
  8. import type { HttpServerService } from '@deepseek-ai/dsh-host-webserver'
  9. import { ClientModuleHostService } from '../src/index.ts'
  10. let root: string | undefined
  11. afterEach(() => {
  12. if (root !== undefined) rmSync(root, { recursive: true, force: true })
  13. root = undefined
  14. })
  15. /** Create a resolvable dshClient package whose client export points at the returned path. */
  16. function writePackage(packageName: string): string {
  17. root ??= realpathSync(mkdtempSync(join(tmpdir(), 'dsh-client-modules-')))
  18. const pkgRoot = join(root, 'node_modules', ...packageName.split('/'))
  19. const clientPath = join(pkgRoot, 'lib', 'client.js')
  20. mkdirSync(pkgRoot, { recursive: true })
  21. writeFileSync(join(pkgRoot, 'package.json'), JSON.stringify({
  22. name: packageName,
  23. exports: {
  24. './client': './lib/client.js',
  25. './package.json': './package.json',
  26. },
  27. dshClient: { platform: 'web' },
  28. }))
  29. return clientPath
  30. }
  31. /** Construct the node-half service over the enabled fixture entries. */
  32. function construct(packageNames: string[]): ClientModuleHostService {
  33. const ctx = new Context()
  34. ctx.baseUrl = pathToFileURL(root!).href + '/'
  35. ctx.provide('loader', {
  36. *entries() {
  37. for (const packageName of packageNames) {
  38. yield { options: { name: packageName }, fiber: {}, disabled: false }
  39. }
  40. },
  41. })
  42. const httpServer: Pick<HttpServerService, 'port' | 'register' | 'tapIndex'> = {
  43. port: 0,
  44. register: () => () => {},
  45. tapIndex: () => () => {},
  46. }
  47. ctx.provide('httpServer', httpServer as HttpServerService)
  48. return new ClientModuleHostService(ctx)
  49. }
  50. describe('client bundle activation', () => {
  51. it('groups missing bundles under one source-build instruction with a package/path list', () => {
  52. const firstName = '@fixture/missing-first'
  53. const secondName = '@fixture/missing-second'
  54. const firstPath = writePackage(firstName)
  55. const secondPath = writePackage(secondName)
  56. expect(() => construct([firstName, secondName])).toThrow([
  57. 'client-modules: 2 client packages failed to compose:',
  58. ' client bundles not found; run `pnpm run build` before launch:',
  59. ` - package: ${firstName}`,
  60. ` path: ${firstPath}`,
  61. ` - package: ${secondName}`,
  62. ` path: ${secondPath}`,
  63. ].join('\n'))
  64. })
  65. it('does not report other bundle read failures as missing builds', () => {
  66. const packageName = '@fixture/unreadable-client'
  67. const clientPath = writePackage(packageName)
  68. mkdirSync(clientPath, { recursive: true })
  69. let thrown: unknown
  70. try {
  71. construct([packageName])
  72. } catch (error) {
  73. thrown = error
  74. }
  75. expect(String(thrown)).toContain('client-modules: 1 client package failed to compose:')
  76. expect(String(thrown)).toContain(' other failures:')
  77. expect(String(thrown)).toContain('EISDIR')
  78. expect(String(thrown)).not.toContain('pnpm run build')
  79. })
  80. })