Repository navigation
fix(exec): decode captureTail streams with StringDecoder for split UTF-8 - #1313
Conversation
|
No findings in the specified diff. The PR description includes sufficient testing: a representative built-CLI run with split UTF-8 output and before/after results, satisfying the real-CLI verification requirement. Review was read-only; I did not run, build, or install anything from the PR. |
SaulMoro
left a comment
There was a problem hiding this comment.
This was generated by AI during triage.
@hobostay Thanks, approving.
We ran teamai pull --silent with the built CLI on this head and on main 2401d047. The team's postPull script writes a Chinese message split in the middle of 标 across two writes:
- main: the
postPullline indebug.loghas U+FFFD replacement characters. - this PR: the message is intact (
错误:会话标题 café 😀 失败).
The four new split-UTF-8 cases fail on main and pass here. tsc, lint, and the exec and post-pull tests pass. Only pull --silent and the headless prePull/postDeploy paths reach captureTail with piped output. Plain pull inherits stdio.
Optional:
[P3 nit]The reader callsend()on the decoders. A read in the middle of a character would emit U+FFFD for the bytes held so far. Every current caller reads afterclose, so nothing hits it today.[P3 nit]The body names a second cause, interleaving through a shared callback. The old code kept no state between chunks, so per-chunk decoding is the only defect. Wording only.
Summary
captureTailinsrc/utils/exec.tsdecoded child process output chunks usingchunk.toString('utf8'). When multi-byte UTF-8 sequences (such as Chinese characters, emojis, or accented characters in script output or error logs) straddled chunk boundaries across pipe reads,toString('utf8')immediately turned partial bytes into\uFFFD, corrupting the error tail. In addition,stdoutandstderrshared the same unbuffered callback, allowing separate streams to interleave partial byte sequences.captureTailnow uses an independentStringDecoder('utf8')forstdoutandstderrand flushes any buffered remainder when the tail reader is called, matching the approach inexecCommandandclone.ts(#1281).Type of Change
Test Plan
npm run build— tsup built cleanlynpx tsc --noEmit— 0 type errorsnpm run lint— oxlint passed with 0 errors and 0 warnings on 1102 filesnpx vitest run src/__tests__/exec.test.ts— 11 passed (including 6 new unit tests forcaptureTailcovering split UTF-8 across chunks, separate stream decoders, whitespace collapsing, length bounds, and idempotent reader calls)npx vitest run src/__tests__/post-pull.test.ts src/__tests__/team-scripts.test.ts— 20 passedgit diff --check— clean whitespaceReal CLI Verification
Tested with the built CLI (
dist/index.js) in an isolated temporary workspace configured with apostPulllifecycle script inteamai.yaml. The child script emitted a multi-byte Chinese character (标) split across two pipe writes followed by exit 1.Before the fix (unbuffered
chunk.toString('utf8')):After the fix (using
StringDecoder('utf8')incaptureTail):The split multi-byte characters and emojis are decoded completely intact with no replacement characters in the captured tail.
Related Issues
Follow-up to #1281 (addressing the
captureTailfollow-up insrc/utils/exec.ts).