diff options
Diffstat (limited to 'internal/tmuxedit')
| -rw-r--r-- | internal/tmuxedit/agent.go | 21 | ||||
| -rw-r--r-- | internal/tmuxedit/agent_test.go | 16 | ||||
| -rw-r--r-- | internal/tmuxedit/agentutil.go | 16 | ||||
| -rw-r--r-- | internal/tmuxedit/agentutil_test.go | 24 | ||||
| -rw-r--r-- | internal/tmuxedit/capture.go | 16 | ||||
| -rw-r--r-- | internal/tmuxedit/capture_test.go | 24 | ||||
| -rw-r--r-- | internal/tmuxedit/cursor_agent.go | 4 | ||||
| -rw-r--r-- | internal/tmuxedit/cursor_agent_test.go | 20 | ||||
| -rw-r--r-- | internal/tmuxedit/pane.go | 28 | ||||
| -rw-r--r-- | internal/tmuxedit/pane_test.go | 40 | ||||
| -rw-r--r-- | internal/tmuxedit/run.go | 45 | ||||
| -rw-r--r-- | internal/tmuxedit/run_test.go | 195 | ||||
| -rw-r--r-- | internal/tmuxedit/send.go | 39 | ||||
| -rw-r--r-- | internal/tmuxedit/send_test.go | 40 |
14 files changed, 241 insertions, 287 deletions
diff --git a/internal/tmuxedit/agent.go b/internal/tmuxedit/agent.go index 1ae8f13..42213ce 100644 --- a/internal/tmuxedit/agent.go +++ b/internal/tmuxedit/agent.go @@ -40,6 +40,7 @@ type baseAgent struct { clearKeys string // tmux key sequence to clear input newlineKeys string // tmux key to insert a newline submitKeys string // tmux key to submit the prompt + deps tmuxEditDeps } // Base returns a pointer to the baseAgent for config merging. @@ -95,10 +96,10 @@ func (b *baseAgent) ClearInput(paneID string) error { if !b.clearFirst || b.clearKeys == "" { return nil } - if err := sendClearSequence(paneID, b.clearKeys); err != nil { + if err := b.deps.sendClearSequence(paneID, b.clearKeys); err != nil { return err } - sleepAfterClear() + b.deps.sleep() return nil } @@ -108,7 +109,21 @@ func (b *baseAgent) SendText(paneID, text string) error { if strings.TrimSpace(text) == "" { return nil } - return sendLines(paneID, text, b.newlineKeys) + return b.deps.sendLines(paneID, text, b.newlineKeys) +} + +func withAgentDeps(agents []Agent, deps tmuxEditDeps) []Agent { + for _, agent := range agents { + withAgentDep(agent, deps) + } + return agents +} + +func withAgentDep(agent Agent, deps tmuxEditDeps) Agent { + if c, ok := agent.(Configurable); ok { + c.Base().deps = deps + } + return agent } // detectAgent tries each agent's Detect method against pane content. diff --git a/internal/tmuxedit/agent_test.go b/internal/tmuxedit/agent_test.go index 1debfc4..ff782d4 100644 --- a/internal/tmuxedit/agent_test.go +++ b/internal/tmuxedit/agent_test.go @@ -94,16 +94,14 @@ func TestBaseAgent_ClearInput_EmptyKeys(t *testing.T) { } func TestBaseAgent_ClearInput_Enabled(t *testing.T) { - noSleep(t) var calls []string - oldSend := sendKeys - defer func() { sendKeys = oldSend }() - sendKeys = func(paneID string, keys ...string) error { + deps := noSleepDeps() + deps.sendKeys = func(paneID string, keys ...string) error { calls = append(calls, fmt.Sprintf("send:%s:%s", paneID, strings.Join(keys, ","))) return nil } - b := &baseAgent{clearFirst: true, clearKeys: "C-u"} + b := &baseAgent{clearFirst: true, clearKeys: "C-u", deps: deps} err := b.ClearInput("%2") if err != nil { t.Fatalf("unexpected error: %v", err) @@ -114,14 +112,12 @@ func TestBaseAgent_ClearInput_Enabled(t *testing.T) { } func TestBaseAgent_ClearInput_Error(t *testing.T) { - noSleep(t) - oldSend := sendKeys - defer func() { sendKeys = oldSend }() - sendKeys = func(string, ...string) error { + deps := noSleepDeps() + deps.sendKeys = func(string, ...string) error { return fmt.Errorf("send failed") } - b := &baseAgent{clearFirst: true, clearKeys: "C-u"} + b := &baseAgent{clearFirst: true, clearKeys: "C-u", deps: deps} err := b.ClearInput("%1") if err == nil { t.Fatal("expected error from sendClearSequence failure") diff --git a/internal/tmuxedit/agentutil.go b/internal/tmuxedit/agentutil.go index 67351d3..0f4f38e 100644 --- a/internal/tmuxedit/agentutil.go +++ b/internal/tmuxedit/agentutil.go @@ -107,14 +107,18 @@ func stripNoise(text string, patterns []string) string { // token individually. Tokens with a "*N" suffix (e.g. "BSpace*200") are // sent N times using tmux send-keys -N for efficient bulk repeats. func sendClearSequence(paneID, clearKeys string) error { + return tmuxEditDeps{}.sendClearSequence(paneID, clearKeys) +} + +func (d tmuxEditDeps) sendClearSequence(paneID, clearKeys string) error { for _, token := range strings.Fields(clearKeys) { key, count := parseKeyRepeat(token) if count > 1 { - if err := sendRepeatedKey(paneID, key, count); err != nil { + if err := d.sendRepeated(paneID, key, count); err != nil { return fmt.Errorf("clear key %q*%d failed: %w", key, count, err) } } else { - if err := sendKeys(paneID, key); err != nil { + if err := d.send(paneID, key); err != nil { return fmt.Errorf("clear key %q failed: %w", key, err) } } @@ -145,9 +149,13 @@ func parseKeyRepeat(token string) (string, int) { // fallback. This is the shared text-sending logic used by agent SendText // implementations. func sendLines(paneID, text, newlineKeys string) error { + return tmuxEditDeps{}.sendLines(paneID, text, newlineKeys) +} + +func (d tmuxEditDeps) sendLines(paneID, text, newlineKeys string) error { lines := strings.Split(text, "\n") for i, line := range lines { - if err := sendKeys(paneID, line); err != nil { + if err := d.send(paneID, line); err != nil { return fmt.Errorf("send line %d failed: %w", i, err) } // Insert inter-line newline (except after the last line) @@ -156,7 +164,7 @@ func sendLines(paneID, text, newlineKeys string) error { if nlKey == "" { nlKey = "Enter" } - if err := sendKeys(paneID, nlKey); err != nil { + if err := d.send(paneID, nlKey); err != nil { return fmt.Errorf("newline after line %d failed: %w", i, err) } } diff --git a/internal/tmuxedit/agentutil_test.go b/internal/tmuxedit/agentutil_test.go index 69111b5..eed245c 100644 --- a/internal/tmuxedit/agentutil_test.go +++ b/internal/tmuxedit/agentutil_test.go @@ -199,16 +199,14 @@ func TestParseKeyRepeat(t *testing.T) { func TestSendClearSequence_EscapeKey(t *testing.T) { var calls []string - oldSend := sendKeys - defer func() { sendKeys = oldSend }() - sendKeys = func(paneID string, keys ...string) error { + deps := tmuxEditDeps{sendKeys: func(paneID string, keys ...string) error { calls = append(calls, strings.Join(keys, ",")) return nil - } + }} // sendClearSequence with "Escape" should succeed and send the key. // The 150ms Escape delay is real but acceptable in tests. - err := sendClearSequence("%1", "Escape C-k") + err := deps.sendClearSequence("%1", "Escape C-k") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -224,13 +222,11 @@ func TestSendClearSequence_EscapeKey(t *testing.T) { } func TestSendClearSequence_SingleKeyError(t *testing.T) { - oldSend := sendKeys - defer func() { sendKeys = oldSend }() - sendKeys = func(string, ...string) error { + deps := tmuxEditDeps{sendKeys: func(string, ...string) error { return fmt.Errorf("send failed") - } + }} - err := sendClearSequence("%1", "C-u") + err := deps.sendClearSequence("%1", "C-u") if err == nil { t.Fatal("expected error from sendKeys failure") } @@ -240,13 +236,11 @@ func TestSendClearSequence_SingleKeyError(t *testing.T) { } func TestSendClearSequence_RepeatedKeyError(t *testing.T) { - oldRepeat := sendRepeatedKey - defer func() { sendRepeatedKey = oldRepeat }() - sendRepeatedKey = func(string, string, int) error { + deps := tmuxEditDeps{sendRepeatedKey: func(string, string, int) error { return fmt.Errorf("repeat failed") - } + }} - err := sendClearSequence("%1", "BSpace*200") + err := deps.sendClearSequence("%1", "BSpace*200") if err == nil { t.Fatal("expected error from sendRepeatedKey failure") } diff --git a/internal/tmuxedit/capture.go b/internal/tmuxedit/capture.go index 2af5698..f4e3a67 100644 --- a/internal/tmuxedit/capture.go +++ b/internal/tmuxedit/capture.go @@ -5,11 +5,17 @@ import ( "strings" ) -// capturePane retrieves the visible content of a tmux pane via -// `tmux capture-pane -p -t <paneID>`. The -p flag prints to stdout -// instead of to a paste buffer. -var capturePane = func(paneID string) (string, error) { - out, err := runCommand("tmux", "capture-pane", "-p", "-t", paneID) +func capturePane(paneID string) (string, error) { + return tmuxEditDeps{}.capture(paneID) +} + +// capture retrieves the visible content of a tmux pane via `tmux capture-pane +// -p -t <paneID>`. The -p flag prints to stdout instead of to a paste buffer. +func (d tmuxEditDeps) capture(paneID string) (string, error) { + if d.capturePane != nil { + return d.capturePane(paneID) + } + out, err := d.command("tmux", "capture-pane", "-p", "-t", paneID) if err != nil { return "", fmt.Errorf("capture-pane failed for %s: %w", paneID, err) } diff --git a/internal/tmuxedit/capture_test.go b/internal/tmuxedit/capture_test.go index 40d0e98..c5a6605 100644 --- a/internal/tmuxedit/capture_test.go +++ b/internal/tmuxedit/capture_test.go @@ -6,15 +6,13 @@ import ( ) func TestCapturePane_Success(t *testing.T) { - old := runCommand - defer func() { runCommand = old }() - runCommand = func(name string, args ...string) ([]byte, error) { + deps := tmuxEditDeps{runCommand: func(name string, args ...string) ([]byte, error) { if name == "tmux" && len(args) >= 3 && args[0] == "capture-pane" { return []byte("Claude Code v1.0\n> hello world\n"), nil } return nil, fmt.Errorf("unexpected: %s %v", name, args) - } - got, err := capturePane("%5") + }} + got, err := deps.capture("%5") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -24,24 +22,20 @@ func TestCapturePane_Success(t *testing.T) { } func TestCapturePane_Error(t *testing.T) { - old := runCommand - defer func() { runCommand = old }() - runCommand = func(string, ...string) ([]byte, error) { + deps := tmuxEditDeps{runCommand: func(string, ...string) ([]byte, error) { return nil, fmt.Errorf("pane not found") - } - _, err := capturePane("%999") + }} + _, err := deps.capture("%999") if err == nil { t.Fatal("expected error for failed capture") } } func TestCapturePane_EmptyContent(t *testing.T) { - old := runCommand - defer func() { runCommand = old }() - runCommand = func(string, ...string) ([]byte, error) { + deps := tmuxEditDeps{runCommand: func(string, ...string) ([]byte, error) { return []byte("\n\n"), nil - } - got, err := capturePane("%1") + }} + got, err := deps.capture("%1") if err != nil { t.Fatalf("unexpected error: %v", err) } diff --git a/internal/tmuxedit/cursor_agent.go b/internal/tmuxedit/cursor_agent.go index 1346d05..ebea38e 100644 --- a/internal/tmuxedit/cursor_agent.go +++ b/internal/tmuxedit/cursor_agent.go @@ -50,9 +50,9 @@ func (c *cursorAgent) ClearInput(paneID string) error { if !c.clearFirst || c.clearKeys == "" { return nil } - if err := sendClearSequence(paneID, c.clearKeys); err != nil { + if err := c.deps.sendClearSequence(paneID, c.clearKeys); err != nil { return err } - sleepAfterClear() + c.deps.sleep() return nil } diff --git a/internal/tmuxedit/cursor_agent_test.go b/internal/tmuxedit/cursor_agent_test.go index 867a55b..d81416b 100644 --- a/internal/tmuxedit/cursor_agent_test.go +++ b/internal/tmuxedit/cursor_agent_test.go @@ -82,24 +82,19 @@ func TestCursorAgent_ExtractPrompt(t *testing.T) { } func TestCursorAgent_ClearInput(t *testing.T) { - noSleep(t) var calls []string - oldSend := sendKeys - oldRepeat := sendRepeatedKey - defer func() { - sendKeys = oldSend - sendRepeatedKey = oldRepeat - }() - sendKeys = func(paneID string, keys ...string) error { + deps := noSleepDeps() + deps.sendKeys = func(paneID string, keys ...string) error { calls = append(calls, fmt.Sprintf("send:%s:%s", paneID, strings.Join(keys, ","))) return nil } - sendRepeatedKey = func(paneID, key string, count int) error { + deps.sendRepeatedKey = func(paneID, key string, count int) error { calls = append(calls, fmt.Sprintf("repeat:%s:%s*%d", paneID, key, count)) return nil } agent := newCursorAgent() + agent.deps = deps err := agent.ClearInput("%5") if err != nil { t.Fatalf("unexpected error: %v", err) @@ -154,14 +149,13 @@ func TestCursorAgent_ClearInput_EmptyKeys(t *testing.T) { } func TestCursorAgent_ClearInput_Error(t *testing.T) { - noSleep(t) - oldSend := sendKeys - defer func() { sendKeys = oldSend }() - sendKeys = func(string, ...string) error { + deps := noSleepDeps() + deps.sendKeys = func(string, ...string) error { return fmt.Errorf("send failed") } agent := newCursorAgent() + agent.deps = deps err := agent.ClearInput("%1") if err == nil { t.Fatal("expected error from sendClearSequence failure") diff --git a/internal/tmuxedit/pane.go b/internal/tmuxedit/pane.go index aae2d69..0b6be93 100644 --- a/internal/tmuxedit/pane.go +++ b/internal/tmuxedit/pane.go @@ -7,8 +7,20 @@ import ( "strings" ) -// runCommand is the seam for exec.Command().Output(). Override in tests. -var runCommand = func(name string, args ...string) ([]byte, error) { +type tmuxEditDeps struct { + runCommand func(string, ...string) ([]byte, error) + capturePane func(string) (string, error) + openEditorPopup func(string, string, string) (string, error) + sendKeys func(string, ...string) error + sendRepeatedKey func(string, string, int) error + sleepAfterClear func() + launchPopup func(string, string, string, string) error +} + +func (d tmuxEditDeps) command(name string, args ...string) ([]byte, error) { + if d.runCommand != nil { + return d.runCommand(name, args...) + } return exec.Command(name, args...).Output() } @@ -16,6 +28,10 @@ var runCommand = func(name string, args ...string) ([]byte, error) { // chain: explicit flag > HEXAI_TMUX_PANE env var > tmux query for active pane. // Returns the pane ID (e.g. "%5") or an error. func resolveTargetPane(flagPane string) (string, error) { + return tmuxEditDeps{}.resolveTargetPane(flagPane) +} + +func (d tmuxEditDeps) resolveTargetPane(flagPane string) (string, error) { // 1. Explicit --pane flag if p := strings.TrimSpace(flagPane); p != "" { return p, nil @@ -25,12 +41,16 @@ func resolveTargetPane(flagPane string) (string, error) { return p, nil } // 3. Query tmux for the active pane in the current window - return queryActivePane() + return d.queryActivePane() } // queryActivePane asks tmux for the active pane ID using display-message. func queryActivePane() (string, error) { - out, err := runCommand("tmux", "display-message", "-p", "#{pane_id}") + return tmuxEditDeps{}.queryActivePane() +} + +func (d tmuxEditDeps) queryActivePane() (string, error) { + out, err := d.command("tmux", "display-message", "-p", "#{pane_id}") if err != nil { return "", fmt.Errorf("cannot determine tmux pane: %w", err) } diff --git a/internal/tmuxedit/pane_test.go b/internal/tmuxedit/pane_test.go index 5b6f1b6..d15ef55 100644 --- a/internal/tmuxedit/pane_test.go +++ b/internal/tmuxedit/pane_test.go @@ -6,13 +6,11 @@ import ( ) func TestResolveTargetPane_FlagWins(t *testing.T) { - old := runCommand - defer func() { runCommand = old }() - runCommand = func(string, ...string) ([]byte, error) { + deps := tmuxEditDeps{runCommand: func(string, ...string) ([]byte, error) { return []byte("%99"), nil - } + }} t.Setenv("HEXAI_TMUX_PANE", "%10") - got, err := resolveTargetPane("%5") + got, err := deps.resolveTargetPane("%5") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -22,13 +20,11 @@ func TestResolveTargetPane_FlagWins(t *testing.T) { } func TestResolveTargetPane_EnvFallback(t *testing.T) { - old := runCommand - defer func() { runCommand = old }() - runCommand = func(string, ...string) ([]byte, error) { + deps := tmuxEditDeps{runCommand: func(string, ...string) ([]byte, error) { return []byte("%99"), nil - } + }} t.Setenv("HEXAI_TMUX_PANE", "%10") - got, err := resolveTargetPane("") + got, err := deps.resolveTargetPane("") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -38,16 +34,14 @@ func TestResolveTargetPane_EnvFallback(t *testing.T) { } func TestResolveTargetPane_TmuxQuery(t *testing.T) { - old := runCommand - defer func() { runCommand = old }() - runCommand = func(name string, args ...string) ([]byte, error) { + deps := tmuxEditDeps{runCommand: func(name string, args ...string) ([]byte, error) { if name == "tmux" && len(args) > 0 && args[0] == "display-message" { return []byte("%42\n"), nil } return nil, fmt.Errorf("unexpected command: %s", name) - } + }} t.Setenv("HEXAI_TMUX_PANE", "") - got, err := resolveTargetPane("") + got, err := deps.resolveTargetPane("") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -57,26 +51,22 @@ func TestResolveTargetPane_TmuxQuery(t *testing.T) { } func TestResolveTargetPane_TmuxError(t *testing.T) { - old := runCommand - defer func() { runCommand = old }() - runCommand = func(string, ...string) ([]byte, error) { + deps := tmuxEditDeps{runCommand: func(string, ...string) ([]byte, error) { return nil, fmt.Errorf("tmux not available") - } + }} t.Setenv("HEXAI_TMUX_PANE", "") - _, err := resolveTargetPane("") + _, err := deps.resolveTargetPane("") if err == nil { t.Fatal("expected error when tmux fails") } } func TestResolveTargetPane_TmuxEmptyOutput(t *testing.T) { - old := runCommand - defer func() { runCommand = old }() - runCommand = func(string, ...string) ([]byte, error) { + deps := tmuxEditDeps{runCommand: func(string, ...string) ([]byte, error) { return []byte(" \n"), nil - } + }} t.Setenv("HEXAI_TMUX_PANE", "") - _, err := resolveTargetPane("") + _, err := deps.resolveTargetPane("") if err == nil { t.Fatal("expected error for empty tmux output") } diff --git a/internal/tmuxedit/run.go b/internal/tmuxedit/run.go index 2dd5afe..ff22b5b 100644 --- a/internal/tmuxedit/run.go +++ b/internal/tmuxedit/run.go @@ -21,10 +21,16 @@ type Options struct { Pane string // --pane flag (target pane ID) } -// openEditorPopup is the seam for opening an editor in a tmux popup. -// It creates a temp file, opens it in a tmux popup with the user's editor, -// waits for completion, and returns the edited content. Override in tests. -var openEditorPopup = func(initial, popupW, popupH string) (string, error) { +func openEditorPopup(initial, popupW, popupH string) (string, error) { + return tmuxEditDeps{}.openEditor(initial, popupW, popupH) +} + +// openEditor creates a temp file, opens it in a tmux popup with the user's +// editor, waits for completion, and returns the edited content. +func (d tmuxEditDeps) openEditor(initial, popupW, popupH string) (string, error) { + if d.openEditorPopup != nil { + return d.openEditorPopup(initial, popupW, popupH) + } ed, err := editor.Resolve() if err != nil { return "", err @@ -48,7 +54,7 @@ var openEditorPopup = func(initial, popupW, popupH string) (string, error) { } // Build the tmux display-popup command to launch the editor - if err := launchPopup(ed, path, popupW, popupH); err != nil { + if err := d.launch(ed, path, popupW, popupH); err != nil { return "", fmt.Errorf("popup editor: %w", err) } @@ -59,11 +65,17 @@ var openEditorPopup = func(initial, popupW, popupH string) (string, error) { return strings.TrimSpace(string(b)), nil } -// launchPopup is the seam for running `tmux display-popup` with the editor. -// The -E flag makes the popup close when the editor exits. The -d flag sets -// the working directory for the popup. Uses .Run() (not .Output()) so the -// popup blocks until the user closes the editor. -var launchPopup = func(ed, path, width, height string) error { +func launchPopup(ed, path, width, height string) error { + return tmuxEditDeps{}.launch(ed, path, width, height) +} + +// launch runs `tmux display-popup` with the editor. The -E flag makes the +// popup close when the editor exits. The -d flag sets the working directory +// for the popup. Uses .Run() so the popup blocks until the user closes it. +func (d tmuxEditDeps) launch(ed, path, width, height string) error { + if d.launchPopup != nil { + return d.launchPopup(ed, path, width, height) + } args := []string{"display-popup", "-E"} // Get current working directory to pass to the popup @@ -144,6 +156,10 @@ func dbg(format string, args ...any) { // It resolves the agent (by name or auto-detect), extracts the current // prompt, opens the editor popup, then clears and sends the result. func runWithConfig(opts Options, cfg appconfig.App) error { + return tmuxEditDeps{}.runWithConfig(opts, cfg) +} + +func (d tmuxEditDeps) runWithConfig(opts Options, cfg appconfig.App) error { closeLog, err := initDebugLog() if err != nil { return fmt.Errorf("init debug log: %w", err) @@ -152,14 +168,14 @@ func runWithConfig(opts Options, cfg appconfig.App) error { dbg("=== hexai-tmux-edit start ===") dbg("opts: pane=%q agent=%q config=%q", opts.Pane, opts.Agent, opts.ConfigPath) - paneID, err := resolveTargetPane(opts.Pane) + paneID, err := d.resolveTargetPane(opts.Pane) if err != nil { dbg("resolveTargetPane error: %v", err) return err } dbg("resolved pane: %q", paneID) - content, err := capturePane(paneID) + content, err := d.capture(paneID) if err != nil { dbg("capturePane error: %v", err) return err @@ -167,8 +183,9 @@ func runWithConfig(opts Options, cfg appconfig.App) error { dbg("captured %d bytes from pane", len(content)) logPaneLines(content) - agents := resolveAgents(cfg.TmuxEditAgents) + agents := withAgentDeps(resolveAgents(cfg.TmuxEditAgents), d) agent := pickAgent(opts.Agent, content, agents) + agent = withAgentDep(agent, d) dbg("agent: name=%q", agent.Name()) original := agent.ExtractPrompt(content) @@ -177,7 +194,7 @@ func runWithConfig(opts Options, cfg appconfig.App) error { popupW, popupH := popupDimensions(cfg) dbg("opening editor popup: w=%s h=%s initial=%q", popupW, popupH, original) - edited, err := openEditorPopup(original, popupW, popupH) + edited, err := d.openEditor(original, popupW, popupH) if err != nil { dbg("openEditorPopup error: %v", err) return err diff --git a/internal/tmuxedit/run_test.go b/internal/tmuxedit/run_test.go index f528b95..59507f9 100644 --- a/internal/tmuxedit/run_test.go +++ b/internal/tmuxedit/run_test.go @@ -10,21 +10,10 @@ import ( ) func TestRunWithConfig_HappyPath(t *testing.T) { - noSleep(t) - // Save and restore all seams - oldCapture := capturePane - oldSendKeys := sendKeys - oldEditorPopup := openEditorPopup - oldRunCmd := runCommand - defer func() { - capturePane = oldCapture - sendKeys = oldSendKeys - openEditorPopup = oldEditorPopup - runCommand = oldRunCmd - }() + deps := noSleepDeps() // Mock: pane resolution via tmux query - runCommand = func(name string, args ...string) ([]byte, error) { + deps.runCommand = func(name string, args ...string) ([]byte, error) { if name == "tmux" && args[0] == "display-message" { return []byte("%5"), nil } @@ -32,12 +21,12 @@ func TestRunWithConfig_HappyPath(t *testing.T) { } // Mock: capture pane content with Aider agent detected; aider uses "> prompt" pattern - capturePane = func(paneID string) (string, error) { + deps.capturePane = func(paneID string) (string, error) { return "aider v0.50\n> fix the bug", nil } // Mock: editor popup returns modified text - openEditorPopup = func(initial, w, h string) (string, error) { + deps.openEditorPopup = func(initial, w, h string) (string, error) { if initial != "fix the bug" { t.Errorf("initial = %q, want 'fix the bug'", initial) } @@ -49,13 +38,13 @@ func TestRunWithConfig_HappyPath(t *testing.T) { // Track send-keys calls var sent []string - sendKeys = func(paneID string, keys ...string) error { + deps.sendKeys = func(paneID string, keys ...string) error { sent = append(sent, strings.Join(keys, ",")) return nil } cfg := appconfig.App{} - err := runWithConfig(Options{}, cfg) + err := deps.runWithConfig(Options{}, cfg) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -75,96 +64,65 @@ func TestRunWithConfig_HappyPath(t *testing.T) { } func TestRunWithConfig_ExplicitAgent(t *testing.T) { - noSleep(t) - oldCapture := capturePane - oldSendKeys := sendKeys - oldEditorPopup := openEditorPopup - oldRunCmd := runCommand - defer func() { - capturePane = oldCapture - sendKeys = oldSendKeys - openEditorPopup = oldEditorPopup - runCommand = oldRunCmd - }() - - runCommand = func(name string, args ...string) ([]byte, error) { + deps := noSleepDeps() + deps.runCommand = func(name string, args ...string) ([]byte, error) { return []byte("%1"), nil } - capturePane = func(string) (string, error) { + deps.capturePane = func(string) (string, error) { return "some generic content\n> hello", nil } - openEditorPopup = func(initial, w, h string) (string, error) { + deps.openEditorPopup = func(initial, w, h string) (string, error) { // With cursor agent, prompt extraction uses │ pattern, so initial should be empty if initial != "" { t.Errorf("initial = %q, want empty (cursor agent doesn't match > pattern)", initial) } return "new prompt", nil } - sendKeys = func(string, ...string) error { return nil } + deps.sendKeys = func(string, ...string) error { return nil } cfg := appconfig.App{} - err := runWithConfig(Options{Agent: "cursor"}, cfg) + err := deps.runWithConfig(Options{Agent: "cursor"}, cfg) if err != nil { t.Fatalf("unexpected error: %v", err) } } func TestRunWithConfig_EditorEmpty(t *testing.T) { - oldCapture := capturePane - oldSendKeys := sendKeys - oldEditorPopup := openEditorPopup - oldRunCmd := runCommand - defer func() { - capturePane = oldCapture - sendKeys = oldSendKeys - openEditorPopup = oldEditorPopup - runCommand = oldRunCmd - }() - - runCommand = func(name string, args ...string) ([]byte, error) { + deps := noSleepDeps() + deps.runCommand = func(name string, args ...string) ([]byte, error) { return []byte("%1"), nil } - capturePane = func(string) (string, error) { + deps.capturePane = func(string) (string, error) { return "aider v0.50\n> ", nil } - openEditorPopup = func(string, string, string) (string, error) { + deps.openEditorPopup = func(string, string, string) (string, error) { return "", nil // user saved empty file } - sendKeys = func(string, ...string) error { + deps.sendKeys = func(string, ...string) error { t.Fatal("sendKeys should not be called when editor returns empty") return nil } cfg := appconfig.App{} - err := runWithConfig(Options{}, cfg) + err := deps.runWithConfig(Options{}, cfg) if err != nil { t.Fatalf("unexpected error: %v", err) } } func TestRunWithConfig_CustomDimensions(t *testing.T) { - oldCapture := capturePane - oldSendKeys := sendKeys - oldEditorPopup := openEditorPopup - oldRunCmd := runCommand - defer func() { - capturePane = oldCapture - sendKeys = oldSendKeys - openEditorPopup = oldEditorPopup - runCommand = oldRunCmd - }() - - runCommand = func(name string, args ...string) ([]byte, error) { + deps := noSleepDeps() + deps.runCommand = func(name string, args ...string) ([]byte, error) { return []byte("%1"), nil } - capturePane = func(string) (string, error) { return "", nil } - openEditorPopup = func(initial, w, h string) (string, error) { + deps.capturePane = func(string) (string, error) { return "", nil } + deps.openEditorPopup = func(initial, w, h string) (string, error) { if w != "90%" || h != "85%" { t.Errorf("dimensions = %sx%s, want 90%%x85%%", w, h) } return "test", nil } - sendKeys = func(string, ...string) error { return nil } + deps.sendKeys = func(string, ...string) error { return nil } cfg := appconfig.App{ FeatureConfig: appconfig.FeatureConfig{TmuxEditConfig: appconfig.TmuxEditConfig{ @@ -172,7 +130,7 @@ func TestRunWithConfig_CustomDimensions(t *testing.T) { TmuxEditPopupHeight: "85%", }}, } - err := runWithConfig(Options{}, cfg) + err := deps.runWithConfig(Options{}, cfg) if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -212,21 +170,18 @@ func TestShellQuote(t *testing.T) { } func TestLaunchPopup_CommandArgs(t *testing.T) { - oldLaunch := launchPopup - defer func() { launchPopup = oldLaunch }() - var capturedArgs struct { ed, path, w, h string } - launchPopup = func(ed, path, w, h string) error { + deps := tmuxEditDeps{launchPopup: func(ed, path, w, h string) error { capturedArgs.ed = ed capturedArgs.path = path capturedArgs.w = w capturedArgs.h = h return nil - } + }} - err := launchPopup("vim", "/tmp/test.md", "90%", "85%") + err := deps.launch("vim", "/tmp/test.md", "90%", "85%") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -239,19 +194,16 @@ func TestLaunchPopup_CommandArgs(t *testing.T) { } func TestLaunchPopup_NoDimensions(t *testing.T) { - oldLaunch := launchPopup - defer func() { launchPopup = oldLaunch }() - var capturedArgs struct { w, h string } - launchPopup = func(ed, path, w, h string) error { + deps := tmuxEditDeps{launchPopup: func(ed, path, w, h string) error { capturedArgs.w = w capturedArgs.h = h return nil - } + }} - err := launchPopup("nano", "/tmp/f.md", "", "") + err := deps.launch("nano", "/tmp/f.md", "", "") if err != nil { t.Fatalf("unexpected error: %v", err) } @@ -261,49 +213,35 @@ func TestLaunchPopup_NoDimensions(t *testing.T) { } func TestRunWithConfig_CaptureError(t *testing.T) { - oldCapture := capturePane - oldRunCmd := runCommand - defer func() { - capturePane = oldCapture - runCommand = oldRunCmd - }() - - runCommand = func(name string, args ...string) ([]byte, error) { + deps := tmuxEditDeps{} + deps.runCommand = func(name string, args ...string) ([]byte, error) { return []byte("%1"), nil } - capturePane = func(string) (string, error) { + deps.capturePane = func(string) (string, error) { return "", fmt.Errorf("capture failed") } cfg := appconfig.App{} - err := runWithConfig(Options{Pane: "%1"}, cfg) + err := deps.runWithConfig(Options{Pane: "%1"}, cfg) if err == nil || !strings.Contains(err.Error(), "capture failed") { t.Errorf("expected capture error, got: %v", err) } } func TestRunWithConfig_EditorError(t *testing.T) { - oldCapture := capturePane - oldEditorPopup := openEditorPopup - oldRunCmd := runCommand - defer func() { - capturePane = oldCapture - openEditorPopup = oldEditorPopup - runCommand = oldRunCmd - }() - - runCommand = func(name string, args ...string) ([]byte, error) { + deps := tmuxEditDeps{} + deps.runCommand = func(name string, args ...string) ([]byte, error) { return []byte("%1"), nil } - capturePane = func(string) (string, error) { + deps.capturePane = func(string) (string, error) { return "some content", nil } - openEditorPopup = func(string, string, string) (string, error) { + deps.openEditorPopup = func(string, string, string) (string, error) { return "", fmt.Errorf("editor crashed") } cfg := appconfig.App{} - err := runWithConfig(Options{Pane: "%1"}, cfg) + err := deps.runWithConfig(Options{Pane: "%1"}, cfg) if err == nil || !strings.Contains(err.Error(), "editor crashed") { t.Errorf("expected editor error, got: %v", err) } @@ -340,87 +278,62 @@ func TestLogPaneLines_WithoutDebugLog(t *testing.T) { } func TestRunWithConfig_ClearInputError(t *testing.T) { - noSleep(t) - oldCapture := capturePane - oldSendKeys := sendKeys - oldEditorPopup := openEditorPopup - oldRunCmd := runCommand - defer func() { - capturePane = oldCapture - sendKeys = oldSendKeys - openEditorPopup = oldEditorPopup - runCommand = oldRunCmd - }() - - runCommand = func(name string, args ...string) ([]byte, error) { + deps := noSleepDeps() + deps.runCommand = func(name string, args ...string) ([]byte, error) { return []byte("%1"), nil } // Use Aider (clearFirst=true, clearKeys="C-u") so ClearInput is exercised - capturePane = func(string) (string, error) { + deps.capturePane = func(string) (string, error) { return "aider v0.50\n> fix the bug", nil } - openEditorPopup = func(string, string, string) (string, error) { + deps.openEditorPopup = func(string, string, string) (string, error) { return "new text", nil } - sendKeys = func(string, ...string) error { + deps.sendKeys = func(string, ...string) error { return fmt.Errorf("clear input failed") } cfg := appconfig.App{} - err := runWithConfig(Options{}, cfg) + err := deps.runWithConfig(Options{}, cfg) if err == nil || !strings.Contains(err.Error(), "clear input failed") { t.Errorf("expected clear input error, got: %v", err) } } func TestRunWithConfig_SendTextError(t *testing.T) { - noSleep(t) - oldCapture := capturePane - oldSendKeys := sendKeys - oldEditorPopup := |
