diff options
Diffstat (limited to 'internal/tmuxedit')
| -rw-r--r-- | internal/tmuxedit/agent_test.go | 46 | ||||
| -rw-r--r-- | internal/tmuxedit/agentutil_test.go | 60 | ||||
| -rw-r--r-- | internal/tmuxedit/claude_agent_test.go | 63 | ||||
| -rw-r--r-- | internal/tmuxedit/cursor_agent_test.go | 49 | ||||
| -rw-r--r-- | internal/tmuxedit/history_test.go | 128 | ||||
| -rw-r--r-- | internal/tmuxedit/run_test.go | 101 |
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 }() |
