From e4a48157d117fc124be4ba7182f7debf7ac14a90 Mon Sep 17 00:00:00 2001 From: qingminlong Date: Fri, 17 Jul 2026 13:53:11 +0800 Subject: [PATCH] fix(test): keep bounded child output UTF-8 safe (#109168) * fix(test): keep bounded child output UTF-8 safe * test: preserve incomplete child output tails --------- Co-authored-by: Peter Steinberger --- test/helpers/bounded-child-output.test.ts | 25 +++++++++++++++++++++++ test/helpers/bounded-child-output.ts | 12 ++++++++++- 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/test/helpers/bounded-child-output.test.ts b/test/helpers/bounded-child-output.test.ts index 32c0e15e21b0..2ec1b29656ce 100644 --- a/test/helpers/bounded-child-output.test.ts +++ b/test/helpers/bounded-child-output.test.ts @@ -22,4 +22,29 @@ describe("bounded child output", () => { expect(output.text()).toBe("x-recent"); }); + + it("drops split UTF-8 prefixes after buffered output overflow", () => { + const output = createBoundedChildOutput(7); + + output.append(Buffer.from("prefix 😀")); + output.append(Buffer.from("tail")); + + expect(output.text()).toBe("tail"); + }); + + it("drops split UTF-8 prefixes from single oversized chunks", () => { + const output = createBoundedChildOutput(7); + + output.append(Buffer.from("prefix 😀tail")); + + expect(output.text()).toBe("tail"); + }); + + it("preserves replacement output for incomplete trailing bytes", () => { + const output = createBoundedChildOutput(7); + + output.append(Buffer.from([0x61, 0xe2])); + + expect(output.text()).toBe("a�"); + }); }); diff --git a/test/helpers/bounded-child-output.ts b/test/helpers/bounded-child-output.ts index d4669e710c81..d2826f5078f2 100644 --- a/test/helpers/bounded-child-output.ts +++ b/test/helpers/bounded-child-output.ts @@ -1,5 +1,15 @@ +import { StringDecoder } from "node:string_decoder"; + export const DEFAULT_CHILD_OUTPUT_TAIL_BYTES = 128 * 1024; +function decodeUtf8Tail(buffer: Buffer): string { + let start = 0; + while (start < buffer.length && (buffer[start]! & 0b1100_0000) === 0b1000_0000) { + start += 1; + } + return new StringDecoder("utf8").end(buffer.subarray(start)); +} + export function createBoundedChildOutput(maxBytes = DEFAULT_CHILD_OUTPUT_TAIL_BYTES) { const limit = Number.isInteger(maxBytes) && maxBytes > 0 ? maxBytes : DEFAULT_CHILD_OUTPUT_TAIL_BYTES; @@ -37,7 +47,7 @@ export function createBoundedChildOutput(maxBytes = DEFAULT_CHILD_OUTPUT_TAIL_BY trim(); }, text(): string { - return Buffer.concat(chunks, totalBytes).toString("utf8"); + return decodeUtf8Tail(Buffer.concat(chunks, totalBytes)); }, }; }