Summary
npm run ci — the mandatory pre-push gate — exits 1 on a clean v2/main tree, and does so in a way that looks like success. Two unhandled promise rejections in clients/web/src/test/integration/mcp/inspectorClient.test.ts make vitest fail the run even though every test passes, which aborts the chain at coverage and silently skips the last three stages.
Reproduce
From a clean v2/main checkout with no local changes:
npm run ci; echo "EXIT=$?"
Test Files 313 passed (313)
Tests 4812 passed (4812)
Errors 2 errors
...
=============================== Coverage summary ===============================
Statements : 98.62% ( 10955/11108 )
...
EXIT=1
Both rejections are the same shape:
⎯⎯⎯⎯ Unhandled Rejection ⎯⎯⎯⎯⎯
SdkError: Connection closed
❯ Client._onclose .../shared/protocol.ts:839:23
❯ StreamableHTTPClientTransport.close .../client/streamableHttp.ts:890:18
❯ MessageTrackingTransport.close ../../core/mcp/messageTrackingTransport.ts:110:31
❯ InspectorClient.disconnect ../../core/mcp/inspectorClient.ts:2092:29
❯ src/test/integration/mcp/inspectorClient.test.ts:2264:21
The second is identical from :2382. Reported against the tests "should not dispatch progressNotification events when progress is disabled" and "should complete when timeout and resetTimeoutOnProgress are set (options passed through)" — a teardown race in disconnect, where the close rejection is never awaited.
Reproduces standalone too:
cd clients/web && npx vitest run --project=integration src/test/integration/mcp/inspectorClient.test.ts
# Tests 140 passed (140)
# Errors 2 errors
Why this is worse than a flake
ci is validate → coverage → verify:build-gate → smoke → Storybook, chained on success. Because coverage exits non-zero, verify:build-gate, smoke, and ci:storybook never run. The failure is easy to misread: the last thing printed is a coverage summary with 100% of tests passing, so it reads as a completed run rather than an aborted one.
That defeats the guarantee AGENTS.md makes for the command — "a true superset of GitHub CI, and passing it locally means CI's gates will pass". Right now it cannot pass, and anyone who eyeballs the tail concludes it did while a third of the gate was skipped. Two of us hit exactly this on #1946 and #1945; both needed the last three stages run by hand afterwards (they pass).
Scope
- Not caused by any current PR — confirmed by stashing all local changes on two separate branches and reproducing on the clean baseline.
- Deterministic on this machine (macOS 26.6 / Node v25.9.0), 3+ runs. Whether CI's Linux runners hit it is unverified; GitHub CI is currently green, which suggests it may be environment-sensitive — that's worth establishing as part of the fix.
Suggested direction
Fix the teardown race so disconnect() doesn't leave an unawaited rejecting close (either await the transport close or attach a handler in InspectorClient.disconnect / MessageTrackingTransport.close). Swallowing it at the vitest config level would restore a green exit code while leaving a real unhandled rejection in the runtime path, so it isn't the right lever.
Summary
npm run ci— the mandatory pre-push gate — exits 1 on a cleanv2/maintree, and does so in a way that looks like success. Two unhandled promise rejections inclients/web/src/test/integration/mcp/inspectorClient.test.tsmake vitest fail the run even though every test passes, which aborts the chain atcoverageand silently skips the last three stages.Reproduce
From a clean
v2/maincheckout with no local changes:Both rejections are the same shape:
The second is identical from
:2382. Reported against the tests "should not dispatch progressNotification events when progress is disabled" and "should complete when timeout and resetTimeoutOnProgress are set (options passed through)" — a teardown race indisconnect, where the close rejection is never awaited.Reproduces standalone too:
Why this is worse than a flake
ciisvalidate→coverage→verify:build-gate→smoke→ Storybook, chained on success. Becausecoverageexits non-zero,verify:build-gate,smoke, andci:storybooknever run. The failure is easy to misread: the last thing printed is a coverage summary with 100% of tests passing, so it reads as a completed run rather than an aborted one.That defeats the guarantee
AGENTS.mdmakes for the command — "a true superset of GitHub CI, and passing it locally means CI's gates will pass". Right now it cannot pass, and anyone who eyeballs the tail concludes it did while a third of the gate was skipped. Two of us hit exactly this on #1946 and #1945; both needed the last three stages run by hand afterwards (they pass).Scope
Suggested direction
Fix the teardown race so
disconnect()doesn't leave an unawaited rejecting close (either await the transport close or attach a handler inInspectorClient.disconnect/MessageTrackingTransport.close). Swallowing it at the vitest config level would restore a green exit code while leaving a real unhandled rejection in the runtime path, so it isn't the right lever.