fix(ci): keep boundary output tails UTF-8 safe (#109167)

* fix(ci): keep boundary output tails UTF-8 safe

* fix(ci): preserve split UTF-8 output tails

Co-authored-by: qingminlong <qing.minlong@xydigit.com>

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
qingminlong
2026-07-16 17:12:50 -07:00
committed by GitHub
co-authored by Peter Steinberger
parent 14dc557cee
commit f3e8164e7c
2 changed files with 50 additions and 2 deletions
+11 -2
View File
@@ -183,6 +183,15 @@ export function formatCommand({ command, args }) {
return [command, ...args].join(" ");
}
function decodeUtf8Tail(buffer) {
let start = 0;
while (start < buffer.length && (buffer[start] & 0b1100_0000) === 0b1000_0000) {
start += 1;
}
// Appends are complete JS strings; only a byte slice's leading boundary can be partial.
return buffer.subarray(start).toString("utf8");
}
/**
* Keeps only the tail of noisy check output so failure logs stay bounded.
*/
@@ -197,7 +206,7 @@ export function createBoundedOutputBuffer(maxBytes = DEFAULT_OUTPUT_MAX_BYTES) {
const textBytes = Buffer.byteLength(text);
if (textBytes >= limit) {
const buffer = Buffer.from(text);
const tail = buffer.subarray(buffer.length - limit).toString("utf8");
const tail = decodeUtf8Tail(buffer.subarray(buffer.length - limit));
chunks.splice(0, chunks.length, tail);
bytes = Buffer.byteLength(tail);
truncated = true;
@@ -218,7 +227,7 @@ export function createBoundedOutputBuffer(maxBytes = DEFAULT_OUTPUT_MAX_BYTES) {
}
const buffer = Buffer.from(first);
const tail = buffer.subarray(overflow).toString("utf8");
const tail = decodeUtf8Tail(buffer.subarray(overflow));
chunks[0] = tail;
bytes = chunks.reduce((total, chunk) => total + Buffer.byteLength(chunk), 0);
truncated = true;
@@ -155,6 +155,21 @@ describe("run-additional-boundary-checks", () => {
expect(output.read()).toBe("[output truncated to last 12 bytes]\nsecond-line\n");
});
it("drops split UTF-8 prefixes when one chunk exceeds the output byte cap", () => {
const output = createBoundedOutputBuffer(5);
output.append("old😀new");
expect(output.read()).toBe("[output truncated to last 5 bytes]\nnew");
});
it("drops split UTF-8 prefixes when older buffered output overflows", () => {
const output = createBoundedOutputBuffer(5);
output.append("old😀");
output.append("new");
expect(output.read()).toBe("[output truncated to last 5 bytes]\nnew");
});
it("parses and applies CI shard specs", () => {
expect(parseShardSpec("2/4")).toEqual({ count: 4, index: 1, label: "2/4" });
expect(parseShardSelection("2/4,3/4")).toEqual([
@@ -280,6 +295,30 @@ describe("run-additional-boundary-checks", () => {
expect(result.output).toContain("timed out after 50ms");
});
it("preserves UTF-8 split across process output chunks before trimming the byte tail", async () => {
const script = [
'const bytes = Buffer.from("old😀new");',
"process.stdout.write(bytes.subarray(0, 5));",
"setTimeout(() => process.stdout.end(bytes.subarray(5)), 20);",
].join("");
const result = await runSingleCheck(
{
label: "split-utf8",
command: process.execPath,
args: ["-e", script],
},
{
checkTimeoutMs: 2_000,
cwd: process.cwd(),
env: process.env,
outputMaxBytes: 5,
},
);
expect(result.code).toBe(0);
expect(result.output).toBe("[output truncated to last 5 bytes]\nnew");
});
it("clamps oversized check timers before scheduling", async () => {
const result = await runSingleCheck(
{