You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#1947 made npm run ci unpassable: two tests fired client.callTool(...) without holding the promise, disconnect() rejected the pending request with Connection closed, and the unhandled rejection failed the whole vitest run — aborting the chain at coverage and silently skipping verify:build-gate, smoke, and Storybook. #1958 fixes those two call sites.
What it does not do is stop the next one. A floating promise is invisible at review time: the call looks like every awaited call minus four characters, and the failure it causes surfaces in a different test, in a different file, as a stack that points at SDK internals. #1947 took a full investigation to attribute; the fix was two lines.
@typescript-eslint/no-floating-promises catches the class outright. It is not currently enabled anywhere — all five ESLint scopes (clients/{web,cli,tui,launcher} and the root core/ + shared gate) extend tseslint.configs.recommended, which is the non-type-aware set. The rule needs type information, so enabling it means turning on parserOptions.projectService for the linted globs.
Measured surface
Probed against clients/web with the rule on (recommendedTypeChecked + projectService), on top of #1958:
Bucket
Violations
*.stories.tsx (play functions)
73
Tests
5
server/ production code
2
Total
80
So the production surface is ~2 sites, and the overwhelming bulk is Storybook play functions — almost certainly un-awaited userEvent.* calls, which are mechanical to fix and arguably worth fixing on their own (an un-awaited userEvent is a real source of interaction-test flake). The other three clients and core/ are unmeasured; this probe covered web only.
The 80 are why this wasn't folded into #1958 — it is a cleanup pass, not a config toggle.
Proposed approach
Turn on projectService per ESLint scope and enable onlyno-floating-promises (not the whole recommendedTypeChecked set — that is a much larger and separate argument).
Measure the remaining four scopes the same way.
Fix the sites. For a deliberately un-awaited call, the fix is to hold and settle the promise — clients/web/src/test/integration/mcp/inspectorClient.test.ts has a settleInFlight() helper from fix: settle the deliberately in-flight tool calls in the progress tests #1958 that documents the pattern and the reasoning.
Land the rule as error once the count is zero.
Things to decide during the work
Scope. Test/story globs only, or production code too? Production is where an unhandled rejection actually hurts a user, and it is only ~2 sites in web — probably worth including rather than deferring.
Lint runtime. Type-aware linting is materially slower, and lint runs inside validate, the fast inner-loop check. Worth timing before/after; if the hit is bad, scoping projectService to the globs that need it is the lever.
void as an escape hatch. The rule accepts void somePromise() as "deliberately ignored". That is strictly better than nothing (it is visible at review time), but it still discards the rejection. Prefer holding and settling the promise where a rejection is actually possible, and reserve void for calls that genuinely cannot reject.
Additional context
Discovered while fixing #1947 (PR #1958) — flagged there as a follow-up rather than folded in.
What problem does this solve?
#1947 made
npm run ciunpassable: two tests firedclient.callTool(...)without holding the promise,disconnect()rejected the pending request withConnection closed, and the unhandled rejection failed the whole vitest run — aborting the chain atcoverageand silently skippingverify:build-gate,smoke, and Storybook. #1958 fixes those two call sites.What it does not do is stop the next one. A floating promise is invisible at review time: the call looks like every awaited call minus four characters, and the failure it causes surfaces in a different test, in a different file, as a stack that points at SDK internals. #1947 took a full investigation to attribute; the fix was two lines.
@typescript-eslint/no-floating-promisescatches the class outright. It is not currently enabled anywhere — all five ESLint scopes (clients/{web,cli,tui,launcher}and the rootcore/+ shared gate) extendtseslint.configs.recommended, which is the non-type-aware set. The rule needs type information, so enabling it means turning onparserOptions.projectServicefor the linted globs.Measured surface
Probed against
clients/webwith the rule on (recommendedTypeChecked+projectService), on top of #1958:*.stories.tsx(play functions)server/production codeSo the production surface is ~2 sites, and the overwhelming bulk is Storybook play functions — almost certainly un-awaited
userEvent.*calls, which are mechanical to fix and arguably worth fixing on their own (an un-awaiteduserEventis a real source of interaction-test flake). The other three clients andcore/are unmeasured; this probe covered web only.The 80 are why this wasn't folded into #1958 — it is a cleanup pass, not a config toggle.
Proposed approach
projectServiceper ESLint scope and enable onlyno-floating-promises(not the wholerecommendedTypeCheckedset — that is a much larger and separate argument).clients/web/src/test/integration/mcp/inspectorClient.test.tshas asettleInFlight()helper from fix: settle the deliberately in-flight tool calls in the progress tests #1958 that documents the pattern and the reasoning.erroronce the count is zero.Things to decide during the work
lintruns insidevalidate, the fast inner-loop check. Worth timing before/after; if the hit is bad, scopingprojectServiceto the globs that need it is the lever.voidas an escape hatch. The rule acceptsvoid somePromise()as "deliberately ignored". That is strictly better than nothing (it is visible at review time), but it still discards the rejection. Prefer holding and settling the promise where a rejection is actually possible, and reservevoidfor calls that genuinely cannot reject.Additional context
Discovered while fixing #1947 (PR #1958) — flagged there as a follow-up rather than folded in.