summaryrefslogtreecommitdiff
path: root/internal/tmuxedit
diff options
context:
space:
mode:
Diffstat (limited to 'internal/tmuxedit')
-rw-r--r--internal/tmuxedit/agent_test.go46
-rw-r--r--internal/tmuxedit/agentutil_test.go60
-rw-r--r--internal/tmuxedit/claude_agent_test.go63
-rw-r--r--internal/tmuxedit/cursor_agent_test.go49
-rw-r--r--internal/tmuxedit/history_test.go128
-rw-r--r--internal/tmuxedit/run_test.go101
6 files changed, 447 insertions, 0 deletions
diff --git a/internal/tmuxedit/agent_test.go b/internal/tmuxedit/agent_test.go
index 3673d70..8bd1ad4 100644
--- a/internal/tmuxedit/agent_test.go
+++ b/internal/tmuxedit/agent_test.go
@@ -1,6 +1,8 @@
package tmuxedit
import (
+ "fmt"
+ "strings"
"testing"
)
@@ -86,6 +88,50 @@ func TestBaseAgent_ClearInput_Disabled(t *testing.T) {
}
}
+func TestBaseAgent_ClearInput_EmptyKeys(t *testing.T) {
+ // clearFirst=true but no clearKeys should be a no-op
+ b := &baseAgent{clearFirst: true, clearKeys: ""}
+ err := b.ClearInput("%1")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+}
+
+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 {
+ calls = append(calls, fmt.Sprintf("send:%s:%s", paneID, strings.Join(keys, ",")))
+ return nil
+ }
+
+ b := &baseAgent{clearFirst: true, clearKeys: "C-u"}
+ err := b.ClearInput("%2")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ if len(calls) != 1 || calls[0] != "send:%2:C-u" {
+ t.Errorf("expected single C-u send call, got %v", calls)
+ }
+}
+
+func TestBaseAgent_ClearInput_Error(t *testing.T) {
+ noSleep(t)
+ oldSend := sendKeys
+ defer func() { sendKeys = oldSend }()
+ sendKeys = func(string, ...string) error {
+ return fmt.Errorf("send failed")
+ }
+
+ b := &baseAgent{clearFirst: true, clearKeys: "C-u"}
+ err := b.ClearInput("%1")
+ if err == nil {
+ t.Fatal("expected error from sendClearSequence failure")
+ }
+}
+
func TestBaseAgent_ExtractPrompt_NoPattern(t *testing.T) {
b := &baseAgent{}
got := b.ExtractPrompt("some content")
diff --git a/internal/tmuxedit/agentutil_test.go b/internal/tmuxedit/agentutil_test.go
index 8bf2e64..69111b5 100644
--- a/internal/tmuxedit/agentutil_test.go
+++ b/internal/tmuxedit/agentutil_test.go
@@ -1,7 +1,9 @@
package tmuxedit
import (
+ "fmt"
"regexp"
+ "strings"
"testing"
)
@@ -195,6 +197,64 @@ 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 {
+ 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")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+ want := []string{"Escape", "C-k"}
+ if len(calls) != len(want) {
+ t.Fatalf("got %d calls, want %d: %v", len(calls), len(want), calls)
+ }
+ for i, w := range want {
+ if calls[i] != w {
+ t.Errorf("call[%d] = %q, want %q", i, calls[i], w)
+ }
+ }
+}
+
+func TestSendClearSequence_SingleKeyError(t *testing.T) {
+ oldSend := sendKeys
+ defer func() { sendKeys = oldSend }()
+ sendKeys = func(string, ...string) error {
+ return fmt.Errorf("send failed")
+ }
+
+ err := sendClearSequence("%1", "C-u")
+ if err == nil {
+ t.Fatal("expected error from sendKeys failure")
+ }
+ if !strings.Contains(err.Error(), "clear key") {
+ t.Errorf("error should mention 'clear key', got: %v", err)
+ }
+}
+
+func TestSendClearSequence_RepeatedKeyError(t *testing.T) {
+ oldRepeat := sendRepeatedKey
+ defer func() { sendRepeatedKey = oldRepeat }()
+ sendRepeatedKey = func(string, string, int) error {
+ return fmt.Errorf("repeat failed")
+ }
+
+ err := sendClearSequence("%1", "BSpace*200")
+ if err == nil {
+ t.Fatal("expected error from sendRepeatedKey failure")
+ }
+ if !strings.Contains(err.Error(), "clear key") {
+ t.Errorf("error should mention 'clear key', got: %v", err)
+ }
+}
+
// mustCompile is a test helper that compiles a regex or fails the test.
func mustCompile(t *testing.T, pattern string) *regexp.Regexp {
t.Helper()
diff --git a/internal/tmuxedit/claude_agent_test.go b/internal/tmuxedit/claude_agent_test.go
index 1a80433..d8a68d9 100644
--- a/internal/tmuxedit/claude_agent_test.go
+++ b/internal/tmuxedit/claude_agent_test.go
@@ -103,6 +103,69 @@ func TestClaudeAgent_ClearInput(t *testing.T) {
}
}
+func TestClaudeAgent_ExtractPrompt_EmptyPattern(t *testing.T) {
+ agent := &claudeAgent{baseAgent{promptPat: "", sectionPat: `^─{5,}`}}
+ got := agent.ExtractPrompt("──────\n❯ hello\n──────")
+ if got != "" {
+ t.Errorf("expected empty for empty pattern, got %q", got)
+ }
+}
+
+func TestClaudeAgent_ExtractPrompt_InvalidRegex(t *testing.T) {
+ agent := &claudeAgent{baseAgent{promptPat: "[invalid", sectionPat: `^─{5,}`}}
+ got := agent.ExtractPrompt("──────\n❯ hello\n──────")
+ if got != "" {
+ t.Errorf("expected empty for invalid regex, got %q", got)
+ }
+}
+
+func TestClaudeAgent_ExtractPrompt_ContinuationBreaksOnEmpty(t *testing.T) {
+ agent := newClaudeAgent()
+ // Empty line between prompt blocks should break continuation
+ content := "──────────────\n" +
+ "❯ first line\n" +
+ " continued\n" +
+ "\n" +
+ "unrelated text\n" +
+ "──────────────"
+ got := agent.ExtractPrompt(content)
+ want := "first line\ncontinued"
+ if got != want {
+ t.Errorf("ExtractPrompt() = %q, want %q", got, want)
+ }
+}
+
+func TestClaudeAgent_ClearInput_Disabled(t *testing.T) {
+ agent := &claudeAgent{baseAgent{clearFirst: false, clearKeys: "C-a C-k"}}
+ err := agent.ClearInput("%1")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+}
+
+func TestClaudeAgent_ClearInput_EmptyKeys(t *testing.T) {
+ agent := &claudeAgent{baseAgent{clearFirst: true, clearKeys: ""}}
+ err := agent.ClearInput("%1")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+}
+
+func TestClaudeAgent_ClearInput_Error(t *testing.T) {
+ noSleep(t)
+ oldSend := sendKeys
+ defer func() { sendKeys = oldSend }()
+ sendKeys = func(string, ...string) error {
+ return fmt.Errorf("send failed")
+ }
+
+ agent := newClaudeAgent()
+ err := agent.ClearInput("%1")
+ if err == nil {
+ t.Fatal("expected error from sendClearSequence failure")
+ }
+}
+
func TestClaudeAgent_Detect(t *testing.T) {
agent := newClaudeAgent()
tests := []struct {
diff --git a/internal/tmuxedit/cursor_agent_test.go b/internal/tmuxedit/cursor_agent_test.go
index 28d7fe1..867a55b 100644
--- a/internal/tmuxedit/cursor_agent_test.go
+++ b/internal/tmuxedit/cursor_agent_test.go
@@ -119,6 +119,55 @@ func TestCursorAgent_ClearInput(t *testing.T) {
}
}
+func TestCursorAgent_ExtractPrompt_EmptyPattern(t *testing.T) {
+ // A cursorAgent with empty promptPat returns empty string
+ agent := &cursorAgent{baseAgent{promptPat: ""}}
+ got := agent.ExtractPrompt("│ → hello │")
+ if got != "" {
+ t.Errorf("expected empty for empty pattern, got %q", got)
+ }
+}
+
+func TestCursorAgent_ExtractPrompt_InvalidRegex(t *testing.T) {
+ // A cursorAgent with invalid regex returns empty string
+ agent := &cursorAgent{baseAgent{promptPat: "[invalid"}}
+ got := agent.ExtractPrompt("│ → hello │")
+ if got != "" {
+ t.Errorf("expected empty for invalid regex, got %q", got)
+ }
+}
+
+func TestCursorAgent_ClearInput_Disabled(t *testing.T) {
+ agent := &cursorAgent{baseAgent{clearFirst: false, clearKeys: "End BSpace*200"}}
+ err := agent.ClearInput("%1")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+}
+
+func TestCursorAgent_ClearInput_EmptyKeys(t *testing.T) {
+ agent := &cursorAgent{baseAgent{clearFirst: true, clearKeys: ""}}
+ err := agent.ClearInput("%1")
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+}
+
+func TestCursorAgent_ClearInput_Error(t *testing.T) {
+ noSleep(t)
+ oldSend := sendKeys
+ defer func() { sendKeys = oldSend }()
+ sendKeys = func(string, ...string) error {
+ return fmt.Errorf("send failed")
+ }
+
+ agent := newCursorAgent()
+ err := agent.ClearInput("%1")
+ if err == nil {
+ t.Fatal("expected error from sendClearSequence failure")
+ }
+}
+
func TestCursorAgent_Detect(t *testing.T) {
agent := newCursorAgent()
tests := []struct {
diff --git a/internal/tmuxedit/history_test.go b/internal/tmuxedit/history_test.go
index 6d369fe..b9d59d3 100644
--- a/internal/tmuxedit/history_test.go
+++ b/internal/tmuxedit/history_test.go
@@ -1,6 +1,7 @@
package tmuxedit
import (
+ "fmt"
"os"
"path/filepath"
"testing"
@@ -182,6 +183,133 @@ func TestSplitLines(t *testing.T) {
}
}
+func TestGetHistory_MalformedEntries(t *testing.T) {
+ tmpDir := t.TempDir()
+ t.Setenv("XDG_STATE_HOME", tmpDir)
+
+ // Create state directory and write a file with some valid and invalid lines
+ stateDir := filepath.Join(tmpDir, "state")
+ if err := os.MkdirAll(stateDir, 0o755); err != nil {
+ t.Fatalf("cannot create state dir: %v", err)
+ }
+ historyPath := filepath.Join(stateDir, "tmux-edit-history.jsonl")
+ content := `{"timestamp":"2025-01-01T00:00:00Z","agent":"claude","cwd":"/tmp","text":"valid"}
+not json at all
+{"timestamp":"2025-01-02T00:00:00Z","agent":"aider","cwd":"/home","text":"also valid"}
+`
+ if err := os.WriteFile(historyPath, []byte(content), 0o644); err != nil {
+ t.Fatalf("cannot write history file: %v", err)
+ }
+
+ entries, err := GetHistory(0)
+ if err != nil {
+ t.Fatalf("GetHistory failed: %v", err)
+ }
+ // Should skip the malformed line and return the 2 valid entries
+ if len(entries) != 2 {
+ t.Fatalf("expected 2 entries, got %d", len(entries))
+ }
+ if entries[0].Text != "valid" {
+ t.Errorf("entries[0].Text = %q, want 'valid'", entries[0].Text)
+ }
+ if entries[1].Text != "also valid" {
+ t.Errorf("entries[1].Text = %q, want 'also valid'", entries[1].Text)
+ }
+}
+
+func TestGetHistory_EmptyLines(t *testing.T) {
+ tmpDir := t.TempDir()
+ t.Setenv("XDG_STATE_HOME", tmpDir)
+
+ stateDir := filepath.Join(tmpDir, "state")
+ if err := os.MkdirAll(stateDir, 0o755); err != nil {
+ t.Fatalf("cannot create state dir: %v", err)
+ }
+ historyPath := filepath.Join(stateDir, "tmux-edit-history.jsonl")
+ // File with empty lines interspersed
+ content := "\n" +
+ `{"timestamp":"2025-01-01T00:00:00Z","agent":"claude","cwd":"/tmp","text":"entry1"}` + "\n" +
+ "\n\n" +
+ `{"timestamp":"2025-01-02T00:00:00Z","agent":"aider","cwd":"/home","text":"entry2"}` + "\n"
+ if err := os.WriteFile(historyPath, []byte(content), 0o644); err != nil {
+ t.Fatalf("cannot write history file: %v", err)
+ }
+
+ entries, err := GetHistory(0)
+ if err != nil {
+ t.Fatalf("GetHistory failed: %v", err)
+ }
+ if len(entries) != 2 {
+ t.Fatalf("expected 2 entries (skipping empty lines), got %d", len(entries))
+ }
+}
+
+func TestGetHistory_LimitZeroReturnsAll(t *testing.T) {
+ tmpDir := t.TempDir()
+ t.Setenv("XDG_STATE_HOME", tmpDir)
+
+ for i := 0; i < 5; i++ {
+ if err := AppendHistory(fmt.Sprintf("entry%d", i), "claude", "/tmp"); err != nil {
+ t.Fatalf("AppendHistory failed: %v", err)
+ }
+ }
+
+ entries, err := GetHistory(0)
+ if err != nil {
+ t.Fatalf("GetHistory failed: %v", err)
+ }
+ if len(entries) != 5 {
+ t.Errorf("expected 5 entries with limit=0, got %d", len(entries))
+ }
+}
+
+func TestGetHistory_LimitLargerThanEntries(t *testing.T) {
+ tmpDir := t.TempDir()
+ t.Setenv("XDG_STATE_HOME", tmpDir)
+
+ if err := AppendHistory("only one", "claude", "/tmp"); err != nil {
+ t.Fatalf("AppendHistory failed: %v", err)
+ }
+
+ entries, err := GetHistory(100)
+ if err != nil {
+ t.Fatalf("GetHistory failed: %v", err)
+ }
+ if len(entries) != 1 {
+ t.Errorf("expected 1 entry with large limit, got %d", len(entries))
+ }
+}
+
+func TestAppendHistory_InvalidStateDir(t *testing.T) {
+ // Point XDG_STATE_HOME to a path that can't be created (file, not dir)
+ tmpDir := t.TempDir()
+ blockingFile := filepath.Join(tmpDir, "blocker")
+ if err := os.WriteFile(blockingFile, []byte("x"), 0o644); err != nil {
+ t.Fatalf("cannot create blocking file: %v", err)
+ }
+ // Set state home to a path under the file (impossible to mkdir)
+ t.Setenv("XDG_STATE_HOME", filepath.Join(blockingFile, "sub"))
+
+ err := AppendHistory("text", "agent", "/cwd")
+ if err == nil {
+ t.Fatal("expected error when state directory cannot be created")
+ }
+}
+
+func TestGetHistory_InvalidStateDir(t *testing.T) {
+ tmpDir := t.TempDir()
+ blockingFile := filepath.Join(tmpDir, "blocker")
+ if err := os.WriteFile(blockingFile, []byte("x"), 0o644); err != nil {
+ t.Fatalf("cannot create blocking file: %v", err)
+ }
+ t.Setenv("XDG_STATE_HOME", filepath.Join(blockingFile, "sub"))
+
+ _, err := GetHistory(0)
+ if err == nil {
+ t.Fatal("expected error when state directory cannot be created")
+ }
+}
+
func containsString(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(s) > len(substr) && findSubstring(s, substr))
}
diff --git a/internal/tmuxedit/run_test.go b/internal/tmuxedit/run_test.go
index 1b603e4..c150cbd 100644
--- a/internal/tmuxedit/run_test.go
+++ b/internal/tmuxedit/run_test.go
@@ -2,6 +2,7 @@ package tmuxedit
import (
"fmt"
+ "log"
"strings"
"testing"
@@ -306,6 +307,106 @@ func TestRunWithConfig_EditorError(t *testing.T) {
}
}
+func TestLogPaneLines_WithDebugLog(t *testing.T) {
+ // Set up debugLog to a buffer to cover the logging branch
+ var buf strings.Builder
+ oldDebugLog := debugLog
+ debugLog = log.New(&buf, "", 0)
+ defer func() { debugLog = oldDebugLog }()
+
+ // Content with box-drawing and arrow characters triggers logging
+ content := "normal line\n│ box line\n→ arrow line\nplain"
+ logPaneLines(content)
+
+ output := buf.String()
+ if !strings.Contains(output, "box line") {
+ t.Errorf("expected log of box-drawing line, got: %s", output)
+ }
+ if !strings.Contains(output, "arrow line") {
+ t.Errorf("expected log of arrow line, got: %s", output)
+ }
+}
+
+func TestLogPaneLines_WithoutDebugLog(t *testing.T) {
+ // When debugLog is nil, logPaneLines should not panic
+ oldDebugLog := debugLog
+ debugLog = nil
+ defer func() { debugLog = oldDebugLog }()
+
+ logPaneLines("│ test line\n→ arrow")
+ // No panic means pass
+}
+
+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) {
+ return []byte("%1"), nil
+ }
+ capturePane = func(string) (string, error) {
+ return "claude code v1.0\n──────\n❯ fix the bug\n──────", nil
+ }
+ openEditorPopup = func(string, string, string) (string, error) {
+ return "new text", nil
+ }
+ sendKeys = func(string, ...string) error {
+ return fmt.Errorf("clear input failed")
+ }
+
+ cfg := appconfig.App{}
+ err := 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 := openEditorPopup
+ oldRunCmd := runCommand
+ defer func() {
+ capturePane = oldCapture
+ sendKeys = oldSendKeys
+ openEditorPopup = oldEditorPopup
+ runCommand = oldRunCmd
+ }()
+
+ runCommand = func(name string, args ...string) ([]byte, error) {
+ return []byte("%1"), nil
+ }
+ // Use generic agent (no clear) so ClearInput succeeds
+ capturePane = func(string) (string, error) {
+ return "some unknown pane content", nil
+ }
+ openEditorPopup = func(string, string, string) (string, error) {
+ return "new text", nil
+ }
+ callCount := 0
+ sendKeys = func(string, ...string) error {
+ callCount++
+ // Fail on text send (generic agent has no clear)
+ return fmt.Errorf("send text failed")
+ }
+
+ cfg := appconfig.App{}
+ err := runWithConfig(Options{}, cfg)
+ if err == nil || !strings.Contains(err.Error(), "send text failed") {
+ t.Errorf("expected send text error, got: %v", err)
+ }
+}
+
func TestRunWithConfig_PaneResolveError(t *testing.T) {
oldRunCmd := runCommand
defer func() { runCommand = oldRunCmd }()