|
| 1 | +# tmux Knowledge for CLI Testing |
| 2 | + |
| 3 | +This document covers essential knowledge for using tmux to test and automate the Codebuff CLI. |
| 4 | + |
| 5 | +## Critical: Sending Input to the CLI |
| 6 | + |
| 7 | +**Standard `tmux send-keys` does NOT work with the Codebuff CLI.** Characters are dropped or garbled due to how OpenTUI handles keyboard input. |
| 8 | + |
| 9 | +### The Problem |
| 10 | + |
| 11 | +```bash |
| 12 | +# ❌ BROKEN: Characters get dropped - only last character appears |
| 13 | +tmux send-keys -t session "hello world" |
| 14 | +tmux send-keys -t session Enter |
| 15 | +# Result: Only "d" appears in the input field! |
| 16 | +``` |
| 17 | + |
| 18 | +### The Solution: Bracketed Paste Mode |
| 19 | + |
| 20 | +Always wrap input in bracketed paste escape sequences (`\e[200~...\e[201~`): |
| 21 | + |
| 22 | +```bash |
| 23 | +# ✅ WORKS: Use bracketed paste escape sequences |
| 24 | +tmux send-keys -t session $'\e[200~hello world\e[201~' |
| 25 | +tmux send-keys -t session Enter |
| 26 | +# Result: "hello world" appears correctly! |
| 27 | +``` |
| 28 | + |
| 29 | +### Why This Happens |
| 30 | + |
| 31 | +- OpenTUI's keyboard input handling processes individual keystrokes |
| 32 | +- When characters arrive rapidly via `send-keys`, they can be dropped or misinterpreted |
| 33 | +- Bracketed paste mode (`\e[200~...\e[201~`) signals that the input is a paste operation |
| 34 | +- Paste events are processed atomically, preserving all characters |
| 35 | + |
| 36 | +### What Works Without Bracketed Paste |
| 37 | + |
| 38 | +- Control keys: `Enter`, `C-c`, `C-u`, `C-d`, `Escape` |
| 39 | +- Arrow keys: `Up`, `Down`, `Left`, `Right` |
| 40 | +- Function keys and special keys |
| 41 | +- Single characters with long delays (200ms+) - but this is slow and unreliable |
| 42 | + |
| 43 | +## Helper Functions |
| 44 | + |
| 45 | +### Bash Helper |
| 46 | + |
| 47 | +```bash |
| 48 | +# Send text to Codebuff CLI in tmux |
| 49 | +send_to_codebuff() { |
| 50 | + local session="$1" |
| 51 | + local text="$2" |
| 52 | + # Use bracketed paste mode for reliable input |
| 53 | + tmux send-keys -t "$session" $'\e[200~'"$text"$'\e[201~' |
| 54 | +} |
| 55 | + |
| 56 | +# Usage: |
| 57 | +send_to_codebuff my-session "fix the bug in main.ts" |
| 58 | +tmux send-keys -t my-session Enter |
| 59 | +``` |
| 60 | + |
| 61 | +### TypeScript Helper |
| 62 | + |
| 63 | +```typescript |
| 64 | +async function sendInput(sessionName: string, text: string): Promise<void> { |
| 65 | + // Use bracketed paste mode for reliable input |
| 66 | + await tmux(['send-keys', '-t', sessionName, '-l', `\x1b[200~${text}\x1b[201~`]) |
| 67 | +} |
| 68 | + |
| 69 | +// Usage: |
| 70 | +await sendInput(sessionName, 'fix the bug') |
| 71 | +await tmux(['send-keys', '-t', sessionName, 'Enter']) |
| 72 | +``` |
| 73 | + |
| 74 | +## Common tmux Patterns |
| 75 | + |
| 76 | +### Starting the CLI in tmux |
| 77 | + |
| 78 | +```bash |
| 79 | +# Create a detached session running the CLI |
| 80 | +tmux new-session -d -s codebuff-test -x 120 -y 30 'bun run src/index.tsx' |
| 81 | + |
| 82 | +# Wait for CLI to initialize |
| 83 | +sleep 3 |
| 84 | +``` |
| 85 | + |
| 86 | +### Sending Input and Capturing Output |
| 87 | + |
| 88 | +```bash |
| 89 | +# Send input using bracketed paste |
| 90 | +tmux send-keys -t codebuff-test $'\e[200~what files are in this project?\e[201~' |
| 91 | +tmux send-keys -t codebuff-test Enter |
| 92 | + |
| 93 | +# Wait for response |
| 94 | +sleep 5 |
| 95 | + |
| 96 | +# Capture output |
| 97 | +tmux capture-pane -t codebuff-test -p |
| 98 | +``` |
| 99 | + |
| 100 | +### Cleaning Up |
| 101 | + |
| 102 | +```bash |
| 103 | +# Kill the session when done |
| 104 | +tmux kill-session -t codebuff-test 2>/dev/null |
| 105 | +``` |
| 106 | + |
| 107 | +## Complete Example Script |
| 108 | + |
| 109 | +```bash |
| 110 | +#!/bin/bash |
| 111 | +SESSION="codebuff-test-$$" |
| 112 | + |
| 113 | +# Start CLI |
| 114 | +tmux new-session -d -s "$SESSION" -x 120 -y 30 'bun --cwd=cli run dev' |
| 115 | +sleep 4 |
| 116 | + |
| 117 | +# Send a prompt |
| 118 | +tmux send-keys -t "$SESSION" $'\e[200~list the files in src/\e[201~' |
| 119 | +tmux send-keys -t "$SESSION" Enter |
| 120 | +sleep 3 |
| 121 | + |
| 122 | +# Capture and display output |
| 123 | +echo "=== CLI Output ===" |
| 124 | +tmux capture-pane -t "$SESSION" -p |
| 125 | + |
| 126 | +# Cleanup |
| 127 | +tmux kill-session -t "$SESSION" 2>/dev/null |
| 128 | +``` |
| 129 | + |
| 130 | +## TypeScript Test Pattern |
| 131 | + |
| 132 | +```typescript |
| 133 | +import { spawn } from 'child_process' |
| 134 | + |
| 135 | +function tmux(args: string[]): Promise<string> { |
| 136 | + return new Promise((resolve, reject) => { |
| 137 | + const proc = spawn('tmux', args, { stdio: 'pipe' }) |
| 138 | + let stdout = '' |
| 139 | + proc.stdout?.on('data', (data) => { stdout += data.toString() }) |
| 140 | + proc.on('close', (code) => { |
| 141 | + code === 0 ? resolve(stdout) : reject(new Error('tmux failed')) |
| 142 | + }) |
| 143 | + }) |
| 144 | +} |
| 145 | + |
| 146 | +async function sendInput(session: string, text: string): Promise<void> { |
| 147 | + await tmux(['send-keys', '-t', session, '-l', `\x1b[200~${text}\x1b[201~`]) |
| 148 | +} |
| 149 | + |
| 150 | +async function testCLI() { |
| 151 | + const session = `test-${Date.now()}` |
| 152 | + |
| 153 | + try { |
| 154 | + // Start CLI |
| 155 | + await tmux(['new-session', '-d', '-s', session, '-x', '120', '-y', '30', |
| 156 | + 'bun', 'run', 'src/index.tsx']) |
| 157 | + await sleep(4000) |
| 158 | + |
| 159 | + // Send input |
| 160 | + await sendInput(session, 'hello world') |
| 161 | + await tmux(['send-keys', '-t', session, 'Enter']) |
| 162 | + await sleep(2000) |
| 163 | + |
| 164 | + // Capture output |
| 165 | + const output = await tmux(['capture-pane', '-t', session, '-p']) |
| 166 | + console.log(output) |
| 167 | + |
| 168 | + } finally { |
| 169 | + await tmux(['kill-session', '-t', session]).catch(() => {}) |
| 170 | + } |
| 171 | +} |
| 172 | +``` |
| 173 | + |
| 174 | +## Debugging Tips |
| 175 | + |
| 176 | +### Attach to a Session |
| 177 | + |
| 178 | +To debug interactively, attach to the tmux session: |
| 179 | + |
| 180 | +```bash |
| 181 | +tmux attach -t codebuff-test |
| 182 | +``` |
| 183 | + |
| 184 | +### Check Session Exists |
| 185 | + |
| 186 | +```bash |
| 187 | +tmux has-session -t codebuff-test 2>/dev/null && echo "exists" || echo "not found" |
| 188 | +``` |
| 189 | + |
| 190 | +### List All Sessions |
| 191 | + |
| 192 | +```bash |
| 193 | +tmux list-sessions |
| 194 | +``` |
| 195 | + |
| 196 | +### View Session Without Attaching |
| 197 | + |
| 198 | +```bash |
| 199 | +# Capture current pane content |
| 200 | +tmux capture-pane -t codebuff-test -p |
| 201 | + |
| 202 | +# Capture with ANSI colors preserved |
| 203 | +tmux capture-pane -t codebuff-test -p -e |
| 204 | +``` |
| 205 | + |
| 206 | +## Troubleshooting |
| 207 | + |
| 208 | +### Input Not Appearing |
| 209 | + |
| 210 | +1. **Forgot bracketed paste**: Always use `$'\e[200~text\e[201~'` |
| 211 | +2. **CLI not ready**: Increase sleep time after starting the session |
| 212 | +3. **Wrong session name**: Verify with `tmux list-sessions` |
| 213 | + |
| 214 | +### Characters Garbled |
| 215 | + |
| 216 | +1. **Not using `-l` flag in TypeScript**: Use `send-keys -l` for literal strings |
| 217 | +2. **Shell escaping issues**: Use `$'...'` syntax in bash for escape sequences |
| 218 | + |
| 219 | +### Session Cleanup Issues |
| 220 | + |
| 221 | +Always use `2>/dev/null` when killing sessions to suppress errors if already closed: |
| 222 | + |
| 223 | +```bash |
| 224 | +tmux kill-session -t "$SESSION" 2>/dev/null || true |
| 225 | +``` |
| 226 | + |
| 227 | +## Integration with Bun Tests |
| 228 | + |
| 229 | +The `.bin/bun` wrapper automatically detects tmux requirements for files matching: |
| 230 | +- `*integration*.test.ts` |
| 231 | +- `*e2e*.test.ts` |
| 232 | + |
| 233 | +Name your test files accordingly to get automatic tmux availability checking. |
0 commit comments