Skip to content

Commit 5f63a87

Browse files
committed
fix(test): reject pending MCP harness deferreds on clear
Final-review feedback: the harness's pushDeferred() Promise was never explicitly settled when an upstream abort happened before release. The client.setRequestHandler frame parked at `await next.promise` only unblocked at client.close() during dispose. clear() now tracks pending deferreds and rejects each so the handler frame settles deterministically between tests rather than leaking until afterAll.
1 parent c9a34dd commit 5f63a87

1 file changed

Lines changed: 34 additions & 8 deletions

File tree

packages/test/src/test/human/mcpHarness.ts

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,27 @@ type ImmediateQueueEntry = { readonly kind: "immediate"; readonly entry: MockRes
2424

2525
type DeferredQueueEntry = {
2626
readonly kind: "deferred";
27-
/** Resolves when release() is called (or pre-resolved if release ran first). */
27+
/** Resolves when release() is called, or rejects when clear() runs. */
2828
readonly promise: Promise<IHumanResponse>;
2929
};
3030

3131
type HarnessQueueEntry = ImmediateQueueEntry | DeferredQueueEntry;
3232

33+
type DeferredHandle = {
34+
readonly settled: { done: boolean };
35+
readonly reject: (err: Error) => void;
36+
};
37+
3338
class HarnessScript implements MockResponseScript {
3439
private readonly _received: IHumanRequest[] = [];
3540
private readonly _queue: HarnessQueueEntry[] = [];
41+
/**
42+
* All deferreds that have been pushed but not yet settled. clear() rejects
43+
* each so any parked client.setRequestHandler `await next.promise` unblocks
44+
* deterministically — otherwise the SDK callback frame leaks until
45+
* client.close() at dispose.
46+
*/
47+
private readonly _pendingDeferreds: DeferredHandle[] = [];
3648

3749
get received(): ReadonlyArray<IHumanRequest> {
3850
return this._received;
@@ -48,15 +60,19 @@ class HarnessScript implements MockResponseScript {
4860

4961
pushDeferred(): { release(response: IHumanResponse): void } {
5062
let resolveFn: (res: IHumanResponse) => void = () => {};
51-
let settled = false;
52-
const promise = new Promise<IHumanResponse>((resolve) => {
63+
let rejectFn: (err: Error) => void = () => {};
64+
const settled = { done: false };
65+
const promise = new Promise<IHumanResponse>((resolve, reject) => {
5366
resolveFn = resolve;
67+
rejectFn = reject;
5468
});
5569
this._queue.push({ kind: "deferred", promise });
70+
const handle: DeferredHandle = { settled, reject: rejectFn };
71+
this._pendingDeferreds.push(handle);
5672
return {
5773
release: (response) => {
58-
if (settled) return;
59-
settled = true;
74+
if (settled.done) return;
75+
settled.done = true;
6076
resolveFn(response);
6177
},
6278
};
@@ -66,9 +82,11 @@ class HarnessScript implements MockResponseScript {
6682
* Consumed by the elicit handler to resolve the next outgoing prompt.
6783
*
6884
* If a deferred entry is awaiting release, this awaits the deferred promise.
69-
* The conformance suite's abort-mid-elicit test aborts the upstream signal
70-
* BEFORE release is called — `Server.elicitInput` rejects on the connector
71-
* side and the dangling promise is GC'd at dispose time. No leak.
85+
* abort-mid-elicit aborts the upstream signal first, but the parked
86+
* `await next.promise` here only unblocks when either `release()` runs or
87+
* `clear()` rejects the deferred. clear() rejects pending deferreds so the
88+
* handler frame unblocks deterministically between tests rather than
89+
* leaking until dispose.
7290
*/
7391
async takeNext(req: IHumanRequest): Promise<IHumanResponse> {
7492
const next = this._queue.shift();
@@ -84,6 +102,14 @@ class HarnessScript implements MockResponseScript {
84102
}
85103

86104
clear(): void {
105+
for (const handle of this._pendingDeferreds) {
106+
if (handle.settled.done) continue;
107+
handle.settled.done = true;
108+
const err = new Error("MCP harness cleared while deferred was pending");
109+
err.name = "AbortError";
110+
handle.reject(err);
111+
}
112+
this._pendingDeferreds.length = 0;
87113
this._received.length = 0;
88114
this._queue.length = 0;
89115
}

0 commit comments

Comments
 (0)