Ver código fonte

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.
Chinesezjc 3 semanas atrás
pai
commit
c20cfe77c6
1 arquivos alterados com 30 adições e 26 exclusões
  1. 30 26
      apps/cli/tests/built-bin.e2e.ts

+ 30 - 26
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<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)
   })
 })