mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-20 21:51:41 +00:00
fix(cli): keep UTF-8 log tails valid (#101029)
* fix(cli): keep UTF-8 log tails valid * fix(cli): share UTF-8-safe byte tails --------- Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
@@ -13,6 +13,15 @@ vi.mock("node:child_process", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
function mockSpawnedChild() {
|
||||
const stdout = new EventEmitter();
|
||||
const stderr = new EventEmitter();
|
||||
const kill = vi.fn(() => true);
|
||||
const child = Object.assign(new EventEmitter(), { kill, stderr, stdout });
|
||||
spawnMock.mockReturnValue(child as unknown as ChildProcess);
|
||||
return { child, kill, stderr, stdout };
|
||||
}
|
||||
|
||||
describe("execFileUtf8Tail", () => {
|
||||
beforeEach(() => {
|
||||
spawnMock.mockReset();
|
||||
@@ -21,11 +30,7 @@ describe("execFileUtf8Tail", () => {
|
||||
it.each(["stdout", "stderr"] as const)(
|
||||
"terminates the child when %s emits an error",
|
||||
async (streamName) => {
|
||||
const stdout = new EventEmitter();
|
||||
const stderr = new EventEmitter();
|
||||
const kill = vi.fn(() => true);
|
||||
const child = Object.assign(new EventEmitter(), { kill, stderr, stdout });
|
||||
spawnMock.mockReturnValue(child as unknown as ChildProcess);
|
||||
const { kill, stderr, stdout } = mockSpawnedChild();
|
||||
|
||||
const resultPromise = execFileUtf8Tail("journalctl", ["--no-pager"], { maxBytes: 1024 });
|
||||
stdout.emit("data", Buffer.from("partial output"));
|
||||
@@ -43,10 +48,7 @@ describe("execFileUtf8Tail", () => {
|
||||
);
|
||||
|
||||
it("does not kill the child when spawning fails", async () => {
|
||||
const child = new EventEmitter();
|
||||
const kill = vi.fn(() => true);
|
||||
Object.assign(child, { kill, stderr: new EventEmitter(), stdout: new EventEmitter() });
|
||||
spawnMock.mockReturnValue(child as unknown as ChildProcess);
|
||||
const { child, kill } = mockSpawnedChild();
|
||||
|
||||
const resultPromise = execFileUtf8Tail("journalctl", ["--no-pager"], { maxBytes: 1024 });
|
||||
child.emit("error", new Error("spawn failed"));
|
||||
@@ -54,4 +56,39 @@ describe("execFileUtf8Tail", () => {
|
||||
await expect(resultPromise).resolves.toMatchObject({ code: 1, stderr: "spawn failed" });
|
||||
expect(kill).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ label: "two-byte", text: "¢z", maxBytes: 2, expected: "z" },
|
||||
{ label: "three-byte", text: "€z", maxBytes: 3, expected: "z" },
|
||||
{ label: "four-byte", text: "😀z", maxBytes: 4, expected: "z" },
|
||||
{ label: "complete", text: "a¢z", maxBytes: 3, expected: "¢z" },
|
||||
])("decodes a $label character at the stdout tail boundary", async (testCase) => {
|
||||
const { child, stdout } = mockSpawnedChild();
|
||||
const resultPromise = execFileUtf8Tail("journalctl", ["--no-pager"], {
|
||||
maxBytes: testCase.maxBytes,
|
||||
});
|
||||
|
||||
stdout.emit("data", Buffer.from(testCase.text, "utf8"));
|
||||
child.emit("close", 0);
|
||||
|
||||
await expect(resultPromise).resolves.toEqual({
|
||||
code: 0,
|
||||
stderr: "",
|
||||
stdout: testCase.expected,
|
||||
truncated: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("decodes a truncated stderr tail at a UTF-8 boundary", async () => {
|
||||
const { child, stderr } = mockSpawnedChild();
|
||||
const resultPromise = execFileUtf8Tail("journalctl", ["--no-pager"], { maxBytes: 1024 });
|
||||
|
||||
stderr.emit("data", Buffer.concat([Buffer.from("😀"), Buffer.alloc(64 * 1024 - 3, "x")]));
|
||||
child.emit("close", 1);
|
||||
|
||||
const result = await resultPromise;
|
||||
expect(result.stderr).toBe("x".repeat(64 * 1024 - 3));
|
||||
expect(result.stderr).not.toContain("<22>");
|
||||
expect(result.truncated).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user