summaryrefslogtreecommitdiff
path: root/internal/tmuxedit
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-06-18 07:45:37 +0300
committerPaul Buetow <paul@buetow.org>2026-06-18 07:45:37 +0300
commit4ffb22e7f69f1c9c79b095d4e60bad3d97aac55b (patch)
tree2dc708cbb95975d34084eb5afd376871caa13e27 /internal/tmuxedit
parentece7dcfd232b780f5650326c8ac2379ca70387d4 (diff)
ik0 replace test seams with dependency injection
Diffstat (limited to 'internal/tmuxedit')
-rw-r--r--internal/tmuxedit/agent.go21
-rw-r--r--internal/tmuxedit/agent_test.go16
-rw-r--r--internal/tmuxedit/agentutil.go16
-rw-r--r--internal/tmuxedit/agentutil_test.go24
-rw-r--r--internal/tmuxedit/capture.go16
-rw-r--r--internal/tmuxedit/capture_test.go24
-rw-r--r--internal/tmuxedit/cursor_agent.go4
-rw-r--r--internal/tmuxedit/cursor_agent_test.go20
-rw-r--r--internal/tmuxedit/pane.go28
-rw-r--r--internal/tmuxedit/pane_test.go40
-rw-r--r--internal/tmuxedit/run.go45
-rw-r--r--internal/tmuxedit/run_test.go195
-rw-r--r--internal/tmuxedit/send.go39
-rw-r--r--internal/tmuxedit/send_test.go40
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