fix(e2e): scope tmux cleanup to owned sessions

Replace deterministic shared tmux names and preemptive deletion with a readable run-unique name, an ownership bit set only after successful creation, and cleanup guarded by that ownership. A collision now fails closed: the runner chooses another unique name or reports failure and never reclassifies the existing session as stale.\n\nwriting-skills pressure evidence used three fresh subjects per arm with real sentinel sessions. RED was 0/3: every subject followed the old recipe by killing the pre-existing session, recreating the same name, and cleaning it again. GREEN was 3/3: every subject preserved the original session identity, created a distinct owned session, captured READY, and removed only the owned session. The controller independently verified original IDs $9, $10, and $11 before removing the disposable sentinels.
This commit is contained in:
Drew Ritter
2026-08-06 14:57:57 -07:00
parent 7dff0b2d48
commit b994fedcca

View File

@@ -1,16 +1,29 @@
# Driving a CLI / TUI (tmux)
Each scenario gets its own named tmux session (cleanup needs a deterministic
name). Fix the size for deterministic capture; prefer the app's plain-text/inline
mode if it has one.
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.
## The four-command recipe
## Ownership-safe recipe
```bash
tmux new-session -d -s <name> -x 200 -y 50 "<cmd> 2>/tmp/<name>-stderr.log"
tmux send-keys -t <name> -l "literal text" # -l = no key-name parsing (paths, slashes)
tmux send-keys -t <name> Enter
tmux capture-pane -t <name> -p # -p = plain text; add -e only for styling
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
tmux new-session -d -s "$session" -x 200 -y 50 "<cmd> 2>\"$stderr_log\""
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
@@ -19,13 +32,10 @@ tmux capture-pane -t <name> -p # -p = plain text; add -e only for
`/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.
Kill any leftover session with the same name before starting a new one, so
reruns don't attach to a stale process:
```bash
tmux kill-session -t <name> 2>/dev/null # idempotent: fine if nothing to kill
```
- 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
@@ -33,19 +43,19 @@ tmux kill-session -t <name> 2>/dev/null # idempotent: fine if nothing to kill
`-l` for literal text. A typical field-by-field fill mixes both:
```bash
tmux send-keys -t <name> "n" # tap a key to open the form
tmux send-keys -t "$session" "n" # tap a key to open the form
sleep 1
tmux send-keys -t <name> BTab # shift-tab back one field
tmux send-keys -t "$session" BTab # shift-tab back one field
sleep 0.3
tmux send-keys -t <name> C-u # clear the current line
tmux send-keys -t "$session" C-u # clear the current line
sleep 0.3
tmux send-keys -t <name> -l "some/literal/path" # literal — no key parsing
tmux send-keys -t "$session" -l "some/literal/path" # literal — no key parsing
sleep 0.3
tmux send-keys -t <name> Tab # forward to next field
tmux send-keys -t "$session" Tab # forward to next field
sleep 0.3
tmux send-keys -t <name> -l "text the user would type"
tmux send-keys -t "$session" -l "text the user would type"
sleep 0.3
tmux send-keys -t <name> Enter # submit
tmux send-keys -t "$session" Enter # submit
```
`sleep 0.3` between keys is usually enough; bump to 0.51.0s for field
@@ -60,7 +70,7 @@ glyph:
```bash
for i in $(seq 1 30); do
pane=$(tmux capture-pane -t <name> -p)
pane=$(tmux capture-pane -t "$session" -p)
echo "$pane" | grep -q "state: processing" && break
sleep 1
done
@@ -78,11 +88,11 @@ immediate capture you can't tell "rendered then reconciled" from "never
rendered":
```bash
tmux send-keys -t <name> -l "trigger the optimistic action"
tmux send-keys -t <name> Enter
echo "=== synchronous ===" ; tmux capture-pane -t <name> -p | grep -E "pending-glyph"
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 <name> -p | grep -E "pending-glyph" || echo "[no pending — reconciled]"
echo "=== reconciled ===" ; tmux capture-pane -t "$session" -p | grep -E "pending-glyph" || echo "[no pending — reconciled]"
```
## Plain-text mode over the alt-screen buffer