| name | testing-tui-runtime |
| description | How to drive the ralph-tui TUI (`run`, watch mode, pause/quit paths) end-to-end in a real terminal with a fake agent and a scratch beads tracker, without AI credentials. |
Runtime/E2E testing of the ralph-tui TUI
Use this when a change must be proven in the real TUI (watch mode, engine status labels,
pause/resume, shutdown/quit paths) rather than with unit tests.
Build & run
cd /path/to/ralph-tui && bun run build
Run the built CLI (not bun run dev) so behaviour matches shipped code:
bun /path/to/ralph-tui/dist/cli.js run [flags]
Scratch tracker (never touch the repo's own .beads)
Create a throwaway project dir with its own beads store and drive scenarios with bd:
export RALPH_TEST_DIR="$(mktemp -d "${TMPDIR:-/tmp}/ralph-tui-run.XXXXXX")" && cd "$RALPH_TEST_DIR"
bd init
bd create "Watch mode epic" --type epic -p 1
bd create "Alpha task" --type task -p 2 --parent <epic-id>
A fresh dir per run, not a fixed one: a reused dir carries over .beads tasks, the
.ralph-tui session file, locks and guards — exactly the things most scenarios here assert on, so
a Recovered stale session banner or a stale-lock prompt from the previous run reads as a
finding of the build under test. The unique dir also gives the run a unique epic id, which is how
you address its process (see the signal trap below).
Gotchas:
bd commands must be run with the scratch dir as the working directory, otherwise you get
"no beads database found".
- With
--tracker beads, the TUI requires --epic <id>; without it the TUI shows
"No epics found" and interactive epic selection blocks automation.
br (beads-rust) may not be installed even though AGENTS.md references it; bd works.
Fake agents (no AI credentials needed)
Put plugins in ~/.config/ralph-tui/plugins/agents/:
echo.ts — returns echo-agent: local fake response\n<promise>COMPLETE</promise> instantly.
slowecho.ts — same but sleeps ~25s in interruptible 500 ms chunks. Essential for testing
quit/pause mid-iteration; an instant agent gives you no window to interrupt.
Select with --agent echo / --agent slowecho.
Making timing visible on screen
Wrap the CLI so quit latency is provable in the recording:
#!/bin/bash
cd "${RALPH_TEST_DIR:?export RALPH_TEST_DIR to the scratch dir first}"
export PATH="$PATH:$HOME/.local/bin"
echo "START $(date +%H:%M:%S.%3N) args: $*"
bun /path/to/ralph-tui/dist/cli.js run "$@"
echo "=== PROCESS EXITED code=$? at $(date +%H:%M:%S.%3N) ==="
exec bash
Typical invocation:
./runwatch.sh --watch --poll 60 --agent echo --tracker beads --epic <epic-id> --no-setup --force
Use a long --poll (60s) to make a stalled pause/quit obvious, and a short one (10s) when you
want auto-pickup to happen quickly.
TUI keys
s start · p pause/resume · q → quit dialog → y · d dashboard · r refresh.
Header status labels: Waiting (watch idle), Selecting, Executing, Paused, Complete.
Dashboard shows Waiting for new tasks when watch-idle.
Known traps
- Interrupt (Ctrl+C) path: on builds that include the #431 fix (
src/tui/utils/keyboard-shortcuts.ts
onInterruptRequest in RunApp.tsx + exitSignals: ['SIGQUIT','SIGABRT'] in run.tsx),
keyboard Ctrl+C and external kill -INT <pid> both render the ⚠ Interrupt Ralph? dialog with
the display intact; n/Esc cancel, y exits code 0 in ~2s and resets the active task to
open, and a second Ctrl+C within ~1s force-quits via process.exit(1) (wrapper code=1,
terminal left un-repainted; on builds whose exit handler calls releaseLockSync the force quit
no longer leaves a lock file behind, on older ones it does).
On older builds Ctrl+C is a complete no-op and kill -INT blanks the display and hangs, so
always confirm which build you are on before blaming a change.
- Ctrl+Shift+C is indistinguishable from Ctrl+C in most terminals: konsole sends byte
0x03
for both (verify with stty -isig; cat -v — Ctrl+Shift+C prints ^C, Alt+C prints ^[c), so the
"copy" shortcut opens the interrupt dialog whenever the terminal does not consume it itself
(konsole only consumes it when a konsole-level text selection exists). Alt+C is safely distinct.
Clipboard contents cannot be verified on a box without xclip/xsel/wl-paste (that is what
src/utils/clipboard.ts shells out to on Linux) — record the clipboard write as untested.
- Early-startup signals need timing, and are quirky: the app's SIGINT/SIGTERM handlers are not
installed for roughly the first ~0.7s. Drive it with
( sleep 0.5; kill -INT "$(pgrep -f "cli\.js run.*--epic $EPIC_ID")" ) & ./runwatch.sh ...,
where $EPIC_ID is this run's epic — check the pattern matches exactly one pid
(pgrep -cf ...) before signalling, and note the wrapper script is a different process from
the CLI it launches, so signal the pid you matched rather than the wrapper. Do not use
pkill -f 'dist/cli.js run': it hits every ralph run on the box, including a parallel test or a
main worktree build you are comparing against, and mutates their session and task state.
Historic behaviour (before
the #434 fix in ): SIGINT in that window exited but left a stale lock,
and SIGTERM was (the TUI mounted and kept running). On builds where
owns the startup signals (guarded by
), expect for SIGTERM / for SIGINT at
every delay in the window, with removed; a signal landing
(~0.05s) uses default disposition but cannot leave a lock. Always sweep
several delays (0.05/0.2/0.3/0.4/0.5/0.7s), log that the signal was actually sent, and check
plus the next launch's banner each time. Two signals in quick
succession inside the window should still yield a single clean 130/143, never .
Devin Secrets Needed
None — the fake agent plugins and the local bd store avoid all external credentials.