From c20cfe77c60e9277ad55fc9db8e93d0b3dcf37a2 Mon Sep 17 00:00:00 2001 From: Chinesezjc Date: Wed, 26 Aug 2026 16:13:46 +0800 Subject: [PATCH] test: align built-bin spawn budget with its outer case budgets The execa timeout was widened to 60s but the outer vitest case budgets stayed at 30s, so a cold-starting built bin would trip the vitest budget first and the execa SIGKILL cleanup could not run inside it. Extract SPAWN_TIMEOUT_MS, share it across the execa deadline, its error text, waitForFile, and the outer case budgets (60s spawn + 30s headroom), so the widening is coherent. --- apps/cli/tests/built-bin.e2e.ts | 56 ++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 26 deletions(-) diff --git a/apps/cli/tests/built-bin.e2e.ts b/apps/cli/tests/built-bin.e2e.ts index 98008cf006..c133320ace 100644 --- a/apps/cli/tests/built-bin.e2e.ts +++ b/apps/cli/tests/built-bin.e2e.ts @@ -19,6 +19,10 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest' /** Published-entry acceptance for argument errors, profile lifecycle, and boot-free config dumps. */ const repoRoot = fileURLToPath(new URL('../../../', import.meta.url)) +// The dsh built bin cold-starts slowly on the contended self-hosted Windows pool; the +// execa deadline, its error text, the outer vitest case budget, and waitForFile all +// share this value so a widening cannot leave a stale 25s diagnostic behind. +const SPAWN_TIMEOUT_MS = 60_000 // The release version, including a prerelease such as 0.0.1-rc.1: `--version` // prints what this manifest carries, so no test may pin it to a literal. const cliVersion = (JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8')) as { version: string }).version @@ -36,7 +40,7 @@ async function runBuiltBin( ) const result = await execa(process.execPath, [dshBin, ...args], { input: '', - timeout: 60_000, + timeout: SPAWN_TIMEOUT_MS, killSignal: 'SIGKILL', reject: false, env: childEnv, @@ -44,13 +48,13 @@ async function runBuiltBin( ...cwd === undefined ? {} : { cwd }, }) if (result.timedOut) { - throw new Error(`dsh built bin did not exit within 25s. stdout:\n${result.stdout}\nstderr:\n${result.stderr}`) + throw new Error(`dsh built bin did not exit within ${SPAWN_TIMEOUT_MS / 1_000}s. stdout:\n${result.stdout}\nstderr:\n${result.stderr}`) } return { stdout: result.stdout, code: result.exitCode ?? -1, stderr: result.stderr } } async function waitForFile(file: string): Promise { - const deadline = Date.now() + 20_000 + const deadline = Date.now() + SPAWN_TIMEOUT_MS while (!existsSync(file)) { if (Date.now() >= deadline) throw new Error(`dsh profile lifecycle marker did not appear: ${file}`) await new Promise(resolve => setTimeout(resolve, 20)) @@ -310,7 +314,7 @@ function startStartupProfile(fixture: StartupFixture, args: readonly string[]) { cwd: fixture.home, input: '', reject: false, - timeout: 60_000, + timeout: SPAWN_TIMEOUT_MS, killSignal: 'SIGKILL', env: { DSH_HOME: fixture.home, @@ -335,7 +339,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', const result = await runBuiltBin(removed) expect(result.code).toBe(1) } - }, 30_000) + }, SPAWN_TIMEOUT_MS + 30_000) it('routes help and usage errors without activating startup-dependent rows', async () => { const home = mkdtempSync(join(tmpdir(), 'dsh-app-help-')) @@ -416,14 +420,14 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', } finally { rmSync(home, { recursive: true, force: true }) } - }, 30_000) + }, SPAWN_TIMEOUT_MS + 30_000) it('serves the SDK protocol through the sdk profile and exits after shutdown', async () => { const home = mkdtempSync(join(tmpdir(), 'dsh-built-sdk-')) const child = execa(process.execPath, [dshBin, '--profile', 'sdk'], { cwd: home, reject: false, - timeout: 60_000, + timeout: SPAWN_TIMEOUT_MS, killSignal: 'SIGKILL', env: { ...process.env, @@ -471,7 +475,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', await child rmSync(home, { recursive: true, force: true }) } - }, 30_000) + }, SPAWN_TIMEOUT_MS + 30_000) it('runs a mock-backed ACP turn through the acp profile and exits on disconnect', async () => { const apiKey = 'built-acp-profile-key' @@ -484,7 +488,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', const child = execa(process.execPath, [dshBin, '--profile', 'acp'], { cwd: home, reject: false, - timeout: 60_000, + timeout: SPAWN_TIMEOUT_MS, killSignal: 'SIGKILL', env: { ...process.env, @@ -555,7 +559,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', await server.close() rmSync(home, { recursive: true, force: true }) } - }, 30_000) + }, SPAWN_TIMEOUT_MS + 30_000) it('runs the headless profile through its app-owned task positional', async () => { const apiKey = 'built-dsh-headless-key' @@ -583,7 +587,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', await server.close() rmSync(home, { recursive: true, force: true }) } - }, 30_000) + }, SPAWN_TIMEOUT_MS + 30_000) it('does not load a project environment for --version', async () => { const project = mkdtempSync(join(tmpdir(), 'dsh-version-project-')) @@ -606,7 +610,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', } finally { rmSync(home, { recursive: true, force: true }) } - }, 30_000) + }, SPAWN_TIMEOUT_MS + 30_000) it('uses the launching endpoint and managed credential through the published entry', async () => { const apiKey = 'built-home-layer-key' @@ -646,7 +650,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', rmSync(home, { recursive: true, force: true }) rmSync(project, { recursive: true, force: true }) } - }, 30_000) + }, SPAWN_TIMEOUT_MS + 30_000) it('reports a patch-overlay boot failure without hanging', async () => { // The HMR main watcher's initial scan once refreshed the include @@ -666,7 +670,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', } finally { rmSync(home, { recursive: true, force: true }) } - }, 30_000) + }, SPAWN_TIMEOUT_MS + 30_000) it('lets a profile without a parser ignore app arguments and dispose on a startup-time signal', async () => { const fixture = createProfileLifecycleFixture() @@ -682,7 +686,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', child.kill('SIGKILL') rmSync(fixture.home, { recursive: true, force: true }) } - }, 30_000) + }, SPAWN_TIMEOUT_MS + 30_000) it('fully settles a custom profile, hot-reloads its patch layer with removal reverting, and disposes on a signal', async () => { const fixture = createProfileLifecycleFixture() @@ -734,7 +738,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', child.kill('SIGKILL') rmSync(fixture.home, { recursive: true, force: true }) } - }, 30_000) + }, SPAWN_TIMEOUT_MS + 30_000) it('hands the app arguments to the profile, which applies them before its rows start', async () => { const fixture = createStartupFixture() @@ -750,7 +754,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', child.kill('SIGKILL') rmSync(fixture.home, { recursive: true, force: true }) } - }, 30_000) + }, SPAWN_TIMEOUT_MS + 30_000) it('starts a consumer on its composed value when the invocation carries no app arguments', async () => { const fixture = createStartupFixture() @@ -764,7 +768,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', child.kill('SIGKILL') rmSync(fixture.home, { recursive: true, force: true }) } - }, 30_000) + }, SPAWN_TIMEOUT_MS + 30_000) it('keeps the app arguments across a user patch reload', async () => { // A live edit recomposes every row while the provider service remains @@ -798,7 +802,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', child.kill('SIGKILL') rmSync(fixture.home, { recursive: true, force: true }) } - }, 30_000) + }, SPAWN_TIMEOUT_MS + 30_000) it("prints the app's own help, starts none of its rows, and exits", async () => { const fixture = createStartupFixture() @@ -811,7 +815,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', } finally { rmSync(fixture.home, { recursive: true, force: true }) } - }, 30_000) + }, SPAWN_TIMEOUT_MS + 30_000) it('anchors a relative add spec to the invoking directory, not the profile', async () => { // `dsh plugin --profile x add .` from a plugin checkout must install THAT @@ -829,7 +833,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', const result = await execa(process.execPath, [dshBin, 'plugin', '--profile', 'anchor', 'add', '.'], { cwd: checkout, input: '', - timeout: 60_000, + timeout: SPAWN_TIMEOUT_MS, killSignal: 'SIGKILL', reject: false, env: { DSH_HOME: home }, @@ -897,7 +901,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', } finally { rmSync(home, { recursive: true, force: true }) } - }, 30_000) + }, SPAWN_TIMEOUT_MS + 30_000) describe('config dump', () => { let home: string @@ -913,7 +917,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', expect(stdout).toContain('# == @deepseek-ai/dsh-base') expect(stdout).toContain("name: '@deepseek-ai/dsh-host-webserver'") expect(existsSync(join(home, 'profiles', 'node_modules'))).toBe(false) - }, 30_000) + }, SPAWN_TIMEOUT_MS + 30_000) it('prints the headless profile without Host or browser layers', async () => { const { stdout, code, stderr } = await runBuiltBin( @@ -926,7 +930,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', expect(stdout).not.toMatch(/name: '@deepseek-ai\/dsh-host-/) expect(stdout).not.toContain("name: '@deepseek-ai/dsh-web-app'") expect(stdout).not.toMatch(/name: '@deepseek-ai\/dsh-client-/) - }, 30_000) + }, SPAWN_TIMEOUT_MS + 30_000) it('prints the exact standalone sdk-minimal tree without dsh-base', async () => { const { stdout, code, stderr } = await runBuiltBin( @@ -959,7 +963,7 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', expect(stdout).toContain('# == @deepseek-ai/dsh-sdk-minimal') expect(stdout).not.toContain('@deepseek-ai/dsh-base') expect(stdout).not.toContain('@deepseek-ai/dsh-web-app') - }, 30_000) + }, SPAWN_TIMEOUT_MS + 30_000) it('composes the profile user layer and a --patch overlay in order', async () => { // Auto-init the web profile first, then write its user layer. @@ -998,6 +1002,6 @@ describe.skipIf(!existsSync(dshBin))('dsh BUILT bin (node lib/bin.js, no tsx)', // Both layers patched the row; the comment lists them in application order. expect(stdout).toContain(`patched by ${profilePatch}, ${overlay}`) expect(stderr).toContain('patch: entry "absent-row" not found') - }, 30_000) + }, SPAWN_TIMEOUT_MS + 30_000) }) })