fix(exec): long retained process output corrupts boundary emoji (#104531)

* fix(process): preserve Unicode in captured output tails

* test(process): consolidate captured output coverage

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
xingzhou
2026-07-12 06:35:59 +08:00
committed by GitHub
parent b5bea9073d
commit 2077ebe8b4
2 changed files with 12 additions and 11 deletions

View File

@@ -345,21 +345,25 @@ describe("process supervisor", () => {
expect(exit.stdout).toBe("");
});
it("bounds retained stdout and stderr while streaming full chunks", async () => {
it("bounds retained output on UTF-16 boundaries while streaming full chunks", async () => {
const adapter = createStubChildAdapter();
createChildAdapterMock.mockResolvedValue(adapter);
const supervisor = createProcessSupervisor();
let streamedStdout = "";
let streamedStderr = "";
const stdoutChunk = `${"a".repeat(300)}stdout-tail`;
const stderrChunk = `${"b".repeat(300)}stderr-tail`;
const maxCapturedOutputChars = 256;
const stdoutMarker = `[openclaw: captured stdout truncated to last ${maxCapturedOutputChars} chars]\n`;
const stderrMarker = `[openclaw: captured stderr truncated to last ${maxCapturedOutputChars} chars]\n`;
const retainedChars = maxCapturedOutputChars - stdoutMarker.length - 1;
const stdoutChunk = `${"a".repeat(stdoutMarker.length)}😀${"s".repeat(retainedChars)}`;
const stderrChunk = `${"b".repeat(stderrMarker.length)}😀${"e".repeat(retainedChars)}`;
const run = await spawnChild(supervisor, {
sessionId: "s-capture-cap",
argv: createWriteStdoutArgv(stdoutChunk),
timeoutMs: 1_000,
stdinMode: "pipe-closed",
maxCapturedOutputChars: 256,
maxCapturedOutputChars,
onStdout: (chunk) => {
streamedStdout += chunk;
},
@@ -375,11 +379,7 @@ describe("process supervisor", () => {
const exit = await run.wait();
expect(streamedStdout).toBe(stdoutChunk);
expect(streamedStderr).toBe(stderrChunk);
expect(exit.stdout.length).toBeLessThanOrEqual(256);
expect(exit.stderr.length).toBeLessThanOrEqual(256);
expect(exit.stdout).toContain("captured stdout truncated");
expect(exit.stderr).toContain("captured stderr truncated");
expect(exit.stdout.endsWith("stdout-tail")).toBe(true);
expect(exit.stderr.endsWith("stderr-tail")).toBe(true);
expect(exit.stdout).toBe(`${stdoutMarker}${"s".repeat(retainedChars)}`);
expect(exit.stderr).toBe(`${stderrMarker}${"e".repeat(retainedChars)}`);
});
});

View File

@@ -2,6 +2,7 @@
import crypto from "node:crypto";
import { performance } from "node:perf_hooks";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { sliceUtf16Safe } from "@openclaw/normalization-core/utf16-slice";
import { getShellConfig } from "../../agents/shell-utils.js";
import { createLazyRuntimeModule } from "../../shared/lazy-runtime.js";
import { createChildAdapter } from "./adapters/child.js";
@@ -54,7 +55,7 @@ function appendCapturedOutput(
}
const marker = `[openclaw: captured ${stream} truncated to last ${maxChars} chars]\n`;
const tailChars = Math.max(0, maxChars - marker.length);
return `${marker}${next.slice(-tailChars)}`;
return `${marker}${sliceUtf16Safe(next, -tailChars)}`;
}
function isTimeoutReason(reason: TerminationReason) {