fix: settle the deliberately in-flight tool calls in the progress tests

Two progress tests fire `client.callTool()` without holding the promise so
they can assert on the notifications the call streams while it is still in
flight. `disconnect()` then closes the SDK client, which rejects every
pending request with "Connection closed" — with nothing holding the
promise that lands as an unhandled rejection, which vitest counts as a run
error and fails `npm run ci` at `coverage` even though all tests pass.

Add a `settleInFlight()` helper that attaches the handler at call time (not
after the assertions, which would leave a window the rejection can escape
through) and returns a promise the test awaits after `disconnect()` so
teardown stays ordered. Neither outcome is asserted: the call may
legitimately complete before the teardown or reject with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDdo1rNRVQnVRRdsqSrvG3
This commit is contained in:
cliffhall 2026-08-10 17:02:05 -04:00
commit 1881e5c73f

View file

@ -95,6 +95,29 @@ async function getTool(client: InspectorClient, name: string): Promise<Tool> {
throw new Error(`Tool ${name} not found`);
}
/**
* Hold a deliberately un-awaited in-flight call so its rejection is handled.
*
* A few tests start a tool call, assert on the notifications it streams, and
* then tear the connection down while the call is still in flight. That is a
* legitimate thing to exercise, but `disconnect()` closes the SDK client, which
* rejects every pending request with "Connection closed" — and a floating
* promise makes that an *unhandled* rejection, which vitest counts as a run
* error and fails `npm run ci` even though every test passes (#1947).
*
* Attach the handler at call time (not after the assertions) so there is no
* window in which the rejection can escape, and `await` the returned promise
* after `disconnect()` so teardown stays ordered. The call may legitimately
* either settle before the teardown or reject with it, so neither outcome is
* asserted here.
*/
function settleInFlight(call: Promise<unknown>): Promise<void> {
return call.then(
() => undefined,
() => undefined,
);
}
/** Get all resources from the client via listResources() (paginates if needed). */
async function getAllResources(
client: InspectorClient,
@ -2221,16 +2244,18 @@ describe("InspectorClient", () => {
const progressToken = 12345;
const sendProgressTool = await getTool(client, "send_progress");
client.callTool(
sendProgressTool,
{
units: 3,
delayMs: 50,
total: 3,
message: "Test progress",
},
undefined, // generalMetadata
{ progressToken: progressToken.toString() }, // toolSpecificMetadata
const inFlight = settleInFlight(
client.callTool(
sendProgressTool,
{
units: 3,
delayMs: 50,
total: 3,
message: "Test progress",
},
undefined, // generalMetadata
{ progressToken: progressToken.toString() }, // toolSpecificMetadata
),
);
const progressEvents = await waitForProgressCount(client, 3, {
@ -2262,6 +2287,7 @@ describe("InspectorClient", () => {
});
await client!.disconnect();
await inFlight;
await server.stop();
});
@ -2349,15 +2375,17 @@ describe("InspectorClient", () => {
const progressToken = 67890;
const sendProgressTool2 = await getTool(client, "send_progress");
client.callTool(
sendProgressTool2,
{
units: 2,
delayMs: 50,
message: "Indeterminate progress",
},
undefined, // generalMetadata
{ progressToken: progressToken.toString() }, // toolSpecificMetadata
const inFlight = settleInFlight(
client.callTool(
sendProgressTool2,
{
units: 2,
delayMs: 50,
message: "Indeterminate progress",
},
undefined, // generalMetadata
{ progressToken: progressToken.toString() }, // toolSpecificMetadata
),
);
const progressEvents = await waitForProgressCount(client, 2, {
@ -2380,6 +2408,7 @@ describe("InspectorClient", () => {
expect((progressEvents[1] as { total?: number }).total).toBeUndefined();
await client!.disconnect();
await inFlight;
await server.stop();
});