From 931785ec71a249c81f736058125b8e907ec6b202 Mon Sep 17 00:00:00 2001 From: Noor Mkdad <37319157+DrMkdaddy@users.noreply.github.com> Date: Fri, 18 Sep 2026 23:06:44 -0700 Subject: [PATCH] test(node): read SSE responses until expected events arrive The Streamable HTTP SSE tests assumed every event for a response arrives in a single reader.read() chunk. fetch may deliver them in separate chunks, so the tests failed on newer Node versions even though the transport behavior was correct. Add readSSEEventUntil(), which accumulates chunks with a streaming TextDecoder until a predicate matches or the stream ends, and use it in the batch-response and notification-replay tests. Fixes #2661 --- .../node/test/streamableHttp.test.ts | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/packages/middleware/node/test/streamableHttp.test.ts b/packages/middleware/node/test/streamableHttp.test.ts index 140717c6bb..b5f0a00415 100644 --- a/packages/middleware/node/test/streamableHttp.test.ts +++ b/packages/middleware/node/test/streamableHttp.test.ts @@ -104,6 +104,23 @@ async function readSSEEvent(response: Response): Promise { return new TextDecoder().decode(value); } +/** + * Read from an SSE response until the accumulated text satisfies the predicate, + * or the stream ends. Fetch may deliver SSE events in separate chunks, so tests + * must not assume a single read contains every event. + */ +async function readSSEEventUntil(reader: ReadableStreamDefaultReader, predicate: (text: string) => boolean): Promise { + const decoder = new TextDecoder(); + let text = ''; + for (;;) { + const { value, done } = await reader.read(); + if (value) text += decoder.decode(value, { stream: true }); + if (predicate(text)) break; + if (done) break; + } + return text; +} + /** * Helper to send JSON-RPC request */ @@ -727,9 +744,8 @@ describe('Zod v4', () => { const reader = response.body?.getReader(); - // The responses may come in any order or together in one chunk - const { value } = await reader!.read(); - const text = new TextDecoder().decode(value); + // The responses may arrive in any order or in separate chunks + const text = await readSSEEventUntil(reader!, t => t.includes('"id":"req-1"') && t.includes('"id":"req-2"')); // Check that both responses were sent on the same stream expect(text).toContain('"id":"req-1"'); @@ -1482,8 +1498,7 @@ describe('Zod v4', () => { // Read the notification from the SSE stream const reader = sseResponse.body?.getReader(); - const { value } = await reader!.read(); - const text = new TextDecoder().decode(value); + const text = await readSSEEventUntil(reader!, t => t.includes('First notification from MCP server') && t.includes('id: ')); // Verify the notification was sent with an event ID expect(text).toContain('id: '); @@ -1515,8 +1530,7 @@ describe('Zod v4', () => { // Read the replayed notification const reconnectReader = reconnectResponse.body?.getReader(); - const reconnectData = await reconnectReader!.read(); - const reconnectText = new TextDecoder().decode(reconnectData.value); + const reconnectText = await readSSEEventUntil(reconnectReader!, t => t.includes('Second notification from MCP server')); // Verify we received the second notification that was sent after our stored eventId expect(reconnectText).toContain('Second notification from MCP server');