#!/usr/bin/env python3 """Drive an interactive shell through a pseudo-terminal, and report what it drew. `cf sh` reads its keys off a terminal in raw mode and draws its lines back onto one, and refuses to start when either standard stream is redirected. So a test that runs it has to give it a terminal, and this is the terminal: a pty whose slave the shell is handed and whose master this holds, typing into one end and reading the drawing off the other. What it hands back is the drawing taken apart rather than the bytes. The shell paints through three writes (`lib/shuttle/paint.ts`), and two of them are told apart in the stream by construction: * a line being edited opens with `ESC 7` and then `CSI 0J`, and closes with `ESC 8` — the cursor is saved before the line is drawn and restored after it; * a line written above it opens with a `CSI 0J` that no save precedes, and closes with the CRLF ending it, ahead of the repaint of the line beneath. So what precedes the clear says which write this is, and what comes out is one record per line typed: the line, what the shell wrote above the next prompt in answer to it, and the prompt it then drew. That triple is the unit an assertion is written against, and the prompt is half of it — after a `cd`, where the prompt stands is the whole of what the verb did. Nothing here waits on a clock for progress. A line has settled when the shell draws a prompt with nothing typed at it, which it does once the line it was running answered and at no other time: every drawing made while a line is being typed carries what was typed after the `> `, and a line written above the prompt while one is in flight repaints an empty line rather than a prompt. Each read blocks until the terminal has bytes rather than asking again on an interval. The one deadline is a stuck-condition backstop, and it is the distinction `docs/development/waiting-in-tests.md` draws under "Browser-hosted unit tests have a harness backstop": a safety net at the harness level rather than a bound at the call site. No line is given a deadline of its own, because one per line would cap what each line can observe, which is the thing being avoided; this one only decides when to stop believing the shell will ever answer. That document is honest about what the shape costs and so is this: an early fire fails a session that would have finished. It is kept because the alternative — letting the continuous-integration step's own bound expire — takes the whole job down with nothing drawn to read, where this one fails with the transcript so far and says how long it waited. Usage: shuttle-terminal.py