startup.spec.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  1. /**
  2. * The Web command-line provider over a real Loader tree: its ordinary service
  3. * releases a consumer whose config reads `ctx.webStartup` directly.
  4. */
  5. import { mkdtempSync, writeFileSync } from 'node:fs'
  6. import { tmpdir } from 'node:os'
  7. import { join } from 'node:path'
  8. import { pathToFileURL } from 'node:url'
  9. import { Context } from '@deepseek-ai/cordis'
  10. import Loader from '@deepseek-ai/cordis-plugin-loader'
  11. import Include from '@deepseek-ai/cordis-plugin-include'
  12. import { internals, provideCmdline } from '@deepseek-ai/dsh-cmdline'
  13. import { afterEach, describe, expect, it } from 'vitest'
  14. import { apply, WEB_STARTUP_SERVICE, type WebStartupValues } from '../src/startup.ts'
  15. /** What one fixture boot observed. */
  16. interface Observed {
  17. exits: number[]
  18. out: string
  19. readerConfig?: unknown
  20. }
  21. const disposers: (() => Promise<void>)[] = []
  22. afterEach(async () => {
  23. for (const dispose of disposers.splice(0)) await dispose()
  24. internals.stdout = process.stdout
  25. internals.stderr = process.stderr
  26. })
  27. /**
  28. * Mount the real provider and a consumer using injection-ordered config.
  29. * @param args - the invocation's inner arguments.
  30. * @returns the service value and observed consumer/process effects.
  31. */
  32. async function bootProvider(args: string[]): Promise<{
  33. values: WebStartupValues | undefined
  34. observed: Observed
  35. }> {
  36. const dir = mkdtempSync(join(tmpdir(), 'dsh-web-startup-'))
  37. const observed: Observed = { exits: [], out: '' }
  38. writeFileSync(join(dir, 'reader.mjs'), `
  39. export function apply(_ctx, config) { globalThis.__webStartupObserved.readerConfig = config }
  40. `)
  41. // Node imports the fixture row outside Vite's source resolver, so delegate
  42. // to the source-plane plugin already imported by this test.
  43. writeFileSync(join(dir, 'provider.mjs'), `
  44. export const name = 'web-startup'
  45. export const inject = ['cmdlineArgs']
  46. export const apply = ctx => globalThis.__webStartupApply(ctx)
  47. `)
  48. writeFileSync(join(dir, 'cordis.yml'), [
  49. '- id: reader',
  50. ` name: ${pathToFileURL(join(dir, 'reader.mjs')).href}`,
  51. ` inject: [${WEB_STARTUP_SERVICE}]`,
  52. ' config:',
  53. " host: !!js ctx.webStartup.host ?? '127.0.0.1'",
  54. ' port: !!js ctx.webStartup.port ?? 3080',
  55. ' trustedHosts: !!js ctx.webStartup.trustedHosts',
  56. '- id: provider',
  57. ` name: ${pathToFileURL(join(dir, 'provider.mjs')).href}`,
  58. '',
  59. ].join('\n'))
  60. const observing = { write: (chunk: string) => { observed.out += chunk; return true } }
  61. internals.stdout = observing
  62. internals.stderr = observing
  63. const globals = globalThis as unknown as {
  64. __webStartupApply: typeof apply
  65. __webStartupObserved: Observed
  66. }
  67. globals.__webStartupApply = apply
  68. globals.__webStartupObserved = observed
  69. const ctx = new Context()
  70. await ctx.plugin(Loader)
  71. ctx.loader.builtins.include = Include
  72. provideCmdline(ctx, { args, exit: code => void observed.exits.push(code) })
  73. await ctx.loader.create({ name: 'cordis:include', config: { path: pathToFileURL(join(dir, 'cordis.yml')).href } })
  74. await ctx.loader.await()
  75. disposers.push(async () => { await ctx.fiber.dispose() })
  76. return {
  77. values: ctx.get(WEB_STARTUP_SERVICE) as WebStartupValues | undefined,
  78. observed,
  79. }
  80. }
  81. describe('web command-line provider', () => {
  82. it('publishes each flag and releases direct service expressions', async () => {
  83. const { values, observed } = await bootProvider([
  84. '--host', '0.0.0.0',
  85. '--port', '8080',
  86. '--trusted-host', 'lab.internal', 'lab-2.internal',
  87. '--trusted-host', '10.0.0.9',
  88. ])
  89. expect(values).toEqual({
  90. host: '0.0.0.0',
  91. port: 8080,
  92. trustedHosts: ['lab.internal', 'lab-2.internal', '10.0.0.9'],
  93. })
  94. expect(observed.readerConfig).toEqual(values)
  95. expect(observed.exits).toEqual([])
  96. })
  97. it('leaves deployment values to each consumer when flags omit them', async () => {
  98. const { values, observed } = await bootProvider([])
  99. expect(values).toEqual({ trustedHosts: [] })
  100. expect(observed.readerConfig).toEqual({
  101. host: '127.0.0.1',
  102. port: 3080,
  103. trustedHosts: [],
  104. })
  105. })
  106. it('prints its own help and leaves the consumer pending', async () => {
  107. const { values, observed } = await bootProvider(['--help'])
  108. expect(observed.out).toContain('dsh --profile web')
  109. expect(observed.out).toContain('--trusted-host')
  110. expect(values).toBeUndefined()
  111. expect(observed.readerConfig).toBeUndefined()
  112. expect(observed.exits).toEqual([0])
  113. })
  114. it('rejects a non-numeric port before the consumer activates', async () => {
  115. const { values, observed } = await bootProvider(['--port', 'abc'])
  116. expect(observed.out).toContain('--port must be a number')
  117. expect(values).toBeUndefined()
  118. expect(observed.readerConfig).toBeUndefined()
  119. expect(observed.exits).toEqual([1])
  120. })
  121. })