fix(e2e): keep phase log tails UTF-8 safe#109923
Conversation
|
Codex review: needs maintainer review before merge. Reviewed July 17, 2026, 2:08 PM ET / 18:08 UTC. Summary PR surface: Tests +63, Other +1. Total +64 across 2 files. Reproducibility: yes. A production PhaseRunner driver positions an emoji across the retained byte boundary, reproduces U+FFFD on main, and the regression test was control-red against the unpatched implementation. Review metrics: none identified. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Risk before merge
Maintainer options:
Next step before merge
Security Review detailsBest possible solution: Land the current canonical-helper implementation and regression tests after refreshing against current main and confirming the exact head passes the required checks. Do we have a high-confidence way to reproduce the issue? Yes. A production PhaseRunner driver positions an emoji across the retained byte boundary, reproduces U+FFFD on main, and the regression test was control-red against the unpatched implementation. Is this the best way to solve the issue? Yes. Reusing the merged AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against f104c51e8598. Label changesLabel changes:
Label justifications:
Evidence reviewedPR surface: Tests +63, Other +1. Total +64 across 2 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
|
Fixed the Focused proof: |
|
ClawSweeper P1 fixed in 927deb9 (pushed; new head).
Validation (Node 24.15.0):
|
927deb9 to
3e19f99
Compare
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 (openclaw#109669) so the retained window starts on a character boundary. Co-Authored-By: Claude <[email protected]>
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]>
Co-Authored-By: Claude <[email protected]>
Co-authored-by: wangmiao0668000666 <[email protected]>
3e19f99 to
0eb4532
Compare
|
Merged via squash.
|
* 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 (openclaw#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]>
What Problem This Solves
PhaseRunner(scripts/e2e/parallels/phase-runner.ts) keeps a bounded in-memory log tail per phase (appendTextTail) and writes that tail to stderr when a phase fails. The bound is applied as a raw byte cut —Buffer.from(combined).subarray(-tailBytes).toString("utf8")— so when the retained window starts in the middle of a multi-byte UTF-8 character (emoji/CJK in test output), the diagnostics tail begins with U+FFFD replacement characters.This is the same bug family as merged #109669 (
scripts/e2e/lib/text-file-utils.mjs) and open sibling #109670 (scripts/e2e/parallels/host-server.ts);phase-runner.tsis the remaining byte-based tail decoder in the parallels harness. The three PRs touch separate files and are not stacked; any order merges cleanly.Why This Change Was Made
#109669 introduced
decodeUtf8Tail, which drops leading UTF-8 continuation bytes before decoding a truncated window. This PR exports that helper and reuses it inappendTextTail, so the byte cap stays unchanged while the emitted tail always starts on a character boundary. The change is three lines in prod code plus a focused regression test.Root cause on
main(scripts/e2e/parallels/phase-runner.ts:18): the byte cut fromsubarray(-tailBytes)can start on a UTF-8 continuation byte, andtoString("utf8")replaces each orphaned continuation byte with U+FFFD.Deliberately out of scope:
scripts/e2e/parallels/filesystem.tshas a similar byte-window read, but its only consumer is a regex scan (extractLastOpenClawVersionFromLog) whose ASCII pattern cannot straddle a character boundary, so a partial leading character there has no observable effect.User Impact
Maintainers reading failed E2E phase output get a clean log tail instead of leading mojibake whenever multibyte text (emoji in test names/output, CJK) crosses the 512 KiB tail cutoff.
Evidence
Real behavior proof against the production
PhaseRunneronmain(driver: construct with a 128-byte cap,append()134 bytes containing an emoji positioned so the byte window starts one byte into it, fail the phase, capture stderr):Before (origin/main):
After (this branch):
Regression coverage:
test/scripts/parallels-phase-runner.test.tsdrives the real class through a failing phase and asserts the stderr tail (1) contains no U+FFFD when the byte cut splits a character, (2) keeps the full emoji tail when the cut lands on a character boundary, (3) leaves short logs untruncated.node scripts/run-vitest.mjs test/scripts/parallels-phase-runner.test.ts— 3/3 pass on this branch; the split-character test fails on unpatchedphase-runner.ts(control-red verified by reverting the prod file toorigin/main).node scripts/run-vitest.mjs test/scripts/e2e-text-file-utils.test.ts— pass (sibling helper tests unaffected by the new export).oxfmt --checkon all touched files — clean;oxlinton touched files — 0 findings;tsgo --noEmit -p tsconfig.scripts.json— no errors in touched files (pre-existingpackages/net-policyerrors come from a stale local[email protected], unrelated).git diff --check— clean.