Files
openclaw/test/scripts/parallels-phase-runner.test.ts
wangmiao0668000666 998f0ed27d 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>
2026-07-20 19:45:35 -07:00

44 lines
1.4 KiB
TypeScript
Raw Permalink Blame History

// 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));
});