From 1f5dc6e046bd8cdb336d21e86065b1b45465029d Mon Sep 17 00:00:00 2001 From: Singmin <74779427+Singmin@users.noreply.github.com> Date: Mon, 19 Oct 2026 00:28:51 +0800 Subject: [PATCH] test(e2e): wait for the SSE round trip in protocol:timeout:max-total The fake clock advanced before the tools/call POST reached the server, so the per-request timeout fired before any progress notification arrived. Wait for the server to enter the tool handler, then for each progress notification to reach the client, before advancing the clock again. --- test/e2e/scenarios/protocol.test.ts | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/test/e2e/scenarios/protocol.test.ts b/test/e2e/scenarios/protocol.test.ts index 4abcde078d..9cd38ac833 100644 --- a/test/e2e/scenarios/protocol.test.ts +++ b/test/e2e/scenarios/protocol.test.ts @@ -662,12 +662,18 @@ verifies('protocol:timeout:basic', async ({ transport }: TestArgs) => { verifies('protocol:timeout:max-total', async ({ transport }: TestArgs) => { vi.useFakeTimers(); try { + let onCall: () => void = () => {}; + const callReceived = new Promise(resolve => { + onCall = resolve; + }); + const makeServer = () => { const s = new McpServer({ name: 's', version: '0' }); s.registerTool( 'slow-progress', { inputSchema: z.object({ delayMs: z.number(), steps: z.number() }) }, async ({ delayMs, steps }, ctx) => { + onCall(); const token = ctx.mcpReq._meta?.progressToken; if (token !== undefined) { for (let i = 1; i <= steps; i++) { @@ -691,6 +697,11 @@ verifies('protocol:timeout:max-total', async ({ transport }: TestArgs) => { const delayMs = 200; const ticks: number[] = []; + let onTick: (() => void) | undefined; + const nextTick = () => + new Promise(resolve => { + onTick = resolve; + }); const call = client.callTool( { name: 'slow-progress', arguments: { delayMs, steps: 100 } }, @@ -698,13 +709,27 @@ verifies('protocol:timeout:max-total', async ({ transport }: TestArgs) => { timeout: perChunk, resetTimeoutOnProgress: true, maxTotalTimeout: maxTotal, - onprogress: p => ticks.push(p.progress) + onprogress: p => { + ticks.push(p.progress); + onTick?.(); + onTick = undefined; + } } ); - call.catch(() => {}); + const callSettled = call.then( + () => {}, + () => {} + ); + + // An SSE round trip is real I/O: the POST must land before the fake clock moves, or the request timeout fires first. + await callReceived; for (let elapsed = 0; elapsed < maxTotal + perChunk; elapsed += delayMs) { + const tick = nextTick(); await vi.advanceTimersByTimeAsync(delayMs); + // Wait for the notification to arrive before advancing again, so the clock cannot outrun the SSE stream. + const next = await Promise.race([tick.then(() => 'progress' as const), callSettled.then(() => 'settled' as const)]); + if (next === 'settled') break; } await expect(call).rejects.toBeInstanceOf(SdkError);