mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
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:
co-authored by
Peter Steinberger
parent
14dc557cee
commit
f3e8164e7c
@@ -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(
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user