test: deterministic remote-server teardown to kill #1667 afterEach hang#1763
Conversation
The remote integration tests close their Hono HTTP server in afterEach with `server.close(cb)`, which waits for all keep-alive sockets to go idle before firing the callback. undici's global-fetch connection pool holds sockets open, and under the full parallel/instrumented `npm run coverage` load they may not go idle before the 30s hook timeout — intermittently failing the dead-transport tests in remote-auth-branches.test.ts. The crash-on-startup / dead-session tests are the worst case: they never disconnect, so the session and its dead subprocess transport linger when the server closes. Force the sockets closed with `server.closeAllConnections()` alongside close() so the callback fires deterministically regardless of parallel load, in both remote-auth-branches.test.ts and its sibling connect-crash.test.ts. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
The transport-onclose path arms a 30s setTimeout to GC a session whose client never opens the events stream. That sweep is best-effort cleanup, not work the process must stay alive for — unref it so a pending timer can't keep the event loop (or a test worker) alive for 30s after everything else has finished. Also narrow closeAllConnections() behind an `in` guard in the two remote teardown helpers: ServerType is Server | Http2Server and the method is http1-only, so the direct call failed tsc. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
|
@claude review |
|
Claude finished @cliffhall's task in 1m 3s —— View job Review: test stability fix for #1667I read the full diff and traced the changed teardown helpers plus the ✅ What's right1. 2. The 3. 🔎 Minor / optional (non-blocking)
VerdictCorrect root-cause fix, minimal blast radius, faithful comments, and it keeps the |
Address review feedback on #1763: the deterministic close()+closeAllConnections teardown (and its multi-line rationale) was duplicated across remote-auth-branches.test.ts and connect-crash.test.ts. Extract it to a shared closeHarnessServer(server) helper so the logic and its comment live in one place. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5
|
Thanks for the thorough review! Responses to the two optional items:
Verified after the refactor: affected tests green (2 files / 11 tests), |
|
@claude review |
|
Claude finished @cliffhall's task in 36s —— View job Re-review: shared teardown helper refactor (#1667)I re-read all four changed files, including the new ✅ What's right1. 2. Ordering is still correct. 3. The 4. 🔎 Minor / optional (non-blocking)
VerdictThe refactor addresses the one actionable item from the prior review without changing the fix's behavior. Correct root-cause fix, faithful comments, one place to maintain the reasoning. Ship it. 👍 |
|
Thanks for the re-review — no actionable items this pass, so I'll leave the PR as-is. Both minor notes (the |
Closes #1667
Problem
The remote integration test
remote-auth-branches.test.ts→ "short-circuits with a transport_error when the session's transport is already dead" intermittently timed out in its 30safterEachunder the fullnpm run coverage/npm run cigate (v8 instrumentation + all projects under parallel load), while passing reliably in isolation.Root cause (issue option 2)
The
afterEachcloses the Hono HTTP server withserver.close(cb). Node'sclose()only stops accepting new connections and then waits for all existing keep-alive sockets to go idle before firing its callback. The globalfetch(undici) keeps its connection pool sockets open, and under heavy parallel/instrumented load they don't reliably go idle before the 30s hook timeout — soclose()'s callback never fires and the hook times out.The dead-transport tests are the worst case: they connect a session whose stdio subprocess crashes and then never disconnect it, so the session (and its dead transport) lingers on the server at teardown.
Fix
Deterministic teardown. Call
server.closeAllConnections()alongsideclose()in the teardown helpers, forcibly destroying the pooled keep-alive sockets so theclose()callback fires regardless of parallel load. Applied to bothremote-auth-branches.test.tsand its siblingconnect-crash.test.ts(same crash-on-startup / never-disconnect pattern). Guarded behind aninnarrow becauseServerTypeisServer | Http2ServerandcloseAllConnectionsis http1-only. Available since Node 18.2; the repo requires Node ≥22.Unref the abandoned-session sweep timer (
core/mcp/remote/node/server.ts). The transport-onclose path arms a 30ssetTimeoutto GC a session whose client never opens the events stream. That sweep is best-effort cleanup, not work the process must stay alive for —unref()it so a pending timer can't keep the event loop (or a test worker) alive for 30s after everything else finishes.Verification
integrationproject: green (49 files / 969 tests).test:coveragegate (unit + integration under v8 instrumentation — the exact context the flake appeared in): green (298 files / 4426 tests);server.tsat 97.9/92.6/97.9/98.4, above the ≥90 gate.🤖 Generated with Claude Code
https://claude.ai/code/session_01XNdjEPKLG637X8YmhDiEk5