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.
This commit is contained in:
Chinesezjc
2026-08-26 16:39:30 +08:00
parent e9cb003e9e
commit c20cfe77c6
+30 -26
View File
@@ -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<void> {
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)
})
})