mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-07 18:42:25 +00:00
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 <noreply@anthropic.com> * 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 <noreply@anthropic.com> * refactor(e2e): reuse tailText wrapper for phase log tails Co-Authored-By: Claude <noreply@anthropic.com> * test(e2e): focus phase tail regression Co-authored-by: wangmiao0668000666 <wang.miao86@xydigit.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
committed by
GitHub
parent
f8380a8324
commit
998f0ed27d
@@ -3,6 +3,7 @@ import { appendFileSync } from "node:fs";
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { clampTimerTimeoutMs } from "@openclaw/normalization-core/number-coercion";
|
||||
import { tailText } from "../lib/text-file-utils.mjs";
|
||||
import { say, warn } from "./host-command.ts";
|
||||
|
||||
const PHASE_LOG_TAIL_MAX_BYTES = 512 * 1024;
|
||||
@@ -15,8 +16,8 @@ function appendTextTail(current: string, chunk: string, maxBytes: number): strin
|
||||
}
|
||||
const marker = `[phase log tail truncated to last ${maxBytes} bytes]\n`;
|
||||
const tailBytes = Math.max(0, maxBytes - Buffer.byteLength(marker));
|
||||
const tail = Buffer.from(combined).subarray(-tailBytes).toString("utf8");
|
||||
return `${marker}${tail}`;
|
||||
// tailText owns the UTF-8-safe byte truncation; the marker keeps the tail self-describing.
|
||||
return `${marker}${tailText(combined, tailBytes)}`;
|
||||
}
|
||||
|
||||
function resolvePhaseTimeoutMs(timeoutSeconds: number): number {
|
||||
|
||||
43
test/scripts/parallels-phase-runner.test.ts
Normal file
43
test/scripts/parallels-phase-runner.test.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// Parallels Phase Runner tests cover bounded in-memory phase log tails.
|
||||
import { afterEach, expect, it, vi } from "vitest";
|
||||
import { PhaseRunner } from "../../scripts/e2e/parallels/phase-runner.ts";
|
||||
import { cleanupTempDirs, makeTempDir } from "../helpers/temp-dir.js";
|
||||
|
||||
const tempRoots: string[] = [];
|
||||
|
||||
function makeTempRoot() {
|
||||
return makeTempDir(tempRoots, "openclaw-parallels-phase-runner-");
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
cleanupTempDirs(tempRoots);
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
async function captureFailedPhaseTail(runner: PhaseRunner, text: string): Promise<string> {
|
||||
const written: string[] = [];
|
||||
vi.spyOn(process.stderr, "write").mockImplementation((chunk) => {
|
||||
written.push(String(chunk));
|
||||
return true;
|
||||
});
|
||||
await expect(
|
||||
runner.phase("utf8-tail", 60, () => {
|
||||
runner.append(text);
|
||||
throw new Error("boom");
|
||||
}),
|
||||
).rejects.toThrow("boom");
|
||||
return written.join("");
|
||||
}
|
||||
|
||||
it("keeps the truncated phase tail UTF-8 safe when the byte cut splits a character", async () => {
|
||||
// 134 bytes total; the retained window starts one byte into the 4-byte
|
||||
// emoji, so a byte-naive decode would emit replacement characters.
|
||||
const tail = await captureFailedPhaseTail(
|
||||
new PhaseRunner(makeTempRoot(), 128),
|
||||
`${"x".repeat(50)}😀${"y".repeat(79)}`,
|
||||
);
|
||||
|
||||
expect(tail).toContain("[phase log tail truncated to last 128 bytes]");
|
||||
expect(tail).not.toContain("<22>");
|
||||
expect(tail).toContain("y".repeat(79));
|
||||
});
|
||||
Reference in New Issue
Block a user