feat: record and replay a PTY stream, so a redraw glitch becomes a test - #379
feat: record and replay a PTY stream, so a redraw glitch becomes a test#379kshivang wants to merge 1 commit into
Conversation
Chasing an intermittent redraw glitch from a screenshot does not work. I tried it this week: I formed three theories about the terminal's wrap and batch logic from one image, and disproved all three by reading the code afterwards. A picture shows that something is wrong, not what. The byte stream is a different kind of evidence. It is deterministic: the same bytes must always produce the same grid. So a recording turns "sometimes the wrapped line garbles" into a failing test. This matters here specifically because of HOW the affected apps redraw. A capture of Claude Code's spinner shows every frame bracketed in DEC 2026, rewriting only the characters that changed, with absolute column addressing and no line clear: CSI 35 C "th" CSI 39 G "nking more with xhigh ef" CSI 64 G "ort" It is diffing against its own model of our grid. One cell of disagreement anywhere becomes permanent visible corruption, because nothing repaints the line to resync, and it surfaces later somewhere unrelated to where it started. Replaying a recording and comparing grids is how that gets localised to the chunk that caused it. Three parts: 1. BOSSTERM_PTY_LOG=1 starts per-tab recording (or set it to a directory). startFileLogging already existed and had NO callers - the facility was there and unreachable, the same shape of dead flag as setEnabled was in this class. 2. The escaper is now lossless. A literal backslash was written through unescaped, so "\e" in output was ambiguous between an escape character and the two characters \ and e - readable, but not replayable. Any Windows path or regex in the output would have replayed as a control sequence. DEL is escaped too. 3. PtyReplay (test sources) parses a recording and feeds it through a real BossEmulator headlessly, returning the grid. Chunk boundaries are preserved rather than concatenated, because a CSI split across two reads is exactly the kind of thing a recording should be able to re-expose. The round-trip test goes through the production writeChunkToFile rather than a reimplementation of it, so the two halves cannot drift apart silently; removing the backslash escape fails it. Writing the differential-redraw test also produced a small correction worth keeping: I expected "thinking more" and got "thinking moreg". The emulator is right - without a trailing clear the previous frame's tail survives, which is precisely the bug class here. Both behaviours are now pinned, with and without the CSI K the real capture ends on.
Review: record and replay a PTY stream (1/3 — the blocking one)The framing is right, and the framing is the valuable part: a byte stream is deterministic evidence and a screenshot isn't, so turning a recording into a fixture is exactly the right move for a differential-redraw bug. Three things worth calling out as genuinely good before the findings:
Findings across three comments, most consequential first. 1. The recording is not lossless for non-ASCII —
|
Review 2/3 — wiring, lifecycle, and the log itself3. The third collector construction site is missed
No in-repo caller today (it's the embedder-facing SSH pre-connect API), so impact is low — but the failure is silent: 4. Two tabs created in the same second overwrite each other's recordingval safe = tabLabel.replace(Regex("[^A-Za-z0-9._-]"), "_").take(40)
val stamp = SimpleDateFormat("yyyyMMdd-HHmmss").format(Date())
An 5.
|
Review 3/3 — smaller things, and the verdictDangling KDoc. The new doc block was inserted between The tag strings are duplicated and can drift.
Replay fidelity gaps. Production does
Last chunk's trailing partial grapheme is dropped. Autoflush on the PTY reader thread. Style nit. Things I checked that are fine: the line regex can't be confused by payload content (all newlines are escaped, so one chunk is exactly one line); Verdict: the direction is right and the tests are well-chosen. #1 is the one I'd treat as blocking — it silently falsifies the PR's central "lossless / replays byte-for-byte" claim on Windows and under a non-UTF-8 CI locale, and it fails in the most misleading way possible. #2–#4 are cheap and materially affect whether a captured fixture is usable at all. The rest can land as follow-ups. Reviewed statically — I couldn't run |
Why
Chasing the intermittent redraw glitch from a screenshot didn't work. I formed three theories about the terminal's wrap and batch logic from one image and disproved all three by reading the code afterwards. A picture shows that something is wrong, not what.
The byte stream is different evidence: it's deterministic. The same bytes must always produce the same grid. So a recording turns "sometimes the wrapped line garbles" into a failing test.
What the capture showed
Every frame of Claude Code's spinner is bracketed in DEC 2026 and rewrites only the characters that changed, using absolute column addressing and no line clear:
still thinking with xhigh effort→thinking more with xhigh effort, rewriting cols 36-37, skipping col 38 (i→i), resuming at 39.It is diffing against its own model of our grid. One cell of disagreement anywhere becomes permanent visible corruption, because nothing ever repaints the line to resync — and it surfaces later, somewhere unrelated to where it started. That is why it looks intermittent, and why the wrap is where it shows rather than necessarily where it's caused.
What's here
1. Recording.
BOSSTERM_PTY_LOG=1starts per-tab capture (or set it to a directory).startFileLoggingalready existed and had no callers — the facility was there and unreachable, the same shape of dead flag assetEnabledwas in this class.2. A lossless escaper. A literal backslash was written through unescaped, so
\ewas ambiguous between an escape character and the two characters\ande. Readable, but not replayable — any Windows path or regex in the output would have replayed as a control sequence. DEL is escaped too.3.
PtyReplay(test sources) parses a recording and feeds it through a realBossEmulatorheadlessly, returning the grid. Chunk boundaries are preserved rather than concatenated, because a CSI split across two reads is exactly what a recording should be able to re-expose.How to use it
BOSSTERM_PTY_LOG=1 ./gradlew :bossterm-app:run --no-daemon # reproduce the glitch, then grab ~/.bossterm/pty-log/<stamp>-<tab>.logDrop the log in as a fixture, assert the grid, and the glitch is a deterministic regression test.
Verification
The round-trip test goes through the production
writeChunkToFilerather than a reimplementation, so the two halves can't drift apart silently. Removing the backslash escape fails it — mutation-checked.Writing the differential-redraw test also produced a correction worth keeping: I expected
"thinking more"and got"thinking moreg". The emulator is right — without a trailing clear the previous frame's tail survives, which is precisely the bug class here. Both behaviours are now pinned, with and without theCSI Kthe real capture ends on.Test-only additions plus two small production changes (env wiring, escaper). Full suite green in both modules.
Not claimed
This does not fix the glitch. It makes the next occurrence diagnosable instead of speculative.