fix(cli-runner): keep output tails utf8 safe (#108355)

* fix(cli-runner): keep output tails utf8 safe

* fix(cli-runner): satisfy output tail byte narrowing

* test(cli-runner): format utf8 tail regression

* refactor(cli-runner): reuse UTF-8 tail helper

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
qingminlong
2026-07-16 04:20:38 -07:00
committed by GitHub
co-authored by Peter Steinberger
parent a719a851bb
commit 71bea81669
3 changed files with 40 additions and 17 deletions
@@ -0,0 +1,32 @@
import { describe, expect, it } from "vitest";
import { appendCliOutputTail } from "./execute-output-buffer.js";
const CLI_RUNNER_OUTPUT_TAIL_BYTES = 64 * 1024;
const REPLACEMENT_CHARACTER = String.fromCharCode(0xfffd);
const MULTIBYTE_CHARACTER = String.fromCodePoint(0x1f642);
describe("appendCliOutputTail", () => {
it("keeps large chunk tails UTF-8 safe when truncation starts inside a character", () => {
const chunk = `${"x".repeat(10)}${MULTIBYTE_CHARACTER}${"y".repeat(
CLI_RUNNER_OUTPUT_TAIL_BYTES - 3,
)}`;
const output = appendCliOutputTail("", chunk);
expect(Buffer.byteLength(output)).toBeLessThanOrEqual(CLI_RUNNER_OUTPUT_TAIL_BYTES);
expect(output).not.toContain(REPLACEMENT_CHARACTER);
expect(output).toBe("y".repeat(CLI_RUNNER_OUTPUT_TAIL_BYTES - 3));
});
it("keeps appended tails UTF-8 safe when rolling overflow starts inside a character", () => {
const existingTail = `${"x".repeat(10)}${MULTIBYTE_CHARACTER}${"y".repeat(
CLI_RUNNER_OUTPUT_TAIL_BYTES - 14,
)}`;
const output = appendCliOutputTail(existingTail, "z".repeat(11));
expect(Buffer.byteLength(output)).toBeLessThanOrEqual(CLI_RUNNER_OUTPUT_TAIL_BYTES);
expect(output).not.toContain(REPLACEMENT_CHARACTER);
expect(output).toBe(`${"y".repeat(CLI_RUNNER_OUTPUT_TAIL_BYTES - 14)}${"z".repeat(11)}`);
});
});
+4 -13
View File
@@ -1,16 +1,7 @@
import { truncateUtf8Suffix } from "../../utils/utf8-truncate.js";
const CLI_RUNNER_OUTPUT_TAIL_BYTES = 64 * 1024;
export function appendCliOutputTail(tail: Buffer, chunk: string): Buffer {
if (!chunk) {
return tail;
}
const chunkBuffer = Buffer.from(chunk);
if (chunkBuffer.byteLength >= CLI_RUNNER_OUTPUT_TAIL_BYTES) {
return Buffer.from(chunkBuffer.subarray(chunkBuffer.byteLength - CLI_RUNNER_OUTPUT_TAIL_BYTES));
}
const next = Buffer.concat([tail, chunkBuffer], tail.byteLength + chunkBuffer.byteLength);
if (next.byteLength <= CLI_RUNNER_OUTPUT_TAIL_BYTES) {
return next;
}
return Buffer.from(next.subarray(next.byteLength - CLI_RUNNER_OUTPUT_TAIL_BYTES));
export function appendCliOutputTail(tail: string, chunk: string): string {
return truncateUtf8Suffix(`${tail}${chunk}`, CLI_RUNNER_OUTPUT_TAIL_BYTES);
}
+4 -4
View File
@@ -1607,12 +1607,12 @@ export async function executePreparedCliRun(
},
})
: null;
let stdoutTail: Buffer = Buffer.alloc(0);
let stdoutTail = "";
let stdoutParseBuffer: Buffer = Buffer.alloc(0);
let stdoutBytes = 0;
const stdoutHash = crypto.createHash("sha256");
let stdoutParseExceeded = false;
let stderrTail: Buffer = Buffer.alloc(0);
let stderrTail = "";
let stderrParseBuffer: Buffer = Buffer.alloc(0);
let stderrBytes = 0;
const stderrHash = crypto.createHash("sha256");
@@ -1760,9 +1760,9 @@ export async function executePreparedCliRun(
}
const stdout = stdoutParseBuffer.toString("utf8").trim();
const stdoutDiagnostic = stdoutTail.toString("utf8").trim();
const stdoutDiagnostic = stdoutTail.trim();
const stderr = stderrParseBuffer.toString("utf8").trim();
const stderrDiagnostic = stderrTail.toString("utf8").trim();
const stderrDiagnostic = stderrTail.trim();
const processDiagnostics = {
backendId: context.backendResolved.id,
processReason: result.reason,