summaryrefslogtreecommitdiff
path: root/prompts/tmux-edit-integration-tests.md
blob: a0525b05fc8a6c08661836c532819878593a210e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
# hexai-tmux-edit Integration Test Runbook

Real-life integration tests against actual tmux panes running AI agent CLIs.
These tests verify prompt capture, agent detection, multi-line extraction,
text clearing, and the full edit-and-replace flow.

**Test Status**:
- ✅ **cursor-agent**: All tests passed (Feb 8, 2026)
- ✅ **amp**: All tests passed (Feb 8, 2026) - Note: amp uses TUI mode with Emacs/readline keybindings
- ⏳ **claude**: Needs testing
- ⏳ **aider**: Needs testing

**Important**: Agent detection and prompt extraction rely on regex patterns
matched against each agent's TUI output (box-drawing characters, prompt
symbols, status text). When agents update their TUI, these patterns may
break. If tests fail after an agent update, check the built-in patterns in
`internal/tmuxedit/agent.go` `builtinAgents()` and adjust the
`DetectPattern`, `PromptPattern`, and `StripPatterns` fields accordingly.
Users can also override patterns via `[[tmux_edit.agents]]` in config
without code changes.

## Prerequisites

- Must be running inside a tmux session
- `hexai-tmux-edit` binary must be installed (`go build -o ~/go/bin/hexai-tmux-edit ./cmd/hexai-tmux-edit/`)
- At least one tmux pane running `cursor-agent` with an empty prompt
- All unit tests must pass first: `go test ./internal/tmuxedit/`

## Finding a test pane

List all panes and pick one running cursor-agent with an idle prompt:

```sh
tmux list-panes -a -F '#{pane_id} #{window_name} #{pane_current_command}'
```

Look for a pane running `cursor-agent`. Use its `%NN` pane ID throughout.
Verify it has an empty input prompt:

```sh
tmux capture-pane -p -t '%NN' | tail -10
```

You should see the box-drawing prompt:
```
 ┌─────────────────────┐
 │ → Add a follow-up   │
 └─────────────────────┘
```

If the prompt has text in it, clear it first:

```sh
tmux send-keys -t '%NN' End && sleep 0.1 && tmux send-keys -t '%NN' -N 200 BSpace
```

## Test 1: Single-line prompt capture

**Goal**: Verify that text typed into cursor-agent's prompt is correctly
detected and extracted.

```sh
# 1. Send test text to the prompt
tmux send-keys -t '%NN' 'hello world test'

# 2. Wait for the TUI to render
sleep 1

# 3. Verify text appeared in prompt
tmux capture-pane -p -t '%NN' | grep '│.*→'
# Expected: │ → hello world test │

# 4. Write a Go test script to verify extraction logic
cat > /tmp/test_capture.go << 'GOEOF'
//go:build ignore

package main

import (
    "fmt"
    "os"
    "os/exec"
    "regexp"
    "strings"
)

func main() {
    pane := os.Args[1]
    out, err := exec.Command("tmux", "capture-pane", "-p", "-t", pane).Output()
    if err != nil {
        fmt.Printf("FAIL: capture error: %v\n", err)
        os.Exit(1)
    }
    content := string(out)

    // Verify cursor agent detected
    detectRe := regexp.MustCompile(`(│\s*→|/ commands · @ files)`)
    if !detectRe.MatchString(content) {
        fmt.Println("FAIL: cursor agent not detected")
        os.Exit(1)
    }
    fmt.Println("PASS: cursor agent detected")

    // Verify prompt extracted
    promptRe := regexp.MustCompile(`(?m)│\s*→?\s*(.+?)\s*│\s*$`)
    m := promptRe.FindStringSubmatch(content)
    if len(m) < 2 {
        fmt.Println("FAIL: no prompt match")
        os.Exit(1)
    }
    text := m[1]
    for _, s := range []string{"INSERT", "Add a follow-up", "ctrl+c to stop"} {
        text = strings.ReplaceAll(text, s, "")
    }
    text = strings.TrimSpace(text)
    fmt.Printf("PASS: extracted %q\n", text)
}
GOEOF

go run /tmp/test_capture.go '%NN'
# Expected: PASS: cursor agent detected
#           PASS: extracted "hello world test"

# 5. Clean up
tmux send-keys -t '%NN' End && sleep 0.1 && tmux send-keys -t '%NN' -N 200 BSpace
rm /tmp/test_capture.go
```

## Test 2: Multi-box disambiguation

**Goal**: Verify that when cursor-agent shows multiple box-drawing sections
(e.g. follow-ups box + input prompt), only the last box (the input prompt)
is captured.

This test requires a pane that has a follow-ups section above the input
prompt. You can create this state by sending text with Shift+Enter (which
cursor interprets as submit-and-queue-follow-up).

```sh
# 1. Verify the pane has multiple boxes visible
tmux capture-pane -p -t '%NN' | grep '│' | head -20
# Look for multiple │ lines from different boxes

# 2. Type text into the (bottom) input prompt
tmux send-keys -t '%NN' 'only this should be captured'
sleep 1

# 3. Run extraction and verify only the last box is picked
cat > /tmp/test_multibox.go << 'GOEOF'
//go:build ignore

package main

import (
    "fmt"
    "os"
    "os/exec"
    "regexp"
    "strings"
)

type promptMatch struct {
    lineNum int
    text    string
}

func main() {
    pane := os.Args[1]
    out, err := exec.Command("tmux", "capture-pane", "-p", "-t", pane).Output()
    if err != nil {
        fmt.Printf("FAIL: capture error: %v\n", err)
        os.Exit(1)
    }
    content := string(out)
    re := regexp.MustCompile(`(?m)│\s*→?\s*(.+?)\s*│\s*$`)
    strips := []string{"INSERT", "Add a follow-up", "ctrl+c to stop"}

    // Find all matches with line numbers
    paneLines := strings.Split(content, "\n")
    var matches []promptMatch
    for i, line := range paneLines {
        m := re.FindStringSubmatch(line)
        if len(m) >= 2 {
            matches = append(matches, promptMatch{i, m[1]})
        }
    }
    fmt.Printf("Total matches across all boxes: %d\n", len(matches))

    // Take last contiguous block
    if len(matches) == 0 {
        fmt.Println("FAIL: no matches found")
        os.Exit(1)
    }
    last := len(matches) - 1
    start := last
    for start > 0 && matches[start].lineNum-matches[start-1].lineNum == 1 {
        start--
    }

    fmt.Printf("Last contiguous block: lines %d-%d (%d matches)\n",
        matches[start].lineNum, matches[last].lineNum, last-start+1)

    var lines []string
    for i := start; i <= last; i++ {
        text := matches[i].text
        for _, s := range strips {
            text = strings.ReplaceAll(text, s, "")
        }
        text = strings.TrimSpace(text)
        if text != "" {
            lines = append(lines, text)
        }
    }
    result := strings.Join(lines, "\n")
    fmt.Printf("EXTRACTED: %q\n", result)

    if strings.Contains(result, "only this should be captured") {
        fmt.Println("PASS: last box correctly isolated")
    } else {
        fmt.Println("FAIL: wrong box content captured")
        os.Exit(1)
    }
}
GOEOF

go run /tmp/test_multibox.go '%NN'
# Expected: Total matches > 1 (from multiple boxes)
#           PASS: last box correctly isolated

# 3. Clean up
tmux send-keys -t '%NN' End && sleep 0.1 && tmux send-keys -t '%NN' -N 200 BSpace
rm /tmp/test_multibox.go
```

## Test 3: Clear and retype flow

**Goal**: Verify that `End + BSpace*200` clears the existing prompt text
and new text can be inserted afterwards.

```sh
# 1. Type original text
tmux send-keys -t '%NN' 'original text here'
sleep 0.5
tmux capture-pane -p -t '%NN' | grep '│.*→'
# Expected: │ → original text here │

# 2. Clear using the same method hexai-tmux-edit uses
tmux send-keys -t '%NN' End
sleep 0.1
tmux send-keys -t '%NN' -N 200 BSpace
sleep 0.5

# 3. Verify prompt is empty
tmux capture-pane -p -t '%NN' | grep '│.*→'
# Expected: │ → Add a follow-up │  (placeholder = empty)

# 4. Type replacement text
tmux send-keys -t '%NN' 'replacement text here'
sleep 0.5
tmux capture-pane -p -t '%NN' | grep '│.*→'
# Expected: │ → replacement text here │

# 5. Clean up
tmux send-keys -t '%NN' End && sleep 0.1 && tmux send-keys -t '%NN' -N 200 BSpace
```

## Test 4: Full end-to-end with mock editor

**Goal**: Run the actual `hexai-tmux-edit` binary against a real pane,
using a mock editor to automate the popup interaction. Verifies the
complete flow: capture -> detect agent -> extract prompt -> edit -> clear
-> send back.

**Note**: Mock editors should include a small delay before exiting to ensure
tmux popups close cleanly. Without the delay, the popup might not register
the editor exit properly and could hang.

```sh
# 1. Type text into the prompt
tmux send-keys -t '%NN' 'hello world'
sleep 0.5
tmux capture-pane -p -t '%NN' | grep '│.*→'
# Expected: │ → hello world │

# 2. Create a mock editor that replaces content (with delay for clean popup close)
cat > /tmp/mock-editor.sh << 'SH'
#!/bin/sh
# Write new content
echo "hello universe" > "$1"
# Small delay ensures tmux popup closes cleanly
sleep 0.1
SH
chmod +x /tmp/mock-editor.sh

# 3. Run hexai-tmux-edit with the mock editor
HEXAI_EDITOR=/tmp/mock-editor.sh hexai-tmux-edit --pane '%NN'
# Wait for popup to close
sleep 0.5

# 4. Verify the prompt was updated
tmux capture-pane -p -t '%NN' | grep '│.*→'
# Expected: │ → hello universe │

# 5. Check debug log for the full trace
cat /tmp/hexai-tmux-edit.log
# Expected log should show:
#   - agent detected as "cursor"
#   - extractPrompt result: "hello world"
#   - editor returned: "hello universe"
#   - deduplicateText result: "hello universe"
#   - sending to pane: "hello universe"
#   - === done ===

# 6. Clean up
rm /tmp/mock-editor.sh
tmux send-keys -t '%NN' End && sleep 0.1 && tmux send-keys -t '%NN' -N 200 BSpace
```

## Test 5: Agent detection does not false-positive on model names

**Goal**: Verify that a cursor-agent pane showing "Claude 4.5 Sonnet" as
the model name is detected as cursor (not claude).

```sh
# 1. Capture pane content and check for model name
tmux capture-pane -p -t '%NN' | grep -i 'claude\|sonnet'
# This may show "Claude 4.5 Sonnet (Thinking)" in the status line

# 2. Verify detection is still "cursor"
cat > /tmp/test_detect.go << 'GOEOF'
//go:build ignore

package main

import (
    "fmt"
    "os"
    "os/exec"
    "regexp"
)

func main() {
    pane := os.Args[1]
    out, _ := exec.Command("tmux", "capture-pane", "-p", "-t", pane).Output()
    content := string(out)

    agents := []struct {
        name   string
        detect string
    }{
        {"cursor", `(│\s*→|/ commands · @ files)`},
        {"claude", `(❯|claude code|anthropic)`},
    }
    for _, a := range agents {
        re := regexp.MustCompile(a.detect)
        if re.MatchString(content) {
            fmt.Printf("Detected: %s\n", a.name)
            if a.name == "cursor" {
                fmt.Println("PASS: correctly detected as cursor")
            } else {
                fmt.Println("FAIL: should be cursor, not " + a.name)
                os.Exit(1)
            }
            return
        }
    }
    fmt.Println("FAIL: no agent detected")
    os.Exit(1)
}
GOEOF

go run /tmp/test_detect.go '%NN'
# Expected: PASS: correctly detected as cursor

rm /tmp/test_detect.go
```

## Test 6: Unchanged text results in no-op

**Goal**: If the user opens the editor and saves without changes,
nothing should be sent to the pane.

```sh
# 1. Type text into the prompt
tmux send-keys -t '%NN' 'do not change me'
sleep 0.5

# 2. Create a mock editor that keeps the content unchanged (with delay)
cat > /tmp/mock-noop-editor.sh << 'SH'
#!/bin/sh
# Do nothing -- file already has the pre-filled content
# Small delay for clean popup close
sleep 0.1
SH
chmod +x /tmp/mock-noop-editor.sh

# 3. Run hexai-tmux-edit
HEXAI_EDITOR=/tmp/mock-noop-editor.sh hexai-tmux-edit --pane '%NN'
sleep 0.5

# 4. Check debug log -- should show "nothing to send"
grep 'nothing to send' /tmp/hexai-tmux-edit.log
# Expected: "nothing to send, exiting"

# 5. Verify prompt text is unchanged (still has original text)
tmux capture-pane -p -t '%NN' | grep '│.*→'
# Expected: │ → do not change me │

# 6. Clean up
rm /tmp/mock-noop-editor.sh
tmux send-keys -t '%NN' End && sleep 0.1 && tmux send-keys -t '%NN' -N 200 BSpace
```

## Cleanup

After all tests, remove any leftover temp files and check for hung popups:

```sh
# Remove test files
rm -f /tmp/test_capture.go /tmp/test_multibox.go /tmp/test_detect.go
rm -f /tmp/mock-editor.sh /tmp/mock-noop-editor.sh

# Check for any hung tmux popups (should show nothing)
tmux list-panes -a | grep popup || echo "✓ No hung popups"

# If popups are hung, kill them
# pkill -f 'tmux.*popup' || true
```

## Troubleshooting

**Popup doesn't close**: Mock editors that exit instantly (< 50ms) might not
give tmux enough time to register the exit. Add `sleep 0.1` before the editor
script exits.

**Popup hangs**: If a popup is stuck open:
```sh
# Find popup panes
tmux list-panes -a | grep popup

# Kill popup processes
pkill -f 'tmux.*popup'

# Or press Escape in the tmux session to close active popup
```