P0.4.2 Graceful Shutdown — Verification
1. Path chosen
Path (c) — Augment inline + barrel. All implementation logic lives in src/startup.ts
alongside the existing transport + DB lifecycle machinery (Wave A inline lock). src/shutdown.ts
is a re-export-only barrel providing a stable public surface.
Rationale documented in docs/audits/p0-4-2-graceful-shutdown-audit.md §4.
2. Gate evidence
2.1. Test run
Test Suites: 14 passed, 14 total
Tests: 691 passed, 691 total
Snapshots: 0 total
Time: ~30s
2.2. New shutdown tests (14 tests)
| # | Test | Describe block |
|---|---|---|
| 1 | registers a handler that is called on shutdown | registration |
| 2 | returns a callable deregister function | registration |
| 3 | deregister removes the handler — not called after deregister | registration |
| 4 | registering the same fn twice calls it twice | registration |
| 5 | two handlers: B (registered last) runs before A | LIFO ordering |
| 6 | three handlers [A, B, C]: shutdown calls in order C, B, A | LIFO ordering |
| 7 | async handlers are awaited sequentially preserving LIFO order | LIFO ordering |
| 8 | throwing handler does not abort remaining handlers | error isolation |
| 9 | throwing handler logs [Shutdown] handler[i] failed: | error isolation |
| 10 | shutdown() never throws even when a handler throws | error isolation |
| 11 | SIGTERM triggers registered handler + exit(0) | signal integration |
| 12 | SIGINT triggers registered handler + exit(0) | signal integration |
| 13 | idempotency: handler called exactly once when shutdown() called twice | signal integration |
| 14 | clearShutdownHandlers resets the list — no handler invoked | signal integration |
All 14 tests: PASS
2.3. Existing startup.test.ts tests
The 950-line src/__tests__/startup.test.ts was not modified. Its 37 tests continue to
pass in the full suite run. No regressions.
2.4. Coverage
startup.ts | 100 | 93.22 | 100 | 100 | 193,357,434-435
- Uncovered branches (193, 357, 434-435): pre-existing null-coalescing fallbacks
(
options = {},loadServerModule().stopdefault,opts?.exitdefault). These branches were uncovered before P0.4.2 and are not introduced by this task. src/shutdown.ts: pure re-export barrel — no executable logic, 100% trivially.
2.5. Lint
npm run lint → exit 0 (no warnings, no errors)
2.6. Build
npm run build (tsc) → exit 0 (no TypeScript errors)
3. Acceptance criteria check
| AC | Status |
|---|---|
registerShutdownHandler(fn) registers a cleanup function |
DONE — src/startup.ts exports registerShutdownHandler; re-exported from src/shutdown.ts |
| SIGINT/SIGTERM: handlers called in reverse registration order | DONE — LIFO loop in shutdown() step 4; tests 5–7 verify |
| DB connection closed before process exit | DONE (pre-existing P0.2.3 feature) — closeDbFn() in step 2 of shutdown() |
| In-flight MCP requests allowed to complete (max 5s timeout) | DONE (pre-existing P0.2.3 feature) — Promise.race in step 1 of shutdown() |
| Exit code 0 clean shutdown, 1 on error | DONE (pre-existing P0.2.3 feature) — gracefulSignalExit returns exit(0) / exit(1) |
src/shutdown.ts module exists |
DONE — created as re-export barrel |
| Test: mock SIGTERM → verify DB close + handler called | DONE — tests 11, 12 cover signal → handler; DB close covered by startup.test.ts |
All 7 acceptance criteria: SATISFIED
4. Files changed
| File | Action | Lines |
|---|---|---|
src/startup.ts |
Modified | +74 lines (handler list, registerShutdownHandler, clearShutdownHandlers, step 4 in shutdown(), clearShutdownHandlers() call in __resetForTests) |
src/shutdown.ts |
Created | 29 lines (barrel re-export) |
src/__tests__/shutdown.test.ts |
Created | ~250 lines (14 tests, 4 describe blocks) |
docs/audits/p0-4-2-graceful-shutdown-audit.md |
Created | 182 lines |
docs/contracts/p0-4-2-graceful-shutdown-contract.md |
Created | 186 lines |
docs/packets/p0-4-2-graceful-shutdown-packet.md |
Created | 190 lines |
docs/verification/p0-4-2-graceful-shutdown-verification.md |
Created | this file |
5. Commit history
| SHA | Step | Message |
|---|---|---|
a3866373 |
1. Audit | audit(p0-4-2-graceful-shutdown): inventory existing inline shutdown + decide path |
84e5c8a9 |
2. Contract | contract(p0-4-2-graceful-shutdown): handler API + signal semantics + exit codes |
9295ede9 |
3. Packet | packet(p0-4-2-graceful-shutdown): execution plan |
1a3c6685 |
4. Implement | feat(p0-4-2-graceful-shutdown): shutdown handlers + 5s timeout + exit codes |
| (this commit) | 5. Verify | verify(p0-4-2-graceful-shutdown): tests + gate evidence |
6. Residual risks
| Risk | Assessment |
|---|---|
| Handler list is module-scoped | Acceptable: signal handlers are inherently process-scoped; test hermeticism ensured by clearShutdownHandlers() in __resetForTests() |
| Late-registered handlers during shutdown not invoked | By design (snapshot before LIFO loop); documented in contract §3.1 |
Barrel re-export is .startup.js runtime path |
ESM .js extension is correct for TypeScript ESM (resolved by ts path mapping) |
node_modules symlink in worktree not tracked by git |
Symlink is in .gitignore via node_modules/ pattern; correct behavior |