startup.spec.ts 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  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. ' openBrowser: !!js ctx.webStartup.openBrowser',
  55. ' port: !!js ctx.webStartup.port ?? 3080',
  56. ' trustedHosts: !!js ctx.webStartup.trustedHosts',
  57. '- id: provider',
  58. ` name: ${pathToFileURL(join(dir, 'provider.mjs')).href}`,
  59. '',
  60. ].join('\n'))
  61. const observing = { write: (chunk: string) => { observed.out += chunk; return true } }
  62. internals.stdout = observing
  63. internals.stderr = observing
  64. const globals = globalThis as unknown as {
  65. __webStartupApply: typeof apply
  66. __webStartupObserved: Observed
  67. }
  68. globals.__webStartupApply = apply
  69. globals.__webStartupObserved = observed
  70. const ctx = new Context()
  71. await ctx.plugin(Loader)
  72. ctx.loader.builtins.include = Include
  73. provideCmdline(ctx, { args, exit: code => void observed.exits.push(code) })
  74. await ctx.loader.create({ name: 'cordis:include', config: { path: pathToFileURL(join(dir, 'cordis.yml')).href } })
  75. await ctx.loader.await()
  76. disposers.push(async () => { await ctx.fiber.dispose() })
  77. return {
  78. values: ctx.get(WEB_STARTUP_SERVICE) as WebStartupValues | undefined,
  79. observed,
  80. }
  81. }
  82. describe('web command-line provider', () => {
  83. it('publishes each flag and releases direct service expressions', async () => {
  84. const { values, observed } = await bootProvider([
  85. '--host', '127.0.0.1',
  86. '--no-open',
  87. '--port', '8080',
  88. '--trusted-host', 'lab.internal', 'lab-2.internal',
  89. '--trusted-host', '10.0.0.9',
  90. ])
  91. expect(values).toEqual({
  92. host: '127.0.0.1',
  93. openBrowser: false,
  94. port: 8080,
  95. trustedHosts: ['lab.internal', 'lab-2.internal', '10.0.0.9'],
  96. })
  97. expect(observed.readerConfig).toEqual(values)
  98. expect(observed.exits).toEqual([])
  99. })
  100. it('leaves deployment values to each consumer when flags omit them', async () => {
  101. const { values, observed } = await bootProvider([])
  102. expect(values).toEqual({ openBrowser: true, trustedHosts: [] })
  103. expect(observed.readerConfig).toEqual({
  104. host: '127.0.0.1',
  105. openBrowser: true,
  106. port: 3080,
  107. trustedHosts: [],
  108. })
  109. })
  110. it('prints its own help and leaves the consumer pending', async () => {
  111. const { values, observed } = await bootProvider(['--help'])
  112. expect(observed.out).toContain('dsh --profile web')
  113. expect(observed.out).toContain('--no-open')
  114. expect(observed.out).toContain('--trusted-host')
  115. expect(values).toBeUndefined()
  116. expect(observed.readerConfig).toBeUndefined()
  117. expect(observed.exits).toEqual([0])
  118. })
  119. it('rejects a non-numeric port before the consumer activates', async () => {
  120. const { values, observed } = await bootProvider(['--port', 'abc'])
  121. expect(observed.out).toContain('--port must be a number')
  122. expect(values).toBeUndefined()
  123. expect(observed.readerConfig).toBeUndefined()
  124. expect(observed.exits).toEqual([1])
  125. })
  126. it('rejects the intentionally unsupported all-interfaces host before the consumer activates', async () => {
  127. const { values, observed } = await bootProvider(['--host', '0.0.0.0'])
  128. expect(observed.out).toContain('--host 0.0.0.0 is intentionally not supported yet for safety: it would expose remote code execution to the network; use 127.0.0.1 instead')
  129. expect(values).toBeUndefined()
  130. expect(observed.readerConfig).toBeUndefined()
  131. expect(observed.exits).toEqual([1])
  132. })
  133. })