Skip to content

fix(tty): wait for the app to finish drawing before a shot - #9

Merged
Shironex merged 7 commits into
mainfrom
fix/tty-frame-settle
Sep 27, 2026
Merged

Shironex merged 7 commits into
mainfrom
fix/tty-frame-settle

Conversation

@Shironex

@Shironex Shironex commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Issue #3: a tty shot could be taken mid-frame. The root cause is confirmed: a macOS pty hands the fixture's details frame (one 1217-byte write) to the kit as a 1024-byte read that ends exactly at oran, plus a 193-byte read 0.05 to 0.8 ms later (logged over 10 runs). waitFor: 'name: postgres-main' matched on the first read and the shutter fired before the second arrived, which is the cut-off image in the issue.

  • Before every tty shot (after waitFor, after the change wait for keys without waitFor, after nav, and for the first shot after ready or a restart), the kit now waits until the flushed screen has not changed for 100 ms, polled every 25 ms. It compares screens, not raw output, so an app that redraws the same frame on a timer settles at once.
  • The wait is capped at 1 s, and never runs past the shot's remaining timeouts.shotMs. An app whose screen never stops changing (a spinner, a clock) is shot when the cap runs out, not failed, with one warning per shot that points to the terminal determinism guide.
  • All three are internal constants with comments on why their values. No new config key, and the exported TtySession type is unchanged.
  • Clip waitFor steps get no settle wait: the recorder samples on its own frame clock by design, and a quiet wait would break that lockstep timing.

CodeQL js/polynomial-redos alerts 1 to 4. A small linear helper in src/text.ts (trimTrailing, trailingRunStart: a loop from the end) replaces the four flagged regexes (src/capture.ts, src/config/resolve.ts x2, src/tty/pty.ts). It also replaces four trims of the same shape that CodeQL did not flag (/\n+$/ in src/record.ts and src/tty/capture.ts, / +$/ in src/tty/render.ts and src/tty/session.ts). Behaviour is unchanged, byte for byte.

Also: macos-latest is added to the build-test matrix (action SHAs unchanged), the guide lines about tty shot timing are updated, the CONTRIBUTING lines about the CI platforms and the fixture TUI's env vars are updated, and there is a patch changeset.

Test plan

  • Regression test (waits for a frame that arrives in two parts before any kind of shot): a new fixture mode, TUI_SPLIT_MS=70, writes the bottom three rows 70 ms after the rest of each frame. On the old code it failed 10 of 10 runs, with every kind of shot (first, waitFor, keys only, restart) missing its bottom rows.
  • bunx vitest run test/capture-tty.test.ts 20 times on this Mac: 6 of 20 failed before (always the determinism test), 0 of 20 failed after.
  • An app that redraws identical frames every 20 ms (TUI_REDRAW_MS) settles with no warning. An app that never stops changing (counter.mjs) is shot, with the warning, at the 1 s cap, and within a 300 ms shotMs when that is shorter.
  • Each new test went red when the thing it guards was broken: comparing snapshots by identity instead of key, removing the cap, ignoring shotMs, a regex inside the helper, an off-by-one in the helper.
  • Pathological input: '/'.repeat(n) + 'x' takes 1405 ms (n = 50k) and 5921 ms (n = 100k) with /\/+$/. With trimTrailing it takes 0.04 ms and 0.01 ms, and 0.11 ms at n = 1M.
  • bun install --frozen-lockfile, bun run typecheck, bun run test (twice, 246 passed, 2 Windows-only skipped), bun run --cwd site test, bun run docs:build, all on macOS.
  • CI green on ubuntu, windows and the new macOS leg. CodeQL reports no new alerts, and alerts 1 to 4 have no instance on the PR merge ref, so they should close on merge.

Closes #3

@Shironex Shironex added bug Something isn't working area: capture url and cdp capture: web apps, Electron and Tauri web builds (src/capture.ts, src/browser.ts) area: tty Terminal apps: tty mode and the terminal engine (src/tty, src/config/tty.ts) area: config Loading, validating and resolving the config, defineConfig and the types (src/config) platform: macos Only happens on macOS labels Sep 27, 2026
@Shironex
Shironex merged commit d30152c into main Sep 27, 2026
8 checks passed
@github-actions github-actions Bot mentioned this pull request Sep 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: capture url and cdp capture: web apps, Electron and Tauri web builds (src/capture.ts, src/browser.ts) area: config Loading, validating and resolving the config, defineConfig and the types (src/config) area: tty Terminal apps: tty mode and the terminal engine (src/tty, src/config/tty.ts) bug Something isn't working platform: macos Only happens on macOS

Projects

None yet

Development

Successfully merging this pull request may close these issues.

tty capture can shoot a frame the app has not finished drawing (flaky determinism test on macOS)

1 participant