From 2df779de58ff8bd0ad33df2b2ad1c80b7ade0d1f Mon Sep 17 00:00:00 2001 From: Alvin Unreal Date: Sat, 16 Aug 2025 07:35:43 +0000 Subject: [PATCH 1/3] Add tests for process_message --- .idx/dev.nix | 56 ++++ go.mod | 4 + go.sum | 1 + internal/confirm.go | 4 +- internal/manager.go | 7 + internal/pane_details.go | 2 +- internal/process_message.go | 9 +- internal/process_message_test.go | 456 +++++++++++++++++++++++++++++++ system/tmux.go | 6 +- system/tmux_send.go | 2 +- system/utils.go | 2 +- 11 files changed, 539 insertions(+), 10 deletions(-) create mode 100644 .idx/dev.nix create mode 100644 internal/process_message_test.go diff --git a/.idx/dev.nix b/.idx/dev.nix new file mode 100644 index 0000000..2f11d06 --- /dev/null +++ b/.idx/dev.nix @@ -0,0 +1,56 @@ +# To learn more about how to use Nix to configure your environment +# see: https://firebase.google.com/docs/studio/customize-workspace +{ pkgs, ... }: { + # Which nixpkgs channel to use. + channel = "stable-24.05"; # or "unstable" + + # Use https://search.nixos.org/packages to find packages + packages = [ + pkgs.go + # pkgs.python311 + # pkgs.python311Packages.pip + # pkgs.nodejs_20 + # pkgs.nodePackages.nodemon + ]; + + # Sets environment variables in the workspace + env = {}; + idx = { + # Search for the extensions you want on https://open-vsx.org/ and use "publisher.id" + extensions = [ + # "vscodevim.vim" + ]; + + # Enable previews + previews = { + enable = true; + previews = { + # web = { + # # Example: run "npm run dev" with PORT set to IDX's defined port for previews, + # # and show it in IDX's web preview panel + # command = ["npm" "run" "dev"]; + # manager = "web"; + # env = { + # # Environment variables to set for your server + # PORT = "$PORT"; + # }; + # }; + }; + }; + + # Workspace lifecycle hooks + workspace = { + # Runs when a workspace is first created + onCreate = { + # Example: install JS dependencies from NPM + # npm-install = "npm install"; + }; + # Runs when the workspace is (re)started + onStart = { + # Example: start a background task to watch and re-build backend code + # watch-backend = "npm run watch-backend"; + run-go-app = "go run main.go"; + }; + }; + }; +} diff --git a/go.mod b/go.mod index 95e81aa..544602c 100644 --- a/go.mod +++ b/go.mod @@ -13,9 +13,11 @@ require ( github.com/nyaosorg/go-readline-ny v1.9.1 github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.18.2 + github.com/stretchr/testify v1.8.4 ) require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dlclark/regexp2 v1.10.0 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -28,6 +30,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/nyaosorg/go-box/v2 v2.2.1 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect @@ -35,6 +38,7 @@ require ( github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/pflag v1.0.5 // indirect + github.com/stretchr/objx v0.5.0 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect diff --git a/go.sum b/go.sum index 80c0bcd..6fd4949 100644 --- a/go.sum +++ b/go.sum @@ -80,6 +80,7 @@ github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= diff --git a/internal/confirm.go b/internal/confirm.go index 9048372..6c95046 100644 --- a/internal/confirm.go +++ b/internal/confirm.go @@ -9,7 +9,7 @@ import ( "github.com/fatih/color" ) -func (m *Manager) confirmedToExec(command string, prompt string, edit bool) (bool, string) { +func (m *Manager) confirmedToExecFn(command string, prompt string, edit bool) (bool, string) { isSafe, _ := m.whitelistCheck(command) if isSafe { return true, command @@ -96,7 +96,7 @@ func (m *Manager) confirmedToExec(command string, prompt string, edit bool) (boo return false, "" default: // any other input is retry confirmation - return m.confirmedToExec(command, prompt, edit) + return m.confirmedToExecFn(command, prompt, edit) } } diff --git a/internal/manager.go b/internal/manager.go index 9e7353e..db6bea8 100644 --- a/internal/manager.go +++ b/internal/manager.go @@ -42,6 +42,10 @@ type Manager struct { WatchMode bool OS string SessionOverrides map[string]interface{} // session-only config overrides + + // Functions for mocking + confirmedToExec func(command string, prompt string, edit bool) (bool, string) + getTmuxPanesInXml func(config *config.Config) string } // NewManager creates a new manager agent @@ -84,6 +88,9 @@ func NewManager(cfg *config.Config) (*Manager, error) { SessionOverrides: make(map[string]interface{}), } + manager.confirmedToExec = manager.confirmedToExecFn + manager.getTmuxPanesInXml = manager.getTmuxPanesInXmlFn + manager.InitExecPane() return manager, nil } diff --git a/internal/pane_details.go b/internal/pane_details.go index 434570f..2fed215 100644 --- a/internal/pane_details.go +++ b/internal/pane_details.go @@ -27,7 +27,7 @@ func (m *Manager) GetTmuxPanes() ([]system.TmuxPaneDetails, error) { return currentPanes, nil } -func (m *Manager) GetTmuxPanesInXml(config *config.Config) string { +func (m *Manager) getTmuxPanesInXmlFn(config *config.Config) string { currentTmuxWindow := strings.Builder{} currentTmuxWindow.WriteString("\n") panes, _ := m.GetTmuxPanes() diff --git a/internal/process_message.go b/internal/process_message.go index 13f7693..d80c65a 100644 --- a/internal/process_message.go +++ b/internal/process_message.go @@ -28,7 +28,7 @@ func (m *Manager) ProcessUserMessage(ctx context.Context, message string) bool { return false } - currentTmuxWindow := m.GetTmuxPanesInXml(m.Config) + currentTmuxWindow := m.getTmuxPanesInXml(m.Config) execPaneEnv := "" if !m.ExecPane.IsSubShell { execPaneEnv = fmt.Sprintf("Keep in mind, you are working within the shell: %s and OS: %s", m.ExecPane.Shell, m.ExecPane.OS) @@ -300,7 +300,12 @@ func (m *Manager) aiFollowedGuidelines(r AIResponse) (string, bool) { } // Check if only one tag is used - tags := []int{len(r.ExecCommand), len(r.SendKeys), len(r.PasteMultilineContent)} + tags := []int{len(r.ExecCommand), len(r.SendKeys)} + if r.PasteMultilineContent != "" { + tags = append(tags, 1) + } else { + tags = append(tags, 0) + } count := 0 for _, len := range tags { if len > 0 { diff --git a/internal/process_message_test.go b/internal/process_message_test.go new file mode 100644 index 0000000..780a9e6 --- /dev/null +++ b/internal/process_message_test.go @@ -0,0 +1,456 @@ +package internal + +import ( + "context" + "testing" + + "github.com/alvinunreal/tmuxai/config" + "github.com/alvinunreal/tmuxai/system" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +// AiClientInterface defines the interface for AI clients to make testing easier +type AiClientInterface interface { + GetResponseFromChatMessages(ctx context.Context, messages []ChatMessage, model string) (string, error) + ChatCompletion(ctx context.Context, messages []Message, model string) (string, error) +} + +// MockAiClient is a mock implementation of AiClientInterface for testing +type MockAiClient struct { + mock.Mock +} + +func (m *MockAiClient) GetResponseFromChatMessages(ctx context.Context, messages []ChatMessage, model string) (string, error) { + args := m.Called(ctx, messages, model) + return args.String(0), args.Error(1) +} + +func (m *MockAiClient) ChatCompletion(ctx context.Context, messages []Message, model string) (string, error) { + args := m.Called(ctx, messages, model) + return args.String(0), args.Error(1) +} + +// Test: ProcessUserMessage with empty status returns false immediately +func TestProcessUserMessage_EmptyStatus(t *testing.T) { + cfg := &config.Config{ + Debug: false, + } + + manager := &Manager{ + Config: cfg, + Status: "", // Empty status + Messages: []ChatMessage{}, + } + + // Mock functions that would normally be called + manager.confirmedToExec = func(command string, prompt string, edit bool) (bool, string) { + return true, command + } + + manager.getTmuxPanesInXml = func(config *config.Config) string { + return "mock pane content" + } + + result := manager.ProcessUserMessage(context.Background(), "test message") + + assert.False(t, result, "ProcessUserMessage should return false when status is empty") +} + +// Test: ProcessUserMessage with context squash requirement +func TestProcessUserMessage_ContextSquash(t *testing.T) { + cfg := &config.Config{ + Debug: false, + MaxContextSize: 100, // Very small to trigger squash + OpenRouter: config.OpenRouterConfig{ + Model: "test-model", + }, + } + + manager := &Manager{ + Config: cfg, + Status: "running", + Messages: make([]ChatMessage, 0), + ExecPane: &system.TmuxPaneDetails{ + IsPrepared: false, + IsSubShell: false, + }, + WatchMode: false, + } + + // Mock functions that would normally be called + manager.confirmedToExec = func(command string, prompt string, edit bool) (bool, string) { + return true, command + } + + manager.getTmuxPanesInXml = func(config *config.Config) string { + return "mock pane content" + } + + // Add enough messages to trigger squash + for i := 0; i < 10; i++ { + manager.Messages = append(manager.Messages, ChatMessage{ + Content: "This is a very long message that will fill up the context size limit to trigger squashing behavior in the process user message function", + FromUser: true, + }) + } + + // Check that needSquash would return true + assert.True(t, manager.needSquash(), "Should need squash with many messages") +} + +// Test: AI guidelines validation with multiple boolean flags should fail +func TestProcessUserMessage_AIGuidelinesValidation(t *testing.T) { + manager := &Manager{ + WatchMode: false, + } + + // Test case 1: Multiple boolean flags set to true (should fail) + response1 := AIResponse{ + Message: "Test message", + RequestAccomplished: true, + ExecPaneSeemsBusy: true, // This should cause validation to fail + WaitingForUserResponse: false, + NoComment: false, + ExecCommand: []string{"echo hello"}, + } + + guidelineError, valid := manager.aiFollowedGuidelines(response1) + assert.False(t, valid, "Should fail validation when multiple boolean flags are set") + assert.Contains(t, guidelineError, "Only one boolean flag should be set", "Error message should mention boolean flags") + + // Test case 2: Multiple XML tag types used (should fail) + response2 := AIResponse{ + Message: "Test message", + RequestAccomplished: true, + ExecCommand: []string{"echo hello"}, + SendKeys: []string{"ctrl+c"}, // Having both ExecCommand and SendKeys should fail + PasteMultilineContent: "", + } + + guidelineError2, valid2 := manager.aiFollowedGuidelines(response2) + assert.False(t, valid2, "Should fail validation when multiple XML tag types are used") + assert.Contains(t, guidelineError2, "only use one type of XML tag", "Error message should mention XML tags") + + // Test case 3: Valid response (should pass) + response3 := AIResponse{ + Message: "Test message", + RequestAccomplished: true, + ExecCommand: []string{"echo hello"}, + } + + _, valid3 := manager.aiFollowedGuidelines(response3) + assert.True(t, valid3, "Should pass validation with correct format") +} + +// Test: ProcessUserMessage with WaitingForUserResponse sets status correctly +func TestProcessUserMessage_WaitingForUserResponse(t *testing.T) { + manager := &Manager{ + Status: "running", + Messages: []ChatMessage{}, + } + + // Test the aiFollowedGuidelines and status setting behavior + response := AIResponse{ + Message: "Waiting for your input", + WaitingForUserResponse: true, + } + + // Simulate what happens in ProcessUserMessage when WaitingForUserResponse is true + if response.WaitingForUserResponse { + manager.Status = "waiting" + } + + assert.Equal(t, "waiting", manager.Status, "Status should be set to 'waiting' when WaitingForUserResponse is true") + + // Test that aiFollowedGuidelines accepts this valid response + _, valid := manager.aiFollowedGuidelines(response) + assert.True(t, valid, "WaitingForUserResponse should be valid according to guidelines") +} + +// Test: ExecCommand processing with confirmation +func TestProcessUserMessage_ExecCommandWithConfirmation(t *testing.T) { + cfg := &config.Config{ + Debug: false, + ExecConfirm: true, // Require confirmation for exec commands + } + + manager := &Manager{ + Config: cfg, + Status: "running", + Messages: []ChatMessage{}, + ExecPane: &system.TmuxPaneDetails{ + Id: "test-pane", + IsPrepared: false, + IsSubShell: false, + }, + } + + // Track if confirmation was called and approved + confirmationCalled := false + commandExecuted := "" + + manager.confirmedToExec = func(command string, prompt string, edit bool) (bool, string) { + confirmationCalled = true + assert.Equal(t, "echo hello", command, "Should ask confirmation for the right command") + assert.Equal(t, "Execute this command?", prompt, "Should use correct confirmation prompt") + return true, command // User approves the command + } + + // Mock tmux command execution by capturing what would be sent + originalTmuxSend := system.TmuxSendCommandToPane + defer func() { system.TmuxSendCommandToPane = originalTmuxSend }() + + system.TmuxSendCommandToPane = func(paneId string, command string, enter bool) error { + commandExecuted = command + assert.Equal(t, "test-pane", paneId, "Should send command to correct pane") + assert.True(t, enter, "Should send enter key after command") + return nil + } + + // Test the ExecCommand processing logic directly + response := AIResponse{ + Message: "I'll run this command for you", + ExecCommand: []string{"echo hello"}, + } + + // Simulate the ExecCommand processing loop from ProcessUserMessage + for _, execCommand := range response.ExecCommand { + isSafe := false + command := execCommand + if manager.GetExecConfirm() { + isSafe, command = manager.confirmedToExec(execCommand, "Execute this command?", true) + } else { + isSafe = true + } + if isSafe { + if manager.ExecPane.IsPrepared { + // Would call m.ExecWaitCapture(command) + } else { + system.TmuxSendCommandToPane(manager.ExecPane.Id, command, true) + } + } + } + + assert.True(t, confirmationCalled, "Confirmation should have been called") + assert.Equal(t, "echo hello", commandExecuted, "Command should have been executed") +} + +// Test: ExecCommand rejection clears status and returns false +func TestProcessUserMessage_ExecCommandRejection(t *testing.T) { + cfg := &config.Config{ + Debug: false, + ExecConfirm: true, // Require confirmation for exec commands + } + + manager := &Manager{ + Config: cfg, + Status: "running", + Messages: []ChatMessage{}, + ExecPane: &system.TmuxPaneDetails{ + Id: "test-pane", + IsPrepared: false, + IsSubShell: false, + }, + } + + // Track if confirmation was called + confirmationCalled := false + commandExecuted := false + + manager.confirmedToExec = func(command string, prompt string, edit bool) (bool, string) { + confirmationCalled = true + assert.Equal(t, "rm -rf /", command, "Should ask confirmation for the dangerous command") + return false, command // User rejects the command + } + + // Mock tmux command execution to track if it was called + originalTmuxSend := system.TmuxSendCommandToPane + defer func() { system.TmuxSendCommandToPane = originalTmuxSend }() + + system.TmuxSendCommandToPane = func(paneId string, command string, enter bool) error { + commandExecuted = true + return nil + } + + // Test the ExecCommand processing logic that should result in rejection + response := AIResponse{ + ExecCommand: []string{"rm -rf /"}, + } + + // Simulate the ExecCommand processing loop from ProcessUserMessage + statusCleared := false + for _, execCommand := range response.ExecCommand { + isSafe := false + command := execCommand + if manager.GetExecConfirm() { + isSafe, command = manager.confirmedToExec(execCommand, "Execute this command?", true) + } else { + isSafe = true + } + if isSafe { + if manager.ExecPane.IsPrepared { + // Would call m.ExecWaitCapture(command) + } else { + system.TmuxSendCommandToPane(manager.ExecPane.Id, command, true) + } + } else { + manager.Status = "" + statusCleared = true + break // This simulates the return false in ProcessUserMessage + } + } + + assert.True(t, confirmationCalled, "Confirmation should have been called") + assert.False(t, commandExecuted, "Command should NOT have been executed when rejected") + assert.True(t, statusCleared, "Status should be cleared when command is rejected") + assert.Equal(t, "", manager.Status, "Status should be empty after rejection") +} + +// Test: RequestAccomplished clears status and returns true +func TestProcessUserMessage_RequestAccomplished(t *testing.T) { + manager := &Manager{ + Status: "running", + Messages: []ChatMessage{}, + } + + // Test the RequestAccomplished logic directly + response := AIResponse{ + Message: "Task completed successfully!", + RequestAccomplished: true, + } + + // Simulate what happens in ProcessUserMessage when RequestAccomplished is true + result := false + if response.RequestAccomplished { + manager.Status = "" + result = true + } + + assert.True(t, result, "Should return true when RequestAccomplished") + assert.Equal(t, "", manager.Status, "Status should be cleared when request is accomplished") + + // Verify this is a valid response according to guidelines + _, valid := manager.aiFollowedGuidelines(response) + assert.True(t, valid, "RequestAccomplished should be valid according to guidelines") +} + +// Test: SendKeys processing with confirmation +func TestProcessUserMessage_SendKeysProcessing(t *testing.T) { + cfg := &config.Config{ + Debug: false, + SendKeysConfirm: true, // Require confirmation for send keys + } + + manager := &Manager{ + Config: cfg, + Status: "running", + Messages: []ChatMessage{}, + ExecPane: &system.TmuxPaneDetails{ + Id: "test-pane", + IsPrepared: false, + IsSubShell: false, + }, + } + + // Track confirmations and keys sent + confirmationCalled := false + keysSent := []string{} + + manager.confirmedToExec = func(command string, prompt string, edit bool) (bool, string) { + confirmationCalled = true + assert.Equal(t, "keys shown above", command, "Should show generic description for keys") + assert.Equal(t, "Send all these keys?", prompt, "Should use correct prompt for multiple keys") + return true, command // User approves sending keys + } + + // Mock tmux command execution to capture keys being sent + originalTmuxSend := system.TmuxSendCommandToPane + defer func() { system.TmuxSendCommandToPane = originalTmuxSend }() + + system.TmuxSendCommandToPane = func(paneId string, command string, enter bool) error { + keysSent = append(keysSent, command) + assert.Equal(t, "test-pane", paneId, "Should send keys to correct pane") + assert.False(t, enter, "Should NOT send enter key for SendKeys") + return nil + } + + // Test the SendKeys processing logic directly + response := AIResponse{ + Message: "I'll send these keys for you", + SendKeys: []string{"ctrl+c", "ctrl+d", "exit"}, + } + + // Simulate the SendKeys processing logic from ProcessUserMessage + if len(response.SendKeys) > 0 { + // Determine confirmation message based on number of keys + confirmMessage := "Send this key?" + if len(response.SendKeys) > 1 { + confirmMessage = "Send all these keys?" + } + + // Get confirmation if required + allConfirmed := true + if manager.GetSendKeysConfirm() { + allConfirmed, _ = manager.confirmedToExec("keys shown above", confirmMessage, true) + } + + if allConfirmed { + // Send each key with delay (without the actual delay in test) + for _, sendKey := range response.SendKeys { + system.TmuxSendCommandToPane(manager.ExecPane.Id, sendKey, false) + } + } + } + + assert.True(t, confirmationCalled, "Confirmation should have been called") + assert.Equal(t, []string{"ctrl+c", "ctrl+d", "exit"}, keysSent, "All keys should have been sent in order") + + // Verify this is a valid response according to guidelines + _, valid := manager.aiFollowedGuidelines(response) + assert.True(t, valid, "SendKeys should be valid according to guidelines") +} + +// Test: Watch mode NoComment behavior +func TestProcessUserMessage_WatchModeNoComment(t *testing.T) { + manager := &Manager{ + Status: "running", + Messages: []ChatMessage{}, + WatchMode: true, // Enable watch mode + } + + // Test the NoComment logic in watch mode + response := AIResponse{ + NoComment: true, // AI has no comment in watch mode + } + + // Simulate what happens in ProcessUserMessage for watch mode with NoComment + result := false + if response.NoComment { + result = false // In watch mode, NoComment means return false (continue watching) + } + + assert.False(t, result, "Should return false for NoComment in watch mode") + + // Verify this is a valid response according to guidelines when in watch mode + _, valid := manager.aiFollowedGuidelines(response) + assert.True(t, valid, "NoComment should be valid according to guidelines in watch mode") + + // Test that NoComment is valid even outside watch mode according to current logic + manager.WatchMode = false + response2 := AIResponse{ + NoComment: true, + } + + // NoComment alone is actually valid even when not in watch mode (boolCount=1 satisfies count+boolCount > 0) + _, valid2 := manager.aiFollowedGuidelines(response2) + assert.True(t, valid2, "NoComment alone should be valid according to current guidelines logic") + + // Test truly invalid case: no boolean flags and no XML tags when not in watch mode + response3 := AIResponse{ + Message: "Just a message with nothing else", + } + + _, valid3 := manager.aiFollowedGuidelines(response3) + assert.False(t, valid3, "Empty response (no flags, no XML tags) should fail validation when not in watch mode") +} \ No newline at end of file diff --git a/system/tmux.go b/system/tmux.go index d12dfc4..3362349 100644 --- a/system/tmux.go +++ b/system/tmux.go @@ -29,7 +29,7 @@ func TmuxCreateNewPane(target string) (string, error) { } // TmuxPanesDetails gets details for all panes in a target window -func TmuxPanesDetails(target string) ([]TmuxPaneDetails, error) { +var TmuxPanesDetails = func(target string) ([]TmuxPaneDetails, error) { cmd := exec.Command("tmux", "list-panes", "-t", target, "-F", "#{pane_id},#{pane_active},#{pane_pid},#{pane_current_command},#{history_size},#{history_limit}") var stdout, stderr bytes.Buffer cmd.Stdout = &stdout @@ -93,7 +93,7 @@ func TmuxPanesDetails(target string) ([]TmuxPaneDetails, error) { } // TmuxCapturePane gets the content of a specific pane by ID -func TmuxCapturePane(paneId string, maxLines int) (string, error) { +var TmuxCapturePane = func(paneId string, maxLines int) (string, error) { cmd := exec.Command("tmux", "capture-pane", "-p", "-t", paneId, "-S", fmt.Sprintf("-%d", maxLines)) var stdout, stderr bytes.Buffer cmd.Stdout = &stdout @@ -134,7 +134,7 @@ func TmuxCurrentWindowTarget() (string, error) { return target, nil } -func TmuxCurrentPaneId() (string, error) { +var TmuxCurrentPaneId = func() (string, error) { tmuxPane := os.Getenv("TMUX_PANE") if tmuxPane == "" { return "", fmt.Errorf("TMUX_PANE environment variable not set") diff --git a/system/tmux_send.go b/system/tmux_send.go index 543e0b3..7eb897c 100644 --- a/system/tmux_send.go +++ b/system/tmux_send.go @@ -9,7 +9,7 @@ import ( "github.com/alvinunreal/tmuxai/logger" ) -func TmuxSendCommandToPane(paneId string, command string, autoenter bool) error { +var TmuxSendCommandToPane = func(paneId string, command string, autoenter bool) error { lines := strings.Split(command, "\n") for i, line := range lines { diff --git a/system/utils.go b/system/utils.go index 90f6566..c3495da 100644 --- a/system/utils.go +++ b/system/utils.go @@ -66,7 +66,7 @@ func GetProcessArgs(pid int) string { return cmdOutput } -func HighlightCode(language string, code string) (string, error) { +var HighlightCode = func(language string, code string) (string, error) { // Get the lexer for the specified language lexer := lexers.Get(language) if lexer == nil { From 9a368760a69cfe4d891980c6ded1a520a324182f Mon Sep 17 00:00:00 2001 From: Alvin Unreal Date: Sat, 16 Aug 2025 12:00:40 +0200 Subject: [PATCH 2/3] Run tests on github --- .github/workflows/lint.yml | 4 ++-- .github/workflows/test.yml | 10 ++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 6264127..269eb3f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -1,8 +1,8 @@ -name: K9s Lint +name: Lint on: pull_request: - branches: [master] + branches: [main] jobs: golangci: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1621abd..fd0ab61 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,16 +1,16 @@ -name: K9s Test +name: Test on: workflow_dispatch: push: branches: - - master + - main tags: - rc* - v* pull_request: branches: - - master + - main jobs: build: runs-on: ubuntu-latest @@ -28,6 +28,4 @@ jobs: run: go env -w CGO_ENABLED=0 - name: Run Tests - run: make test - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: go test ./... From 7b66e73103092ed96219d71e57cef8e000673a6c Mon Sep 17 00:00:00 2001 From: Alvin Unreal Date: Sat, 16 Aug 2025 12:22:40 +0200 Subject: [PATCH 3/3] Fix linting --- config/config.go | 12 +++-- internal/ai_client.go | 18 +++---- internal/ai_client_test.go | 2 +- internal/chat.go | 2 +- internal/chat_command.go | 6 +-- internal/config_helpers.go | 6 +-- internal/confirm.go | 4 +- internal/countdown.go | 3 +- internal/exec_pane.go | 18 ++----- internal/manager.go | 4 +- internal/process_message.go | 10 ++-- internal/process_message_test.go | 80 ++++++++++++++++---------------- 12 files changed, 79 insertions(+), 86 deletions(-) diff --git a/config/config.go b/config/config.go index 542d72c..85eff82 100644 --- a/config/config.go +++ b/config/config.go @@ -101,7 +101,7 @@ func Load() (*Config, error) { // Automatically bind all config keys to environment variables configType := reflect.TypeOf(*config) for _, key := range EnumerateConfigKeys(configType, "") { - viper.BindEnv(key) + _ = viper.BindEnv(key) } viper.AutomaticEnv() @@ -177,9 +177,10 @@ func TryInferType(key, value string) any { if key == fullKey { switch field.Type.Kind() { case reflect.Bool: - if value == "true" { + switch value { + case "true": typedValue = true - } else if value == "false" { + case "false": typedValue = false } case reflect.Int, reflect.Int64, reflect.Int32: @@ -205,9 +206,10 @@ func TryInferType(key, value string) any { if ntag == nestedKey { switch nf.Type.Kind() { case reflect.Bool: - if value == "true" { + switch value { + case "true": typedValue = true - } else if value == "false" { + case "false": typedValue = false } case reflect.Int, reflect.Int64, reflect.Int32: diff --git a/internal/ai_client.go b/internal/ai_client.go index 40a8332..885f3cd 100644 --- a/internal/ai_client.go +++ b/internal/ai_client.go @@ -150,7 +150,7 @@ func (c *AiClient) ChatCompletion(ctx context.Context, messages []Message, model logger.Error("Failed to send request: %v", err) return "", fmt.Errorf("failed to send request: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() // Read the response body, err := io.ReadAll(resp.Body) @@ -194,7 +194,7 @@ func debugChatMessages(chatMessages []ChatMessage, response string) { debugDir := fmt.Sprintf("%s/debug", configDir) if _, err := os.Stat(debugDir); os.IsNotExist(err) { - os.Mkdir(debugDir, 0755) + _ = os.Mkdir(debugDir, 0755) } debugFileName := fmt.Sprintf("%s/debug-%s.txt", debugDir, timestamp) @@ -204,9 +204,9 @@ func debugChatMessages(chatMessages []ChatMessage, response string) { logger.Error("Failed to create debug file: %v", err) return } - defer file.Close() + defer func() { _ = file.Close() }() - file.WriteString("================== SENT CHAT MESSAGES ==================\n\n") + _, _ = file.WriteString("================== SENT CHAT MESSAGES ==================\n\n") for i, msg := range chatMessages { role := "assistant" @@ -218,11 +218,11 @@ func debugChatMessages(chatMessages []ChatMessage, response string) { } timeStr := msg.Timestamp.Format(time.RFC3339) - file.WriteString(fmt.Sprintf("Message %d: Role=%s, Time=%s\n", i+1, role, timeStr)) - file.WriteString(fmt.Sprintf("Content:\n%s\n\n", msg.Content)) + _, _ = fmt.Fprintf(file, "Message %d: Role=%s, Time=%s\n", i+1, role, timeStr) + _, _ = fmt.Fprintf(file, "Content:\n%s\n\n", msg.Content) } - file.WriteString("================== RECEIVED RESPONSE ==================\n\n") - file.WriteString(response) - file.WriteString("\n\n================== END DEBUG ==================\n") + _, _ = file.WriteString("================== RECEIVED RESPONSE ==================\n\n") + _, _ = file.WriteString(response) + _, _ = file.WriteString("\n\n================== END DEBUG ==================\n") } diff --git a/internal/ai_client_test.go b/internal/ai_client_test.go index 87b6dba..2b25e8c 100644 --- a/internal/ai_client_test.go +++ b/internal/ai_client_test.go @@ -21,7 +21,7 @@ func TestAzureOpenAIEndpoint(t *testing.T) { t.Errorf("missing api-key header") } w.Header().Set("Content-Type", "application/json") - w.Write([]byte(`{"choices":[{"message":{"content":"ok"}}]}`)) + _, _ = w.Write([]byte(`{"choices":[{"message":{"content":"ok"}}]}`)) })) defer server.Close() diff --git a/internal/chat.go b/internal/chat.go index e79d9ac..deb0574 100644 --- a/internal/chat.go +++ b/internal/chat.go @@ -94,7 +94,7 @@ func (c *CLIInterface) Start(initMessage string) error { historyLines = append(historyLines, history.At(i)) } historyData := strings.Join(historyLines, "\n") - os.WriteFile(historyFilePath, []byte(historyData), 0644) + _ = os.WriteFile(historyFilePath, []byte(historyData), 0644) } // Process the input (preserving multiline content) diff --git a/internal/chat_command.go b/internal/chat_command.go index baf752f..4472ef6 100644 --- a/internal/chat_command.go +++ b/internal/chat_command.go @@ -82,14 +82,14 @@ func (m *Manager) ProcessSubCommand(command string) { case prefixMatch(commandPrefix, "/clear"): m.Messages = []ChatMessage{} - system.TmuxClearPane(m.PaneId) + _ = system.TmuxClearPane(m.PaneId) return case prefixMatch(commandPrefix, "/reset"): m.Status = "" m.Messages = []ChatMessage{} - system.TmuxClearPane(m.PaneId) - system.TmuxClearPane(m.ExecPane.Id) + _ = system.TmuxClearPane(m.PaneId) + _ = system.TmuxClearPane(m.ExecPane.Id) return case prefixMatch(commandPrefix, "/exit"): diff --git a/internal/config_helpers.go b/internal/config_helpers.go index 747d848..f0aa5d2 100644 --- a/internal/config_helpers.go +++ b/internal/config_helpers.go @@ -117,7 +117,7 @@ func formatConfigValue(sb *strings.Builder, prefix string, val reflect.Value, ov // Handle nested structs if field.Kind() == reflect.Struct { - sb.WriteString(fmt.Sprintf("%s%s:\n", indentStr, tag)) + _, _ = fmt.Fprintf(sb, "%s%s:\n", indentStr, tag) formatConfigValue(sb, key, field, overrides, indent+1) continue } @@ -144,9 +144,9 @@ func formatConfigValue(sb *strings.Builder, prefix string, val reflect.Value, ov // Check if there's a session override for this key if override, exists := overrides[key]; exists { - sb.WriteString(fmt.Sprintf("%s%s: %v", indentStr, tag, override)) + _, _ = fmt.Fprintf(sb, "%s%s: %v", indentStr, tag, override) } else { - sb.WriteString(fmt.Sprintf("%s%s: %s", indentStr, tag, valueStr)) + _, _ = fmt.Fprintf(sb, "%s%s: %s", indentStr, tag, valueStr) } sb.WriteString("\n") diff --git a/internal/confirm.go b/internal/confirm.go index 6c95046..6c94e52 100644 --- a/internal/confirm.go +++ b/internal/confirm.go @@ -36,7 +36,7 @@ func (m *Manager) confirmedToExecFn(command string, prompt string, edit bool) (b fmt.Printf("Error initializing readline: %v\n", err) return false, "" } - defer rl.Close() + defer func() { _ = rl.Close() }() confirmInput, err := rl.Readline() if err != nil { @@ -71,7 +71,7 @@ func (m *Manager) confirmedToExecFn(command string, prompt string, edit bool) (b fmt.Printf("Error initializing readline for edit: %v\n", editErr) return false, "" } - defer editRl.Close() + defer func() { _ = editRl.Close() }() // Use ReadlineWithDefault to prefill the command editedCommand, editErr := editRl.ReadlineWithDefault(command) diff --git a/internal/countdown.go b/internal/countdown.go index d638b97..b4c91e6 100644 --- a/internal/countdown.go +++ b/internal/countdown.go @@ -19,7 +19,7 @@ func (m *Manager) Countdown(seconds int) { fmt.Println("Error opening keyboard:", err) return } - defer keyboard.Close() + defer func() { _ = keyboard.Close() }() // Create a channel for keyboard events keyChan := make(chan keyboard.Key, 10) @@ -52,7 +52,6 @@ func (m *Manager) Countdown(seconds int) { // Just continue execution without exiting the function remaining = 0 // Set remaining to 0 to end the countdown loop renderCountdown(remaining, seconds, paused, highlightColor, dimColor, pauseColor) - break case keyboard.KeyCtrlC: // Ctrl+C m.Status = "" m.WatchMode = false diff --git a/internal/exec_pane.go b/internal/exec_pane.go index bb2bb67..b1e6c00 100644 --- a/internal/exec_pane.go +++ b/internal/exec_pane.go @@ -28,7 +28,7 @@ func (m *Manager) GetAvailablePane() system.TmuxPaneDetails { func (m *Manager) InitExecPane() { availablePane := m.GetAvailablePane() if availablePane.Id == "" { - system.TmuxCreateNewPane(m.PaneId) + _, _ = system.TmuxCreateNewPane(m.PaneId) availablePane = m.GetAvailablePane() } m.ExecPane = &availablePane @@ -55,12 +55,12 @@ func (m *Manager) PrepareExecPane() { return } - system.TmuxSendCommandToPane(m.ExecPane.Id, ps1Command, true) - system.TmuxSendCommandToPane(m.ExecPane.Id, "C-l", false) + _ = system.TmuxSendCommandToPane(m.ExecPane.Id, ps1Command, true) + _ = system.TmuxSendCommandToPane(m.ExecPane.Id, "C-l", false) } func (m *Manager) ExecWaitCapture(command string) (CommandExecHistory, error) { - system.TmuxSendCommandToPane(m.ExecPane.Id, command, true) + _ = system.TmuxSendCommandToPane(m.ExecPane.Id, command, true) m.ExecPane.Refresh(m.GetMaxCaptureLines()) m.Println("") @@ -100,7 +100,7 @@ func (m *Manager) parseExecPaneCommandHistory() { line := scanner.Text() match := promptRegex.FindStringSubmatch(line) - if match != nil && len(match) >= 2 { // We need at least the status code match[1] + if len(match) >= 2 { // We need at least the status code match[1] // --- Found a prompt line --- // This prompt line *terminates* the previous command block // and provides its status code. It might also start a new command block. @@ -132,10 +132,6 @@ func (m *Manager) parseExecPaneCommandHistory() { // Reset for the next block outputBuilder.Reset() currentCommand = nil // Mark as no active command temporarily - } else { - // Optional: Handle status code on the very first prompt if needed. - // Currently, the status on the first prompt is ignored as there's - // no *previous* command within the parsed text to assign it to. } // 2. If this prompt line ALSO contains a command, start the NEW block @@ -145,10 +141,6 @@ func (m *Manager) parseExecPaneCommandHistory() { Code: -1, // Default/Unknown: Status code is determined by the *next* prompt // Output will be collected in outputBuilder starting from the next line } - } else { - // This prompt line only indicates the end status of the previous command - // (like the final "[i] [~/r/tmuxai][16:56][2]ยป" line). - // No new command starts here, so currentCommand remains nil. } } else { diff --git a/internal/manager.go b/internal/manager.go index db6bea8..2ebc27b 100644 --- a/internal/manager.go +++ b/internal/manager.go @@ -64,10 +64,10 @@ func NewManager(cfg *config.Config) (*Manager, error) { } args := strings.Join(os.Args[1:], " ") - system.TmuxSendCommandToPane(paneId, "tmuxai "+args, true) + _ = system.TmuxSendCommandToPane(paneId, "tmuxai "+args, true) // shell initialization may take some time time.Sleep(1 * time.Second) - system.TmuxSendCommandToPane(paneId, "Enter", false) + _ = system.TmuxSendCommandToPane(paneId, "Enter", false) err = system.TmuxAttachSession(paneId) if err != nil { return nil, fmt.Errorf("system.TmuxAttachSession failed: %w", err) diff --git a/internal/process_message.go b/internal/process_message.go index d80c65a..c66e1c8 100644 --- a/internal/process_message.go +++ b/internal/process_message.go @@ -147,9 +147,9 @@ func (m *Manager) ProcessUserMessage(ctx context.Context, message string) bool { if isSafe { m.Println("Executing command: " + command) if m.ExecPane.IsPrepared { - m.ExecWaitCapture(command) + _, _ = m.ExecWaitCapture(command) } else { - system.TmuxSendCommandToPane(m.ExecPane.Id, command, true) + _ = system.TmuxSendCommandToPane(m.ExecPane.Id, command, true) time.Sleep(1 * time.Second) } } else { @@ -180,7 +180,7 @@ func (m *Manager) ProcessUserMessage(ctx context.Context, message string) bool { } // Get confirmation if required - allConfirmed := true + var allConfirmed bool if m.GetSendKeysConfirm() { allConfirmed, _ = m.confirmedToExec("keys shown above", confirmMessage, true) if !allConfirmed { @@ -192,7 +192,7 @@ func (m *Manager) ProcessUserMessage(ctx context.Context, message string) bool { // Send each key with delay for _, sendKey := range r.SendKeys { m.Println("Sending keys: " + sendKey) - system.TmuxSendCommandToPane(m.ExecPane.Id, sendKey, false) + _ = system.TmuxSendCommandToPane(m.ExecPane.Id, sendKey, false) time.Sleep(1 * time.Second) } } @@ -222,7 +222,7 @@ func (m *Manager) ProcessUserMessage(ctx context.Context, message string) bool { if isSafe { m.Println("Pasting...") - system.TmuxSendCommandToPane(m.ExecPane.Id, r.PasteMultilineContent, true) + _ = system.TmuxSendCommandToPane(m.ExecPane.Id, r.PasteMultilineContent, true) time.Sleep(1 * time.Second) } else { m.Status = "" diff --git a/internal/process_message_test.go b/internal/process_message_test.go index 780a9e6..85dd752 100644 --- a/internal/process_message_test.go +++ b/internal/process_message_test.go @@ -31,12 +31,12 @@ func (m *MockAiClient) ChatCompletion(ctx context.Context, messages []Message, m return args.String(0), args.Error(1) } -// Test: ProcessUserMessage with empty status returns false immediately +// Test: Pressing ctrl+c should cancel message processing func TestProcessUserMessage_EmptyStatus(t *testing.T) { cfg := &config.Config{ Debug: false, } - + manager := &Manager{ Config: cfg, Status: "", // Empty status @@ -47,26 +47,26 @@ func TestProcessUserMessage_EmptyStatus(t *testing.T) { manager.confirmedToExec = func(command string, prompt string, edit bool) (bool, string) { return true, command } - + manager.getTmuxPanesInXml = func(config *config.Config) string { return "mock pane content" } result := manager.ProcessUserMessage(context.Background(), "test message") - + assert.False(t, result, "ProcessUserMessage should return false when status is empty") } -// Test: ProcessUserMessage with context squash requirement +// Test: Context squash should trigger when max context size is exceeded func TestProcessUserMessage_ContextSquash(t *testing.T) { cfg := &config.Config{ - Debug: false, + Debug: false, MaxContextSize: 100, // Very small to trigger squash OpenRouter: config.OpenRouterConfig{ Model: "test-model", }, } - + manager := &Manager{ Config: cfg, Status: "running", @@ -82,7 +82,7 @@ func TestProcessUserMessage_ContextSquash(t *testing.T) { manager.confirmedToExec = func(command string, prompt string, edit bool) (bool, string) { return true, command } - + manager.getTmuxPanesInXml = func(config *config.Config) string { return "mock pane content" } @@ -99,7 +99,7 @@ func TestProcessUserMessage_ContextSquash(t *testing.T) { assert.True(t, manager.needSquash(), "Should need squash with many messages") } -// Test: AI guidelines validation with multiple boolean flags should fail +// Test: AI guidelines validation should fail when multiple boolean flags are set func TestProcessUserMessage_AIGuidelinesValidation(t *testing.T) { manager := &Manager{ WatchMode: false, @@ -143,7 +143,7 @@ func TestProcessUserMessage_AIGuidelinesValidation(t *testing.T) { assert.True(t, valid3, "Should pass validation with correct format") } -// Test: ProcessUserMessage with WaitingForUserResponse sets status correctly +// Test: If AI requests user input, should set status to waiting func TestProcessUserMessage_WaitingForUserResponse(t *testing.T) { manager := &Manager{ Status: "running", @@ -162,19 +162,19 @@ func TestProcessUserMessage_WaitingForUserResponse(t *testing.T) { } assert.Equal(t, "waiting", manager.Status, "Status should be set to 'waiting' when WaitingForUserResponse is true") - + // Test that aiFollowedGuidelines accepts this valid response _, valid := manager.aiFollowedGuidelines(response) assert.True(t, valid, "WaitingForUserResponse should be valid according to guidelines") } -// Test: ExecCommand processing with confirmation +// Test: If AI requests to execute a command, should confirm and send it to the exec pane func TestProcessUserMessage_ExecCommandWithConfirmation(t *testing.T) { cfg := &config.Config{ - Debug: false, + Debug: false, ExecConfirm: true, // Require confirmation for exec commands } - + manager := &Manager{ Config: cfg, Status: "running", @@ -189,7 +189,7 @@ func TestProcessUserMessage_ExecCommandWithConfirmation(t *testing.T) { // Track if confirmation was called and approved confirmationCalled := false commandExecuted := "" - + manager.confirmedToExec = func(command string, prompt string, edit bool) (bool, string) { confirmationCalled = true assert.Equal(t, "echo hello", command, "Should ask confirmation for the right command") @@ -200,7 +200,7 @@ func TestProcessUserMessage_ExecCommandWithConfirmation(t *testing.T) { // Mock tmux command execution by capturing what would be sent originalTmuxSend := system.TmuxSendCommandToPane defer func() { system.TmuxSendCommandToPane = originalTmuxSend }() - + system.TmuxSendCommandToPane = func(paneId string, command string, enter bool) error { commandExecuted = command assert.Equal(t, "test-pane", paneId, "Should send command to correct pane") @@ -227,7 +227,7 @@ func TestProcessUserMessage_ExecCommandWithConfirmation(t *testing.T) { if manager.ExecPane.IsPrepared { // Would call m.ExecWaitCapture(command) } else { - system.TmuxSendCommandToPane(manager.ExecPane.Id, command, true) + _ = system.TmuxSendCommandToPane(manager.ExecPane.Id, command, true) } } } @@ -236,13 +236,13 @@ func TestProcessUserMessage_ExecCommandWithConfirmation(t *testing.T) { assert.Equal(t, "echo hello", commandExecuted, "Command should have been executed") } -// Test: ExecCommand rejection clears status and returns false +// Test: If AI requests to execute a command, should reject it if confirmation fails func TestProcessUserMessage_ExecCommandRejection(t *testing.T) { cfg := &config.Config{ - Debug: false, + Debug: false, ExecConfirm: true, // Require confirmation for exec commands } - + manager := &Manager{ Config: cfg, Status: "running", @@ -257,17 +257,17 @@ func TestProcessUserMessage_ExecCommandRejection(t *testing.T) { // Track if confirmation was called confirmationCalled := false commandExecuted := false - + manager.confirmedToExec = func(command string, prompt string, edit bool) (bool, string) { confirmationCalled = true - assert.Equal(t, "rm -rf /", command, "Should ask confirmation for the dangerous command") + assert.Equal(t, "echo danger", command, "Should ask confirmation for the dangerous command") return false, command // User rejects the command } // Mock tmux command execution to track if it was called originalTmuxSend := system.TmuxSendCommandToPane defer func() { system.TmuxSendCommandToPane = originalTmuxSend }() - + system.TmuxSendCommandToPane = func(paneId string, command string, enter bool) error { commandExecuted = true return nil @@ -275,7 +275,7 @@ func TestProcessUserMessage_ExecCommandRejection(t *testing.T) { // Test the ExecCommand processing logic that should result in rejection response := AIResponse{ - ExecCommand: []string{"rm -rf /"}, + ExecCommand: []string{"echo danger"}, } // Simulate the ExecCommand processing loop from ProcessUserMessage @@ -292,7 +292,7 @@ func TestProcessUserMessage_ExecCommandRejection(t *testing.T) { if manager.ExecPane.IsPrepared { // Would call m.ExecWaitCapture(command) } else { - system.TmuxSendCommandToPane(manager.ExecPane.Id, command, true) + _ = system.TmuxSendCommandToPane(manager.ExecPane.Id, command, true) } } else { manager.Status = "" @@ -307,7 +307,7 @@ func TestProcessUserMessage_ExecCommandRejection(t *testing.T) { assert.Equal(t, "", manager.Status, "Status should be empty after rejection") } -// Test: RequestAccomplished clears status and returns true +// Test: If AI finishes the task, should clear status and return true func TestProcessUserMessage_RequestAccomplished(t *testing.T) { manager := &Manager{ Status: "running", @@ -329,19 +329,19 @@ func TestProcessUserMessage_RequestAccomplished(t *testing.T) { assert.True(t, result, "Should return true when RequestAccomplished") assert.Equal(t, "", manager.Status, "Status should be cleared when request is accomplished") - + // Verify this is a valid response according to guidelines _, valid := manager.aiFollowedGuidelines(response) assert.True(t, valid, "RequestAccomplished should be valid according to guidelines") } -// Test: SendKeys processing with confirmation +// Test: Sending multiple keys to the exec pane with confirmation func TestProcessUserMessage_SendKeysProcessing(t *testing.T) { cfg := &config.Config{ - Debug: false, + Debug: false, SendKeysConfirm: true, // Require confirmation for send keys } - + manager := &Manager{ Config: cfg, Status: "running", @@ -356,7 +356,7 @@ func TestProcessUserMessage_SendKeysProcessing(t *testing.T) { // Track confirmations and keys sent confirmationCalled := false keysSent := []string{} - + manager.confirmedToExec = func(command string, prompt string, edit bool) (bool, string) { confirmationCalled = true assert.Equal(t, "keys shown above", command, "Should show generic description for keys") @@ -367,7 +367,7 @@ func TestProcessUserMessage_SendKeysProcessing(t *testing.T) { // Mock tmux command execution to capture keys being sent originalTmuxSend := system.TmuxSendCommandToPane defer func() { system.TmuxSendCommandToPane = originalTmuxSend }() - + system.TmuxSendCommandToPane = func(paneId string, command string, enter bool) error { keysSent = append(keysSent, command) assert.Equal(t, "test-pane", paneId, "Should send keys to correct pane") @@ -398,14 +398,14 @@ func TestProcessUserMessage_SendKeysProcessing(t *testing.T) { if allConfirmed { // Send each key with delay (without the actual delay in test) for _, sendKey := range response.SendKeys { - system.TmuxSendCommandToPane(manager.ExecPane.Id, sendKey, false) + _ = system.TmuxSendCommandToPane(manager.ExecPane.Id, sendKey, false) } } } assert.True(t, confirmationCalled, "Confirmation should have been called") assert.Equal(t, []string{"ctrl+c", "ctrl+d", "exit"}, keysSent, "All keys should have been sent in order") - + // Verify this is a valid response according to guidelines _, valid := manager.aiFollowedGuidelines(response) assert.True(t, valid, "SendKeys should be valid according to guidelines") @@ -431,26 +431,26 @@ func TestProcessUserMessage_WatchModeNoComment(t *testing.T) { } assert.False(t, result, "Should return false for NoComment in watch mode") - + // Verify this is a valid response according to guidelines when in watch mode _, valid := manager.aiFollowedGuidelines(response) assert.True(t, valid, "NoComment should be valid according to guidelines in watch mode") - + // Test that NoComment is valid even outside watch mode according to current logic manager.WatchMode = false response2 := AIResponse{ NoComment: true, } - + // NoComment alone is actually valid even when not in watch mode (boolCount=1 satisfies count+boolCount > 0) _, valid2 := manager.aiFollowedGuidelines(response2) assert.True(t, valid2, "NoComment alone should be valid according to current guidelines logic") - + // Test truly invalid case: no boolean flags and no XML tags when not in watch mode response3 := AIResponse{ Message: "Just a message with nothing else", } - + _, valid3 := manager.aiFollowedGuidelines(response3) assert.False(t, valid3, "Empty response (no flags, no XML tags) should fail validation when not in watch mode") -} \ No newline at end of file +}