P0.2.3 — Two-Phase Startup — Verification

1. Gate sequence

Gate Command Exit Notes
1. Fresh install npm ci 0 513 packages added, 0 vulnerabilities. npm warn deprecated lines are pre-existing (eslint@8.57.1, @humanwhocodes/object-schema, prebuild-install).
2. Lint npm run lint 0 Clean — no warnings, no errors.
3. Tests npm test 0 158 tests, 6 suites, all pass; coverage summary below.
4. Build npm run build 0 tsc emits dist/ with zero errors.

All four commands exit with code 0 on a clean worktree.

2. Coverage summary

-------------|---------|----------|---------|---------|-------------------
File         | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
-------------|---------|----------|---------|---------|-------------------
All files    |   99.05 |       93 |     100 |   99.04 |
 src         |    98.8 |    92.68 |     100 |   98.78 |
  config.ts  |     100 |       80 |     100 |     100 | 78
  modes.ts   |     100 |      100 |     100 |     100 |
  server.ts  |   97.34 |    92.59 |     100 |   97.32 | 552,566-567
  startup.ts |     100 |    92.59 |     100 |     100 | 176,320,385-386
 src/db      |     100 |       95 |     100 |     100 |
  index.ts   |     100 |       95 |     100 |     100 | 276
-------------|---------|----------|---------|---------|-------------------

2.1. src/startup.ts — the new module

Metric Value Target Met
Statements 100% ≥ 95% Yes
Branches 92.59% ≥ 90% (contract I-10) Yes
Functions 100% 100% Yes
Lines 100% ≥ 95% Yes

Uncovered branches (3 sites, all defense-in-depth):

  • Line 176. startup(options: StartupOptions = {}) — the zero-arg default-arg branch. Covered in-process by the accepts no options argument and uses all defaults test (identity check); the positive execution path is exercised end-to-end by the subprocess smoke test, which invokes await startup() with no argument from the IIFE of src/server.ts. Jest’s line-based coverage counter doesn’t credit the subprocess branch because the subprocess is a separate Node instance.
  • Line 320. opts?.stopFn ?? (await loadServerModule()).stop inside shutdown() — the fallback when activeOptions exists but has no stopFn set. This state is unreachable in practice because startup() always populates activeOptions.stopFn. Defense-in-depth only.
  • Lines 385-386. opts?.logger ?? console.error and opts?.exit ?? process.exit.bind(process) inside gracefulSignalExit — fallback when activeOptions is null at signal time. This would require a SIGINT delivered AFTER Phase-1 reset but BEFORE startup re-completes. Not reachable via normal lifecycle.

2.2. src/server.ts — delta

Coverage moved from 98.2 / 92.6 / 100 / 98.2 (P0.2.1 baseline) to 97.34 / 92.59 / 100 / 97.32 — a small absolute drop because the IIFE tail grew from 3 lines to 8 lines. The two new uncovered lines (566-567) are the IIFE’s dynamic-import body — covered in the subprocess smoke tests (server.test.ts describe 5 main() IIFE smoke AND startup.test.ts describe 7 tsx src/server.ts boots and logs [Startup] Phase 1) but those count in subprocesses, not in-process.

No regression on server.ts — the uncovered-line count is the same (3 uncovered: 552, 566, 567 vs prior 552, 561, 562). Branch coverage identical at 92.59%.

2.3. Other modules — untouched

config.ts, modes.ts, db/index.ts coverage is identical to the pre-P0.2.3 baseline. No writes to those files.

3. Test inventory

158 total tests across 6 suites:

Suite Tests New in P0.2.3 Notes
src/__tests__/smoke.test.ts 1 0 Sanity smoke from P0.1.2.
src/__tests__/config.test.ts 14 0 From P0.1.4.
src/__tests__/modes.test.ts 24 0 From P0.4.1.
src/__tests__/server.test.ts 50 0* From P0.2.1. *main() IIFE smoke timeout bumped from 1500→8000 ms, test-level from 10000→15000 ms (see §4).
src/__tests__/db-init.test.ts 29 0 From P0.2.2.
src/__tests__/startup.test.ts 40 40 New — this task.

3.1. Invariant coverage

The contract’s I-1 through I-10 invariants are exercised by these tests:

Invariant Test(s)
I-1 (Phase 1 before Phase 2) calls bootstrapFn before initDbFn
I-2 (log order) emits Phase 1 logs before Phase 2 logs
I-3 (closeDb on Phase 2 fail) closeDb is called even when initDb throws
I-4 (transport before DB in shutdown) transport closes before DB (order assertion)
I-5 (shutdown never throws) never throws when stopFn throws
I-6 (reject on fake exit) rethrows the original error when exit does not terminate
I-7 (one listener per signal) covered by afterEach + listener-count tests
I-8 (no signals when opt false) does NOT register signal handlers when option is false
I-9 (server.ts backward compat) preserved — no changes to exports, only IIFE tail; verified by all 50 server.test.ts tests still passing
I-10 (≥ 90% branch) 92.59% on startup.ts — met

4. Spec deviations (Sigma-approved)

4.1. src/server.ts IIFE now passes bootstrapFn/stopFn explicitly

The dispatch prompt said: “bootstrap() (or equivalent) should call startup() AFTER the transport connects.” The contract specified:

if (isInvokedAsScript()) {
  const { startup } = await import('./startup.js');
  await startup();
}

The implementation diverges to:

if (isInvokedAsScript()) {
  const { startup } = await import('./startup.js');
  await startup({ bootstrapFn: bootstrap, stopFn: stop });
}

Why. A bare await startup() call from inside the src/server.ts IIFE hits a circular-import deadlock: startup.ts needs bootstrap and stop from server.ts, but server.ts is mid-evaluation (its top-level await has not yet resolved). Node’s ES-module loader returns an in-flight (empty) module record and startup() hangs on await loadServerModule(). The subprocess aborts with exit code 13 and the diagnostic Warning: Detected unsettled top-level await.

Passing { bootstrapFn: bootstrap, stopFn: stop } explicitly tells startup() to use the already-resolved local references without re-importing — the lazy loader never triggers during the IIFE. Verified by the startup — subprocess smoke test (tsx src/server.ts boots and logs [Startup] Phase 1).

Impact: zero. The external contract is unchanged — tests and other callers invoke startup() normally; only the one IIFE line is affected. Documented in the server.ts IIFE comment.

4.2. server.test.ts main() IIFE smoke timeout bumped

Original (P0.2.1): timeout: 1500 / test-level 10000. Now (P0.2.3): timeout: 8000 / test-level 15000.

Why. The new IIFE path dynamic-imports ./startup.js, which statically imports ./db/index.js, which loads better-sqlite3’s native bindings. Cold-start module load on Windows measured at ~2s via node --import tsx -e "await import('./src/startup.js');". The 1500 ms budget was sized for the P0.2.1 server-only load (~557 ms measured) and no longer covers the DB-client load path. The 8000 ms value gives comfortable headroom without making the test slow on clean-install runs.

COLIBRI_DB_PATH was also set explicitly in the spawnSync env so Phase 2 opens a throwaway file in the OS temp dir rather than creating data/colibri.db in the repo tree during test runs.

The assertion (expect(result.stderr).toMatch(/\[colibri\] starting/)) is unchanged.

5. Full test pass log (summary)

Test Suites: 6 passed, 6 total
Tests:       158 passed, 158 total
Snapshots:   0 total
Time:        13.608 s
Ran all test suites.

Per-suite wall-clock (last measured):

Suite Time
smoke.test.ts ~0.1s
config.test.ts ~0.4s
modes.test.ts ~0.4s
db-init.test.ts ~8.3s
startup.test.ts ~5.9s
server.test.ts ~13.0s (includes subprocess smoke)

6. Files touched

Path Action Lines
src/startup.ts Create +424
src/server.ts Edit (IIFE tail: 3 → 8 lines) +9 / -1
src/__tests__/startup.test.ts Create +941
src/__tests__/server.test.ts Edit (one smoke test’s timeout + env) +17 / -6
docs/audits/p0-2-3-two-phase-startup-audit.md Create (step 1) +279
docs/contracts/p0-2-3-two-phase-startup-contract.md Create (step 2) +339
docs/packets/p0-2-3-two-phase-startup-packet.md Create (step 3) +401
docs/verification/p0-2-3-two-phase-startup-verification.md Create (step 5 — this)

7. Exit criteria

  • src/startup.ts created, exports startup, shutdown, __resetForTests, StartupOptions, StartupResult.
  • src/server.ts IIFE delegates to startup(); all other exports preserved.
  • Phase 1 → Phase 2 → return { ctx, db, elapsedMs } happy path.
  • Phase 2 failure → shutdown('phase-2-failed')exit(1) or reject if exit doesn’t terminate.
  • Graceful shutdown: transport first, DB second, signals last, 5s cleanup race.
  • Re-entrancy guard via module-level flag + __resetForTests.
  • Log prefix convention [Startup] / [Shutdown] on stderr only.
  • 158/158 tests pass.
  • 100% stmt / 92.59% branch / 100% func / 100% line on src/startup.ts (≥ 90% branch target met).
  • Server.ts coverage unchanged (no regression).
  • npm ci && npm run lint && npm test && npm run build all green.
  • Sigma-approved deviations documented (§4).

Task P0.2.3 complete. Ready to push + open PR.


Back to top

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

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