Packet — debug-startup-smoke-flake

Step 3 of the 5-step executor chain. Execution plan that gates Step 4 (implement). All changes are confined to src/__tests__/startup.test.ts.

Files touched

  • src/__tests__/startup.test.ts — only file edited. No production source modified.

Edit plan

1. Add typed constants near the top of describe 'startup — subprocess smoke'

Just before the it(...) block at :767, introduce:

/**
 * Cold-start budget for `tsx src/server.ts` running as a subprocess.
 *
 * The standalone cold-start measurement on Windows + Node 22 is ~4.2 s
 * (see docs/audits/debug-startup-smoke-flake-audit.md). Under full-suite
 * parallel Jest load (~7 workers), that easily inflates 5–8×, so the
 * spawnSync timeout MUST be wide enough to cover the worst-case cold
 * compile + AV scan + worker contention without blowing past the Jest
 * test timeout.
 *
 * The budget is named so a future regression can grep for it.
 */
const STARTUP_SUBPROCESS_TIMEOUT_MS = 30_000;
const STARTUP_SUBPROCESS_TEST_TIMEOUT_MS = STARTUP_SUBPROCESS_TIMEOUT_MS + 5_000;

2. Replace the magic numbers

At :796 change timeout: 3000, to timeout: STARTUP_SUBPROCESS_TIMEOUT_MS,.

At :805 change the trailing }, 15000); to }, STARTUP_SUBPROCESS_TEST_TIMEOUT_MS);.

3. Enrich the diagnostic on failure

Replace the two bare expect(stderr).toMatch(...) lines with assertions that include the full subprocess outcome in their failure message. Concretely:

const stderr = result.stderr ?? '';
const stdout = result.stdout ?? '';
const diagnostic = [
  `subprocess outcome:`,
  `  status: ${String(result.status)}`,
  `  signal: ${String(result.signal)}`,
  `  error:  ${result.error?.message ?? '(none)'}`,
  `  stdout (${stdout.length} bytes): ${stdout.slice(0, 2_000)}`,
  `  stderr (${stderr.length} bytes): ${stderr.slice(0, 4_000)}`,
].join('\n');

// The server still emits [colibri] starting from P0.2.1's start().
expect(stderr, diagnostic).toMatch(/\[colibri\] starting/);
// And now Phase 1 log from P0.2.3.
expect(stderr, diagnostic).toMatch(/\[Startup\] Phase 1/);

Compatibility check. Jest’s expect(...).toMatch does not accept a custom message argument the way Vitest does. The Jest-native idiom is expect(stderr).toMatch(/.../) and rely on Jest’s own diff output. To attach a diagnostic we use a different shape: a wrapper assertion with a try/catch that re-throws an enriched error, OR conditionally log the diagnostic via console.error BEFORE the assertion. The simpler, lint-friendly path is the latter — emit the diagnostic via Jest’s own reporter only when a match would fail.

Picked: log the diagnostic to stderr (visible in the Jest failure output) iff a match is going to fail, then assert. Implementation:

if (!/\[colibri\] starting/.test(stderr) || !/\[Startup\] Phase 1/.test(stderr)) {
  // eslint-disable-next-line no-console
  console.error(diagnostic);
}
expect(stderr).toMatch(/\[colibri\] starting/);
expect(stderr).toMatch(/\[Startup\] Phase 1/);

This keeps the test output bare on the green path (no diagnostic noise) and rich on the red path (full subprocess outcome + truncated stderr/stdout).

4. No other edits

  • The two existing expect(stderr).toMatch(...) regexes are unchanged. They remain the canonical signals.
  • The clean-env loop, the in-memory tmp DB path, and the input: '' are unchanged.
  • No production code is touched.
  • No other test in this file is touched.

Risk assessment

Risk Likelihood Impact Mitigation
30 s timeout is still too tight Low Test re-flakes at lower rate Future bump to 60 s is a one-line change with an obvious anchor (the named constant).
30 s timeout masks a real production hang Low A real bug stays hidden longer The diagnostic-enrichment ensures the failure message tells the operator what the subprocess was doing — a hung child shows signal: SIGTERM + a partial stderr that diverges from the warm baseline. Future readers can compare.
Wider Jest timeout slows the suite None Only one test uses the wider timeout; the per-suite total wall time is unaffected on green runs (which is every run, post-fix).
console.error in tests breaks lint Low Lint gate fails The eslint-disable comment is scoped to one line. The lint config already permits console.error in test files (per existing usage of logger: () => undefined and logger: console.error in this very file).

Acceptance check (drives Step 5)

After the fix lands:

  1. npm run build — green.
  2. npm run lint — green.
  3. npm test × 5 sequential — 5/5 green; the subprocess-smoke test passes every time.

If any of those three fail, the fix is rejected and the implementation step iterates.

Files NOT touched

Per task constraints:

  • src/server.ts — no.
  • src/startup.ts — no.
  • src/db/index.ts — no.
  • jest.config.ts — no. (No --runInBand annotation; the goal is to make the test work without serialization.)
  • package.json — no.
  • All other tests — no.

Back to top

Colibri — documentation-first MCP runtime. Apache 2.0 + Commons Clause.

This site uses Just the Docs, a documentation theme for Jekyll.