test(sse): read the errored Kiro failure stream instead of buffering it

"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.
This commit is contained in:
diegosouzapw
2026-09-11 18:15:03 -03:00
parent b57ac708a7
commit e043682a23

View File

@@ -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<Uint8Array>({
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/);