Skip to content

Commit 5b3fe0a

Browse files
committed
test(mcp): cover abort during HTTP auth recovery on callBlocks
1 parent 6d07b36 commit 5b3fe0a

2 files changed

Lines changed: 118 additions & 0 deletions

File tree

‎src/mcp/client-auth-reauth-cap.test.ts‎

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,48 @@ describe("HTTP MCP re-auth loop prevention", () => {
465465
expect(authURLCount).toBe(1);
466466
});
467467

468+
test("block-path caller abort does not cancel shared recovery for another call", async () => {
469+
finishAuthError = undefined;
470+
callbackGate = new Promise((resolve) => {
471+
releaseCallback = resolve;
472+
});
473+
const connected = await connectMCPServer(config, {
474+
onAuthURL: () => (authURLCount += 1),
475+
});
476+
expect(connected.ok).toBe(true);
477+
if (!connected.ok) return;
478+
callFailuresLeft = 2;
479+
const callBlocks = connected.client.callBlocks;
480+
expect(callBlocks).toBeDefined();
481+
if (callBlocks === undefined) return;
482+
const firstAbort = new AbortController();
483+
const first = callBlocks("first", {}, firstAbort.signal);
484+
const second = callBlocks("second", {}, new AbortController().signal);
485+
while (waitForCodeCalls === 0) await Promise.resolve();
486+
487+
let abortTimer: ReturnType<typeof setTimeout> | undefined;
488+
const abortTimeout = new Promise<never>((_, reject) => {
489+
abortTimer = setTimeout(
490+
() => reject(new Error("timed out waiting for caller abort")),
491+
1000,
492+
);
493+
});
494+
try {
495+
firstAbort.abort(new Error("caller stopped"));
496+
await expect(Promise.race([first, abortTimeout])).rejects.toThrow(
497+
"caller stopped",
498+
);
499+
} finally {
500+
if (abortTimer !== undefined) clearTimeout(abortTimer);
501+
releaseCallback?.();
502+
}
503+
504+
await expect(second).resolves.toEqual([]);
505+
expect(waitForCodeCalls).toBe(1);
506+
expect(finishAuthCalls).toBe(1);
507+
expect(authURLCount).toBe(1);
508+
});
509+
468510
test("aborted waiter still fires onAuthorized when background finishAuth succeeds", async () => {
469511
finishAuthError = undefined;
470512
callbackGate = new Promise((resolve) => {
@@ -508,6 +550,50 @@ describe("HTTP MCP re-auth loop prevention", () => {
508550
expect(authURLCount).toBe(1 + MAX_BROWSER_AUTH_ATTEMPTS);
509551
});
510552

553+
test("block-path aborted waiter still fires onAuthorized when background finishAuth succeeds", async () => {
554+
finishAuthError = undefined;
555+
callbackGate = new Promise((resolve) => {
556+
releaseCallback = resolve;
557+
});
558+
const connected = await connectMCPServer(config, {
559+
onAuthURL: () => (authURLCount += 1),
560+
onAuthorized: () => (authorizedCount += 1),
561+
});
562+
expect(connected.ok).toBe(true);
563+
if (!connected.ok) return;
564+
const callBlocks = connected.client.callBlocks;
565+
expect(callBlocks).toBeDefined();
566+
if (callBlocks === undefined) return;
567+
callFailuresLeft = 1;
568+
const abort = new AbortController();
569+
const call = callBlocks("ping", {}, abort.signal);
570+
while (authURLCount === 0 || waitForCodeCalls === 0)
571+
await Promise.resolve();
572+
573+
let abortTimer: ReturnType<typeof setTimeout> | undefined;
574+
const abortTimeout = new Promise<never>((_, reject) => {
575+
abortTimer = setTimeout(
576+
() => reject(new Error("timed out waiting for caller abort")),
577+
1000,
578+
);
579+
});
580+
try {
581+
abort.abort(new Error("caller stopped"));
582+
await expect(Promise.race([call, abortTimeout])).rejects.toThrow(
583+
"caller stopped",
584+
);
585+
} finally {
586+
if (abortTimer !== undefined) clearTimeout(abortTimer);
587+
releaseCallback?.();
588+
}
589+
expect(authorizedCount).toBe(0);
590+
while (finishAuthCalls === 0) await Promise.resolve();
591+
for (let tick = 0; tick < 20 && authorizedCount === 0; tick += 1)
592+
await Promise.resolve();
593+
expect(authorizedCount).toBe(1);
594+
expect(finishAuthCalls).toBe(1);
595+
});
596+
511597
test("refresh-only recovery clears prior browser-cap counts", async () => {
512598
const connected = await connectMCPServer(config, {
513599
onAuthURL: () => (authURLCount += 1),

‎src/mcp/plugin.test.ts‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,4 +179,36 @@ describe("mcpClientToAgentTools", () => {
179179
toolOutputAbsolutePath(contextDir, key, "text/plain"),
180180
);
181181
});
182+
183+
test("a parked block-path call aborted mid-recovery surfaces a failed result", async () => {
184+
const gate = skipGate();
185+
const client: MCPClient = {
186+
...fakeClient("unused"),
187+
callBlocks: (_tool, _args, signal) =>
188+
new Promise<never>((_resolve, reject) => {
189+
if (signal.aborted) {
190+
reject(signal.reason ?? new Error("aborted"));
191+
return;
192+
}
193+
signal.addEventListener(
194+
"abort",
195+
() => reject(signal.reason ?? new Error("aborted")),
196+
{ once: true },
197+
);
198+
}),
199+
};
200+
const [tool] = mcpClientToAgentTools(client, gate);
201+
if (tool?.kind !== "full") throw new Error("expected full tool");
202+
203+
const controller = new AbortController();
204+
const pending = tool.handler(
205+
{ id: "c-mcp-abort", name: "mcp__acme__fetch_secret", arguments: {} },
206+
controller.signal,
207+
);
208+
controller.abort(new Error("caller stopped"));
209+
const result = await pending;
210+
211+
expect(result.isError).toBe(true);
212+
expect(result.content).toContain("caller stopped");
213+
});
182214
});

0 commit comments

Comments
 (0)