lazy-search-startup.compat.spec.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
  1. /**
  2. * Node 22 startup-output smoke for the shipped Web CLI composition.
  3. *
  4. * Only the dedicated Node compatibility gate opts this test in after building
  5. * both artifacts; ordinary Vitest inventory deterministically skips it.
  6. * The child runs built artifacts under plain Node with the real shipped
  7. * config (base.cordis.yml + the web.cordis.yml overlay).
  8. * Its URL line follows AppCLIEntry's settled boot; SIGTERM then exercises the
  9. * shipped quiescent disposer.
  10. */
  11. import { spawn } from 'node:child_process'
  12. import { existsSync } from 'node:fs'
  13. import { mkdtemp, readFile, rm } from 'node:fs/promises'
  14. import { tmpdir } from 'node:os'
  15. import { join, resolve } from 'node:path'
  16. import { fileURLToPath } from 'node:url'
  17. import yaml from 'js-yaml'
  18. import { describe, expect, it } from 'vitest'
  19. const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
  20. const builtBin = join(repoRoot, 'apps/cli/lib/bin.js')
  21. const webDist = join(repoRoot, 'apps/web/dist/index.html')
  22. // The web overlay owns the session-query-sqlite lazy-open patch row.
  23. const configPath = join(repoRoot, 'apps/cli/config/web.cordis.yml')
  24. const requireBuiltArtifacts = process.env.DSH_REQUIRE_BUILT_CLI_SMOKE === '1'
  25. interface ConfigRow {
  26. id?: string
  27. config?: { openAt?: unknown }
  28. }
  29. const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
  30. kind: 'scalar',
  31. construct: value => String(value),
  32. })
  33. const configSchema = yaml.JSON_SCHEMA.extend(jsExprType)
  34. /** Boot the built Web CLI, wait for its settled URL, then dispose through SIGTERM. */
  35. function runBuiltWeb(cwd: string): Promise<{ stdout: string; stderr: string; code: number }> {
  36. return new Promise((resolveRun, rejectRun) => {
  37. const env: NodeJS.ProcessEnv = {
  38. ...process.env,
  39. DEEPSEEK_API_KEY: 'dsh-cli-smoke-dummy-key',
  40. DSH_HOME: join(cwd, '.dsh'),
  41. }
  42. delete env.DEEPSEEK_BASE_URL
  43. delete env.NODE_OPTIONS
  44. delete env.NODE_NO_WARNINGS
  45. const child = spawn(process.execPath, [
  46. builtBin,
  47. 'web',
  48. '--host',
  49. '127.0.0.1',
  50. '--port',
  51. '0',
  52. ], {
  53. cwd,
  54. env,
  55. stdio: ['ignore', 'pipe', 'pipe'],
  56. })
  57. let stdout = ''
  58. let stderr = ''
  59. let settled = false
  60. child.stdout.setEncoding('utf8')
  61. child.stderr.setEncoding('utf8')
  62. child.stdout.on('data', (chunk: string) => {
  63. stdout += chunk
  64. if (!settled && /dsh web: http:\/\/127\.0\.0\.1:\d+/u.test(stdout)) {
  65. settled = true
  66. child.kill('SIGTERM')
  67. }
  68. })
  69. child.stderr.on('data', (chunk: string) => { stderr += chunk })
  70. const timer = setTimeout(() => {
  71. child.kill('SIGKILL')
  72. rejectRun(new Error(`built Web CLI did not settle and dispose within 60s\nstdout:\n${stdout}\nstderr:\n${stderr}`))
  73. }, 60_000)
  74. child.on('error', (error) => {
  75. clearTimeout(timer)
  76. rejectRun(error)
  77. })
  78. child.on('close', (code) => {
  79. clearTimeout(timer)
  80. if (!settled) {
  81. rejectRun(new Error(`built Web CLI exited before settled startup (code ${String(code)})\nstdout:\n${stdout}\nstderr:\n${stderr}`))
  82. return
  83. }
  84. resolveRun({ stdout, stderr, code: code ?? -1 })
  85. })
  86. })
  87. }
  88. describe.skipIf(!requireBuiltArtifacts)('built CLI lazy-search startup', () => {
  89. it('boots and disposes the shipped composition without a SQLite startup warning', async () => {
  90. expect(existsSync(builtBin), `missing built CLI ${resolve(builtBin)}; run pnpm build`).toBe(true)
  91. expect(existsSync(webDist), `missing Web dist ${resolve(webDist)}; run pnpm run build:web`).toBe(true)
  92. const rows = yaml.load(await readFile(configPath, 'utf8'), { schema: configSchema }) as ConfigRow[]
  93. const searchRow = rows.find(row => row.id === 'session-query-sqlite')
  94. expect(searchRow?.config?.openAt).toBe('first-search')
  95. const cwd = await mkdtemp(join(tmpdir(), 'dsh-cli-lazy-search-'))
  96. try {
  97. const result = await runBuiltWeb(cwd)
  98. expect(result.stdout).toMatch(/dsh web: http:\/\/127\.0\.0\.1:\d+/u)
  99. expect(result.code).toBe(0)
  100. expect(result.stderr).not.toMatch(/ExperimentalWarning: SQLite/u)
  101. } finally {
  102. await rm(cwd, { recursive: true, force: true })
  103. }
  104. }, 70_000)
  105. })