# Conflicts: # apps/cli/README.i18n.yaml # apps/cli/README.md # apps/cli/README.zh.md # apps/cli/cordis.yml # apps/cli/package.json # docs/config-catalog.md # packages/client/runtime/README.i18n.yaml # packages/client/runtime/src/client/contract/sessions.ts # packages/client/test-runtime/src/sessions.ts # packages/client/ui-workspace/README.i18n.yaml # packages/client/ui-workspace/README.md # packages/client/ui-workspace/README.zh.md # packages/client/ui-workspace/src/client/WorkspaceBrowser.tsx # packages/client/ui-workspace/src/client/tree.ts # packages/client/ui-workspace/tests/apply.spec.ts # packages/client/ui-workspace/tests/tree.spec.ts # packages/host/apiproxy/README.i18n.yaml # packages/host/apiproxy/src/api-proxy.ts # packages/host/apiproxy/src/api/index.ts # packages/host/apiproxy/tests/client-handler.spec.ts # packages/host/apiproxy/tests/rpc-schemas.spec.ts # pnpm-lock.yaml
113 lines
4.1 KiB
TypeScript
113 lines
4.1 KiB
TypeScript
/**
|
|
* 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)
|
|
})
|