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(...).toMatchdoes not accept a custom message argument the way Vitest does. The Jest-native idiom isexpect(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 viaconsole.errorBEFORE 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:
npm run build— green.npm run lint— green.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--runInBandannotation; the goal is to make the test work without serialization.)package.json— no.- All other tests — no.