package executor import ( "context" "os/exec" "strings" "sync" "testing" "time" ) func TestNew(t *testing.T) { e := New("test-namespace") if e.namespace != "test-namespace" { t.Errorf("namespace = %q, want %q", e.namespace, "test-namespace") } } func TestCommand_Types(t *testing.T) { tests := []struct { cmdType CommandType want string }{ {CommandTypeClaude, "claude"}, {CommandTypeShell, "shell"}, {CommandTypeGit, "git"}, } for _, tt := range tests { t.Run(string(tt.cmdType), func(t *testing.T) { if string(tt.cmdType) != tt.want { t.Errorf("CommandType = %q, want %q", tt.cmdType, tt.want) } }) } } func TestExecutor_buildArgs(t *testing.T) { e := New("apps") tests := []struct { name string cmd *Command wantArgs []string wantErr bool }{ { name: "claude command", cmd: &Command{ ID: "cmd-1", PodName: "claudebox-test", Type: CommandTypeClaude, Args: []string{"Write a hello world"}, }, wantArgs: []string{ "exec", "-n", "apps", "claudebox-test", "--", "claude", "Write a hello world", }, }, { name: "shell command", cmd: &Command{ ID: "cmd-2", PodName: "claudebox-test", Type: CommandTypeShell, Args: []string{"ls -la /workspace"}, }, wantArgs: []string{ "exec", "-n", "apps", "claudebox-test", "--", "bash", "-c", "ls -la /workspace", }, }, { name: "git command", cmd: &Command{ ID: "cmd-3", PodName: "claudebox-test", Type: CommandTypeGit, Args: []string{"status"}, }, wantArgs: []string{ "exec", "-n", "apps", "claudebox-test", "--", "git", "-C", "/workspace", "status", }, }, { name: "git command with multiple args", cmd: &Command{ ID: "cmd-4", PodName: "claudebox-test", Type: CommandTypeGit, Args: []string{"commit", "-m", "test message"}, }, wantArgs: []string{ "exec", "-n", "apps", "claudebox-test", "--", "git", "-C", "/workspace", "commit", "-m", "test message", }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // We can't directly test buildArgs since it's internal to Exec, // but we can verify the command construction by checking what would be built var args []string switch tt.cmd.Type { case CommandTypeClaude: args = []string{ "exec", "-n", e.namespace, tt.cmd.PodName, "--", "claude", tt.cmd.Args[0], } case CommandTypeShell: args = []string{ "exec", "-n", e.namespace, tt.cmd.PodName, "--", "bash", "-c", tt.cmd.Args[0], } case CommandTypeGit: args = append([]string{ "exec", "-n", e.namespace, tt.cmd.PodName, "--", "git", "-C", "/workspace", }, tt.cmd.Args...) } if len(args) != len(tt.wantArgs) { t.Errorf("args length = %d, want %d", len(args), len(tt.wantArgs)) return } for i, arg := range args { if arg != tt.wantArgs[i] { t.Errorf("args[%d] = %q, want %q", i, arg, tt.wantArgs[i]) } } }) } } func TestExecutor_Exec_UnknownType(t *testing.T) { e := New("test") var output []string handler := func(stream, line string) { output = append(output, line) } result := e.Exec(context.Background(), &Command{ Type: CommandType("unknown"), }, handler) if result.ExitCode != 1 { t.Errorf("ExitCode = %d, want 1", result.ExitCode) } if result.Error == nil { t.Error("Error should not be nil for unknown command type") } if !strings.Contains(result.Error.Error(), "unknown command type") { t.Errorf("Error = %v, want to contain 'unknown command type'", result.Error) } } func TestExecutor_Exec_ContextCancellation(t *testing.T) { // Skip if kubectl is not available if _, err := exec.LookPath("kubectl"); err != nil { t.Skip("kubectl not available") } e := New("default") ctx, cancel := context.WithCancel(context.Background()) var wg sync.WaitGroup var result Result wg.Add(1) go func() { defer wg.Done() result = e.Exec(ctx, &Command{ ID: "test-cancel", PodName: "nonexistent-pod", Type: CommandTypeShell, Args: []string{"sleep 10"}, }, func(stream, line string) {}) }() // Cancel immediately cancel() wg.Wait() // The command should either fail due to context cancellation or pod not found // Either way it shouldn't hang if result.ExitCode == 0 && result.Error == nil { t.Error("Expected command to fail due to cancellation or pod not found") } } func TestExecutor_CheckConnection(t *testing.T) { // This test requires kubectl to be configured if _, err := exec.LookPath("kubectl"); err != nil { t.Skip("kubectl not available") } e := New("default") ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // CheckConnection will succeed if kubectl is configured, fail otherwise // We just verify it doesn't panic _ = e.CheckConnection(ctx) } func TestExecutor_PodExists(t *testing.T) { // Skip if kubectl is not available if _, err := exec.LookPath("kubectl"); err != nil { t.Skip("kubectl not available") } e := New("default") ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() // Check for a pod that definitely doesn't exist exists, err := e.PodExists(ctx, "definitely-nonexistent-pod-12345") // Should return false without error (or skip if cluster not available) if err != nil { t.Skipf("cluster not available: %v", err) } if exists { t.Error("Expected pod to not exist") } } // TestStreamOutput tests the streamOutput function behavior func TestStreamOutput(t *testing.T) { tests := []struct { name string input string want []string }{ { name: "single line", input: "hello world", want: []string{"hello world"}, }, { name: "multiple lines", input: "line1\nline2\nline3", want: []string{"line1", "line2", "line3"}, }, { name: "empty input", input: "", want: nil, }, { name: "trailing newline", input: "hello\n", want: []string{"hello"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { var got []string handler := func(stream, line string) { if stream != "stdout" { t.Errorf("stream = %q, want %q", stream, "stdout") } got = append(got, line) } r := strings.NewReader(tt.input) streamOutput(r, "stdout", handler) if len(got) != len(tt.want) { t.Errorf("got %d lines, want %d", len(got), len(tt.want)) return } for i, line := range got { if line != tt.want[i] { t.Errorf("line[%d] = %q, want %q", i, line, tt.want[i]) } } }) } } // TestResult verifies Result struct behavior func TestResult(t *testing.T) { t.Run("successful result", func(t *testing.T) { r := Result{ ExitCode: 0, DurationMs: 1500, Error: nil, } if r.ExitCode != 0 { t.Errorf("ExitCode = %d, want 0", r.ExitCode) } if r.DurationMs != 1500 { t.Errorf("DurationMs = %d, want 1500", r.DurationMs) } if r.Error != nil { t.Errorf("Error = %v, want nil", r.Error) } }) t.Run("failed result", func(t *testing.T) { r := Result{ ExitCode: 1, DurationMs: 500, Error: nil, } if r.ExitCode != 1 { t.Errorf("ExitCode = %d, want 1", r.ExitCode) } }) } // TestCommand verifies Command struct func TestCommand(t *testing.T) { now := time.Now() cmd := Command{ ID: "cmd-123", PodName: "test-pod", Type: CommandTypeClaude, Args: []string{"prompt here"}, StartedAt: now, } if cmd.ID != "cmd-123" { t.Errorf("ID = %q, want %q", cmd.ID, "cmd-123") } if cmd.PodName != "test-pod" { t.Errorf("PodName = %q, want %q", cmd.PodName, "test-pod") } if cmd.Type != CommandTypeClaude { t.Errorf("Type = %q, want %q", cmd.Type, CommandTypeClaude) } if len(cmd.Args) != 1 || cmd.Args[0] != "prompt here" { t.Errorf("Args = %v, want [\"prompt here\"]", cmd.Args) } if !cmd.StartedAt.Equal(now) { t.Errorf("StartedAt = %v, want %v", cmd.StartedAt, now) } }