From e043682a235d697fe85dee0d2ddcf3e26f723e6c Mon Sep 17 00:00:00 2001 From: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> Date: Fri, 11 Sep 2026 18:15:03 -0300 Subject: [PATCH] test(sse): read the errored Kiro failure stream instead of buffering it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Kiro stream errors become Responses response.failed events" drove the transform with `new Response(transform.readable).text()`. That cannot observe the contract it is asserting: createStreamFailureAborter forwards the translated failure event and then errors the controller on purpose, so a translated upstream error never ends as a clean, successful-looking stream (the sibling tests in stream-passthrough-error-redaction and stream-utils pin exactly that). `.text()` discards the forwarded bytes and rejects, `await writer.close()` threw ERR_INVALID_STATE first, and the abandoned `.text()` rejection surfaced as an unhandledRejection after the test ended. Drive it the way production does instead — `pipeThrough` plus a reader, the same shape as collectUntilFailure() in the sibling suite. The forwarded event is kept (a pending read is fulfilled before the error resets the queue) and the deliberate termination is now asserted rather than tripped over. All four original assertions are unchanged; the termination assertion is added. Production code is untouched: `pipeThrough` marks its internal pipeTo promise handled, so the abandoned rejection never existed off the test bench. 6/6 green. --- tests/unit/kiro-tool-call-validation.test.ts | 52 ++++++++++++++------ 1 file changed, 37 insertions(+), 15 deletions(-) diff --git a/tests/unit/kiro-tool-call-validation.test.ts b/tests/unit/kiro-tool-call-validation.test.ts index f0e7c47f7f..392b2975b5 100644 --- a/tests/unit/kiro-tool-call-validation.test.ts +++ b/tests/unit/kiro-tool-call-validation.test.ts @@ -236,23 +236,45 @@ test("Kiro stream errors become Responses response.failed events", async () => { null, "kiro-model" ); - const writer = transform.writable.getWriter(); - const responseText = new Response(transform.readable).text(); + // Drive the transform the way production does — `response.body.pipeThrough(transform)` + // read chunk by chunk — instead of `new Response(transform.readable).text()`. + // createStreamFailureAborter forwards the translated failure event and then errors the + // controller on purpose, so a translated upstream error can never end as a clean, + // successful-looking stream (open-sse/utils/streamFailureBoundary.ts). `.text()` cannot + // observe that: it discards the forwarded bytes and rejects, and the abandoned + // rejection lands as an unhandledRejection after the test ends. A reader keeps the + // event that was already delivered and still sees the termination. + const upstream = new ReadableStream({ + start(controller) { + controller.enqueue( + textEncoder.encode( + `data: ${JSON.stringify({ + error: { + message: "Invalid Kiro tool_call payload: missing nested MCP tool name at input.name", + type: "invalid_request_error", + code: "invalid_kiro_tool_call", + }, + })}\n\n` + ) + ); + controller.close(); + }, + }); - await writer.write( - textEncoder.encode( - `data: ${JSON.stringify({ - error: { - message: "Invalid Kiro tool_call payload: missing nested MCP tool name at input.name", - type: "invalid_request_error", - code: "invalid_kiro_tool_call", - }, - })}\n\n` - ) - ); - await writer.close(); - const text = await responseText; + const reader = upstream.pipeThrough(transform).getReader(); + let text = ""; + let streamError: unknown = null; + try { + for (;;) { + const chunk = await reader.read(); + if (chunk.done) break; + text += new TextDecoder().decode(chunk.value); + } + } catch (caught) { + streamError = caught; + } + assert.ok(streamError, "a translated upstream error must terminate the stream"); assert.match(text, /event: response\.failed/); assert.match(text, /invalid_kiro_tool_call/); assert.match(text, /missing nested MCP tool name/);