tmux-cli-test
npx skills add https://github.com/gpu-cli/skills --skill tmux-cli-test
Agent 安装分布
Skill 文档
tmux CLI Testing
Test CLI applications by running them in tmux sessions, waiting for conditions, sending input, and asserting on output. Never sleep – always wait on a condition.
Helpers
Source the helper script for all primitives:
source .claude/skills/tmux-cli-test/scripts/tmux_helpers.sh
Quick Reference
| Function | Purpose |
|---|---|
tmux_start <session> <cmd> |
Launch command in detached tmux session |
tmux_kill <session> |
Kill session |
tmux_is_alive <session> |
Check if session is running |
tmux_capture <session> |
Get pane text |
tmux_capture_ansi <session> |
Get pane text with ANSI codes |
tmux_wait_for <session> <text> [timeout] |
Poll until text appears |
tmux_wait_for_regex <session> <pattern> [timeout] |
Poll until regex matches |
tmux_wait_gone <session> <text> [timeout] |
Poll until text disappears |
tmux_wait_exit <session> [timeout] |
Poll until process exits |
tmux_send <session> <keys...> |
Send keys (tmux key names) |
tmux_type <session> <text> |
Type literal text |
tmux_assert_contains <session> <text> [label] |
Assert text present |
tmux_assert_not_contains <session> <text> [label] |
Assert text absent |
tmux_assert_matches <session> <pattern> [label] |
Assert regex matches |
tmux_send_and_wait <session> <keys> <text> [timeout] |
Send then wait |
tmux_test <session> <cmd> <ready_text> <fn> |
Full lifecycle test |
Configuration
Override defaults before calling functions:
TMUX_TEST_POLL_INTERVAL=0.3 # seconds between polls
TMUX_TEST_TIMEOUT=30 # max wait seconds
TMUX_TEST_WIDTH=120 # terminal columns
TMUX_TEST_HEIGHT=30 # terminal rows
Workflow
Every test follows this pattern:
1. Start session with command
2. Wait for ready signal (text appearing)
3. Interact (send keys, wait for responses)
4. Assert on captured output
5. Kill session
Minimal Example
source .claude/skills/tmux-cli-test/scripts/tmux_helpers.sh
tmux_start "test-help" "./crates/target/debug/gpu --help"
tmux_wait_for "test-help" "Usage:" 10
tmux_assert_contains "test-help" "run"
tmux_assert_contains "test-help" "dashboard"
tmux_kill "test-help"
Using tmux_test for Lifecycle
test_help_page() {
local s="$1"
tmux_assert_contains "$s" "run" "help shows run command"
tmux_assert_contains "$s" "dashboard" "help shows dashboard command"
tmux_assert_not_contains "$s" "ERROR" "no errors in help"
}
tmux_test "help-test" "./crates/target/debug/gpu --help" "Usage:" test_help_page
GPU CLI Patterns
Testing the Dashboard (TUI)
source .claude/skills/tmux-cli-test/scripts/tmux_helpers.sh
TMUX_TEST_WIDTH=140
TMUX_TEST_HEIGHT=35
GPU_BIN="./crates/target/debug/gpu"
tmux_start "dash" "$GPU_BIN dashboard"
tmux_wait_for "dash" "Pods" 15
# Verify panels rendered
tmux_assert_contains "dash" "Pods" "pods panel visible"
tmux_assert_contains "dash" "Jobs" "jobs panel visible"
# Navigate with keys
tmux_send "dash" j
tmux_send "dash" j
tmux_wait_for "dash" "â¶" 5 # selection cursor
# Switch panel
tmux_send "dash" Tab
tmux_wait_for_regex "dash" "Jobs.*selected" 5
# Open help overlay
tmux_send "dash" '?'
tmux_wait_for "dash" "Help" 5
tmux_assert_contains "dash" "Keybindings" "help shows keybindings"
# Close help
tmux_send "dash" Escape
tmux_wait_gone "dash" "Keybindings" 5
# Quit
tmux_send "dash" q
tmux_wait_exit "dash" 5
Testing Interactive Prompts
tmux_start "init" "$GPU_BIN init"
tmux_wait_for "init" "project" 10
# Type project name
tmux_type "init" "my-test-project"
tmux_send "init" Enter
# Wait for next prompt
tmux_wait_for "init" "GPU" 10
# Select GPU with arrow keys
tmux_send "init" j j Enter
tmux_wait_for "init" "provider" 10
tmux_kill "init"
Testing Command Output
tmux_start "status" "$GPU_BIN status"
tmux_wait_for_regex "status" "(No active|Pod)" 10
FRAME=$(tmux_capture "status")
if echo "$FRAME" | grep -q "No active"; then
echo "No pods running - expected for cold test"
elif echo "$FRAME" | grep -q "Pod"; then
tmux_assert_matches "status" "Pod.*READY\|ACTIVE" "pod in valid state"
fi
tmux_wait_exit "status" 15
Testing Error Handling
tmux_start "bad-cmd" "$GPU_BIN run --nonexistent-flag"
tmux_wait_for_regex "bad-cmd" "error|Error|unknown" 10
tmux_assert_not_contains "bad-cmd" "panic" "no panics on bad input"
tmux_wait_exit "bad-cmd" 5
Testing Long-Running Commands with Ctrl-C
tmux_start "run-job" "$GPU_BIN run python -c 'import time; time.sleep(3600)'"
tmux_wait_for "run-job" "Running" 30
# Interrupt
tmux_send "run-job" C-c
tmux_wait_for_regex "run-job" "cancel|interrupt|stopped" 10
tmux_wait_exit "run-job" 10
Docker Containers
For testing CLI apps running inside Docker containers (e.g., FTR testing), source the Docker helpers:
source .claude/skills/tmux-cli-test/scripts/tmux_docker_helpers.sh
TMUX_DOCKER_CONTAINER="gpu-ftr-alex-chen-001"
# TMUX_DOCKER_SESSION="test" # default, override if needed
Set TMUX_DOCKER_CONTAINER once, then all docker_tmux_* calls route through docker exec to that container. Same API as the local helpers but without session/container args:
| Function | Purpose |
|---|---|
docker_tmux_send <keys...> |
Send tmux keys |
docker_tmux_type <text> |
Type literal text |
docker_tmux_capture |
Get pane text |
docker_tmux_capture_ansi |
Get pane text with ANSI codes |
docker_tmux_wait_for <text> [timeout] |
Poll until text appears |
docker_tmux_wait_regex <pattern> [timeout] |
Poll until regex matches |
docker_tmux_wait_gone <text> [timeout] |
Poll until text disappears |
docker_tmux_assert_contains <text> [label] |
Assert text present |
docker_tmux_assert_not_contains <text> [label] |
Assert text absent |
docker_tmux_assert_matches <pattern> [label] |
Assert regex matches |
docker_tmux_send_and_wait <keys> <text> [timeout] |
Send then wait |
Docker Example
source .claude/skills/tmux-cli-test/scripts/tmux_docker_helpers.sh
TMUX_DOCKER_CONTAINER="gpu-ftr-alex-chen-001"
docker_tmux_send "gpu dashboard" Enter
docker_tmux_wait_for "Pods" 15
docker_tmux_capture
docker_tmux_send j
docker_tmux_send "?"
docker_tmux_wait_for "Help" 5
docker_tmux_assert_contains "Keybindings" "help shows keybindings"
docker_tmux_send Escape
docker_tmux_wait_gone "Help" 5
docker_tmux_send q
Writing Tests Inline
When no helper script is needed (quick one-off checks), use tmux commands directly:
# Start
tmux new-session -d -s test -x 120 -y 30 "./crates/target/debug/gpu dashboard"
# Poll for ready (inline wait loop)
for i in $(seq 1 100); do
tmux capture-pane -t test -p | grep -q "Pods" && break
sleep 0.3
done
# Assert
FRAME=$(tmux capture-pane -t test -p)
echo "$FRAME" | grep -q "Pods" && echo "PASS" || echo "FAIL"
# Cleanup
tmux send-keys -t test q
tmux kill-session -t test 2>/dev/null
Anti-Patterns
| Bad | Good | Why |
|---|---|---|
sleep 3 |
tmux_wait_for s "Ready" |
Sleeps are flaky and slow |
sleep 5 && tmux capture-pane |
tmux_wait_for s "expected" && tmux_capture s |
Condition, not duration |
| Hardcoded binary path | GPU_BIN=./crates/target/debug/gpu |
Easy to switch debug/release |
| No cleanup on failure | tmux_test or explicit tmux_kill |
Leftover sessions break next run |
grep -q without timeout loop |
tmux_wait_for |
Text may not be rendered yet |
Checking .len() of TUI text |
Check display content only | Unicode width != byte length |
Debugging Failed Tests
When a test fails, capture the frame for diagnosis:
# Capture what's actually on screen
tmux_capture "session-name"
# Save to file for comparison
tmux_capture_to_file "session-name" "/tmp/failed-frame.txt"
# Capture with colors to verify styling
tmux_capture_ansi "session-name"
Session Naming Convention
Use descriptive, prefixed session names to avoid collisions:
gpu-test-dashboard
gpu-test-init
gpu-test-run-basic
gpu-test-status
gpu-test-error-handling