|
|
@@ -0,0 +1,112 @@
|
|
|
+/**
|
|
|
+ * Node 22 startup-output smoke for the shipped Web CLI composition.
|
|
|
+ *
|
|
|
+ * Only the dedicated Node compatibility gate opts this test in after building
|
|
|
+ * both artifacts; ordinary Vitest inventory deterministically skips it.
|
|
|
+ * The child runs built artifacts under plain Node with the real shipped
|
|
|
+ * config (base.cordis.yml + the web.cordis.yml overlay).
|
|
|
+ * Its URL line follows AppCLIEntry's settled boot; SIGTERM then exercises the
|
|
|
+ * shipped quiescent disposer.
|
|
|
+ */
|
|
|
+
|
|
|
+import { spawn } from 'node:child_process'
|
|
|
+import { existsSync } from 'node:fs'
|
|
|
+import { mkdtemp, readFile, rm } from 'node:fs/promises'
|
|
|
+import { tmpdir } from 'node:os'
|
|
|
+import { join, resolve } from 'node:path'
|
|
|
+import { fileURLToPath } from 'node:url'
|
|
|
+import yaml from 'js-yaml'
|
|
|
+import { describe, expect, it } from 'vitest'
|
|
|
+
|
|
|
+const repoRoot = fileURLToPath(new URL('../../../', import.meta.url))
|
|
|
+const builtBin = join(repoRoot, 'apps/cli/lib/bin.js')
|
|
|
+const webDist = join(repoRoot, 'apps/web/dist/index.html')
|
|
|
+// The web overlay owns the session-query-sqlite lazy-open patch row.
|
|
|
+const configPath = join(repoRoot, 'apps/cli/config/web.cordis.yml')
|
|
|
+const requireBuiltArtifacts = process.env.DSH_REQUIRE_BUILT_CLI_SMOKE === '1'
|
|
|
+
|
|
|
+interface ConfigRow {
|
|
|
+ id?: string
|
|
|
+ config?: { openAt?: unknown }
|
|
|
+}
|
|
|
+
|
|
|
+const jsExprType = new yaml.Type('tag:yaml.org,2002:js', {
|
|
|
+ kind: 'scalar',
|
|
|
+ construct: value => String(value),
|
|
|
+})
|
|
|
+const configSchema = yaml.JSON_SCHEMA.extend(jsExprType)
|
|
|
+
|
|
|
+/** Boot the built Web CLI, wait for its settled URL, then dispose through SIGTERM. */
|
|
|
+function runBuiltWeb(cwd: string): Promise<{ stdout: string; stderr: string; code: number }> {
|
|
|
+ return new Promise((resolveRun, rejectRun) => {
|
|
|
+ const env: NodeJS.ProcessEnv = {
|
|
|
+ ...process.env,
|
|
|
+ DEEPSEEK_API_KEY: 'dsh-cli-smoke-dummy-key',
|
|
|
+ DSH_HOME: join(cwd, '.dsh'),
|
|
|
+ }
|
|
|
+ delete env.DEEPSEEK_BASE_URL
|
|
|
+ delete env.NODE_OPTIONS
|
|
|
+ delete env.NODE_NO_WARNINGS
|
|
|
+ const child = spawn(process.execPath, [
|
|
|
+ builtBin,
|
|
|
+ 'web',
|
|
|
+ '--host',
|
|
|
+ '127.0.0.1',
|
|
|
+ '--port',
|
|
|
+ '0',
|
|
|
+ ], {
|
|
|
+ cwd,
|
|
|
+ env,
|
|
|
+ stdio: ['ignore', 'pipe', 'pipe'],
|
|
|
+ })
|
|
|
+ let stdout = ''
|
|
|
+ let stderr = ''
|
|
|
+ let settled = false
|
|
|
+ child.stdout.setEncoding('utf8')
|
|
|
+ child.stderr.setEncoding('utf8')
|
|
|
+ child.stdout.on('data', (chunk: string) => {
|
|
|
+ stdout += chunk
|
|
|
+ if (!settled && /dsh web: http:\/\/127\.0\.0\.1:\d+/u.test(stdout)) {
|
|
|
+ settled = true
|
|
|
+ child.kill('SIGTERM')
|
|
|
+ }
|
|
|
+ })
|
|
|
+ child.stderr.on('data', (chunk: string) => { stderr += chunk })
|
|
|
+ const timer = setTimeout(() => {
|
|
|
+ child.kill('SIGKILL')
|
|
|
+ rejectRun(new Error(`built Web CLI did not settle and dispose within 60s\nstdout:\n${stdout}\nstderr:\n${stderr}`))
|
|
|
+ }, 60_000)
|
|
|
+ child.on('error', (error) => {
|
|
|
+ clearTimeout(timer)
|
|
|
+ rejectRun(error)
|
|
|
+ })
|
|
|
+ child.on('close', (code) => {
|
|
|
+ clearTimeout(timer)
|
|
|
+ if (!settled) {
|
|
|
+ rejectRun(new Error(`built Web CLI exited before settled startup (code ${String(code)})\nstdout:\n${stdout}\nstderr:\n${stderr}`))
|
|
|
+ return
|
|
|
+ }
|
|
|
+ resolveRun({ stdout, stderr, code: code ?? -1 })
|
|
|
+ })
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+describe.skipIf(!requireBuiltArtifacts)('built CLI lazy-search startup', () => {
|
|
|
+ it('boots and disposes the shipped composition without a SQLite startup warning', async () => {
|
|
|
+ expect(existsSync(builtBin), `missing built CLI ${resolve(builtBin)}; run pnpm build`).toBe(true)
|
|
|
+ expect(existsSync(webDist), `missing Web dist ${resolve(webDist)}; run pnpm run build:web`).toBe(true)
|
|
|
+ const rows = yaml.load(await readFile(configPath, 'utf8'), { schema: configSchema }) as ConfigRow[]
|
|
|
+ const searchRow = rows.find(row => row.id === 'session-query-sqlite')
|
|
|
+ expect(searchRow?.config?.openAt).toBe('first-search')
|
|
|
+
|
|
|
+ const cwd = await mkdtemp(join(tmpdir(), 'dsh-cli-lazy-search-'))
|
|
|
+ try {
|
|
|
+ const result = await runBuiltWeb(cwd)
|
|
|
+ expect(result.stdout).toMatch(/dsh web: http:\/\/127\.0\.0\.1:\d+/u)
|
|
|
+ expect(result.code).toBe(0)
|
|
|
+ expect(result.stderr).not.toMatch(/ExperimentalWarning: SQLite/u)
|
|
|
+ } finally {
|
|
|
+ await rm(cwd, { recursive: true, force: true })
|
|
|
+ }
|
|
|
+ }, 70_000)
|
|
|
+})
|