Files
superpowers/skills/agentic-end-to-end-testing/driving-cli-tui.md
Drew Ritter 602ba17888 fix(e2e): gate tmux ownership on creation success
Make the copyable recipe independent of ambient set -e behavior. A failed tmux new-session now reports the failure and exits while session_owned is still false; only a successful creation reaches the ownership assignment.

A real collision reproduced the prior snippet killing its sentinel after marking a failed creation as owned. The guarded version preserved the exact sentinel identity, and three refined fresh-agent runs again preserved IDs 17, 18, and 19 while cleaning only their fallback sessions.
2026-08-06 15:04:25 -07:00

115 lines
4.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Driving a CLI / TUI (tmux)
Each scenario gets its own run-unique tmux session. Record ownership only after
creation succeeds, then clean up only that owned session. Fix the size for
deterministic capture; prefer the app's plain-text/inline mode if it has one.
## Ownership-safe recipe
```bash
scenario="widget-form"
session="${scenario}-$(date +%s)-$$"
stderr_log="/tmp/${session}-stderr.log"
session_owned=0
cleanup() {
if [ "$session_owned" -eq 1 ]; then
tmux kill-session -t "$session" 2>/dev/null || true
fi
}
trap cleanup EXIT
if ! tmux new-session -d -s "$session" -x 200 -y 50 "<cmd> 2>\"$stderr_log\""; then
echo "failed to create tmux session: $session" >&2
exit 1
fi
session_owned=1
tmux send-keys -t "$session" -l "literal text" # -l = no key-name parsing (paths, slashes)
tmux send-keys -t "$session" Enter
tmux capture-pane -t "$session" -p # -p = plain text; add -e only for styling
```
- `-x 200 -y 50` fixes the pane size so `capture-pane` output is deterministic
run to run — a resized pane reflows text differently.
- Always `-l` for user-typed strings; without it a literal path like
`/foo/bar` gets parsed as arrow-key escapes instead of typed characters.
- Redirect stderr to a file — panics, log lines, and debug probes land there,
not in the pane, so they won't show up in a `capture-pane` snapshot at all.
- The timestamp/PID suffix keeps the name readable and run-unique. If creation
still reports a collision, choose another unique name or report the failure.
Never kill the colliding session: `session_owned` remains `0`, so cleanup
cannot touch it.
## Form fill: send-keys patterns
`send-keys` parses keystrokes by name (`Enter`, `BTab`, `C-u`) unless you pass
`-l` for literal text. A typical field-by-field fill mixes both:
```bash
tmux send-keys -t "$session" "n" # tap a key to open the form
sleep 1
tmux send-keys -t "$session" BTab # shift-tab back one field
sleep 0.3
tmux send-keys -t "$session" C-u # clear the current line
sleep 0.3
tmux send-keys -t "$session" -l "some/literal/path" # literal — no key parsing
sleep 0.3
tmux send-keys -t "$session" Tab # forward to next field
sleep 0.3
tmux send-keys -t "$session" -l "text the user would type"
sleep 0.3
tmux send-keys -t "$session" Enter # submit
```
`sleep 0.3` between keys is usually enough; bump to 0.51.0s for field
transitions where the UI re-renders.
## Polling capture-pane for state
Poll `capture-pane -p` for a state string and grep the **glyph or word**, not
the color — `-p` drops ANSI styling by default (add `-e` only if you need
styling), and colors are also just harder to grep reliably than a fixed
glyph:
```bash
for i in $(seq 1 30); do
pane=$(tmux capture-pane -t "$session" -p)
echo "$pane" | grep -q "state: processing" && break
sleep 1
done
```
TUIs commonly use a distinct glyph per state, e.g. a Braille spinner (`⠋`)
while pending and an X mark (`✗`) on failure, with the glyph simply removed
once reconciled. Grep for the glyph itself, not for a color code.
## Two captures for optimistic UI
Mirror the web sync/async pattern: capture the pane immediately after the
triggering keypress, then again after a reconcile window. Without the
immediate capture you can't tell "rendered then reconciled" from "never
rendered":
```bash
tmux send-keys -t "$session" -l "trigger the optimistic action"
tmux send-keys -t "$session" Enter
echo "=== synchronous ===" ; tmux capture-pane -t "$session" -p | grep -E "pending-glyph"
sleep 6
echo "=== reconciled ===" ; tmux capture-pane -t "$session" -p | grep -E "pending-glyph" || echo "[no pending — reconciled]"
```
## Plain-text mode over the alt-screen buffer
If the TUI has a flag that disables its alternate-screen buffer (a debug or
plain-output mode), use it when launching under tmux. `capture-pane` then sees
plain scrollback text instead of raw escape sequences from a full-screen
redraw, which is much easier to grep.
## Non-interactive CLIs don't need tmux
If the surface under test is a one-shot command rather than an interactive
session, skip tmux entirely — run the command and capture its stdout/stderr
directly. The tmux machinery exists for interaction, not for driving a binary
in general. Still run it against a real, freshly built instance, not a stale
one left over from an earlier session.