Skip to content

Commit 998f0ed

Browse files
wangmiao0668000666claudesteipete
authored
fix(e2e): keep phase log tails UTF-8 safe (#109923)
* fix(e2e): keep phase log tails UTF-8 safe The phase runner keeps a bounded in-memory log tail per phase and writes it to stderr when a phase fails. The byte-based cut can land inside a multi-byte UTF-8 character, and decoding from that offset emits U+FFFD replacement characters at the start of the diagnostics tail. Reuse the decodeUtf8Tail helper from scripts/e2e/lib/text-file-utils.mjs (#109669) so the retained window starts on a character boundary. Co-Authored-By: Claude <[email protected]> * fix(e2e): declare decodeUtf8Tail in text-file-utils types phase-runner.ts imports decodeUtf8Tail from text-file-utils.mjs; the adjacent declaration file must export it or check-test-types fails with TS2305. Co-Authored-By: Claude <[email protected]> * refactor(e2e): reuse tailText wrapper for phase log tails Co-Authored-By: Claude <[email protected]> * test(e2e): focus phase tail regression Co-authored-by: wangmiao0668000666 <[email protected]> --------- Co-authored-by: Claude <[email protected]> Co-authored-by: Peter Steinberger <[email protected]>
1 parent f8380a8 commit 998f0ed

2 files changed

Lines changed: 46 additions & 2 deletions

File tree

scripts/e2e/parallels/phase-runner.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { appendFileSync } from "node:fs";
33
import { writeFile } from "node:fs/promises";
44
import path from "node:path";
55
import { clampTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion";
6+
import { tailText } from "../lib/text-file-utils.mjs";
67
import { say, warn } from "./host-command.ts";
78

89
const PHASE_LOG_TAIL_MAX_BYTES = 512 * 1024;
@@ -15,8 +16,8 @@ function appendTextTail(current: string, chunk: string, maxBytes: number): strin
1516
}
1617
const marker = `[phase log tail truncated to last ${maxBytes} bytes]\n`;
1718
const tailBytes = Math.max(0, maxBytes - Buffer.byteLength(marker));
18-
const tail = Buffer.from(combined).subarray(-tailBytes).toString("utf8");
19-
return `${marker}${tail}`;
19+
// tailText owns the UTF-8-safe byte truncation; the marker keeps the tail self-describing.
20+
return `${marker}${tailText(combined, tailBytes)}`;
2021
}
2122

2223
function resolvePhaseTimeoutMs(timeoutSeconds: number): number {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Parallels Phase Runner tests cover bounded in-memory phase log tails.
2+
import { afterEach, expect, it, vi } from "vitest";
3+
import { PhaseRunner } from "../../scripts/e2e/parallels/phase-runner.ts";
4+
import { cleanupTempDirs, makeTempDir } from "../helpers/temp-dir.js";
5+
6+
const tempRoots: string[] = [];
7+
8+
function makeTempRoot() {
9+
return makeTempDir(tempRoots, "openclaw-parallels-phase-runner-");
10+
}
11+
12+
afterEach(() => {
13+
cleanupTempDirs(tempRoots);
14+
vi.restoreAllMocks();
15+
});
16+
17+
async function captureFailedPhaseTail(runner: PhaseRunner, text: string): Promise<string> {
18+
const written: string[] = [];
19+
vi.spyOn(process.stderr, "write").mockImplementation((chunk) => {
20+
written.push(String(chunk));
21+
return true;
22+
});
23+
await expect(
24+
runner.phase("utf8-tail", 60, () => {
25+
runner.append(text);
26+
throw new Error("boom");
27+
}),
28+
).rejects.toThrow("boom");
29+
return written.join("");
30+
}
31+
32+
it("keeps the truncated phase tail UTF-8 safe when the byte cut splits a character", async () => {
33+
// 134 bytes total; the retained window starts one byte into the 4-byte
34+
// emoji, so a byte-naive decode would emit replacement characters.
35+
const tail = await captureFailedPhaseTail(
36+
new PhaseRunner(makeTempRoot(), 128),
37+
`${"x".repeat(50)}😀${"y".repeat(79)}`,
38+
);
39+
40+
expect(tail).toContain("[phase log tail truncated to last 128 bytes]");
41+
expect(tail).not.toContain("�");
42+
expect(tail).toContain("y".repeat(79));
43+
});

0 commit comments

Comments
 (0)