package claudecode import ( "context" "strings" "testing" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/port" ) // Ensure Adapter implements port.CodeAgent at compile time. var _ port.CodeAgent = (*Adapter)(nil) func TestAdapter_Name(t *testing.T) { adapter := NewAdapter("test-ns") if name := adapter.Name(); name != "Claude Code" { t.Errorf("expected name 'Claude Code', got %q", name) } } func TestAdapter_Provider(t *testing.T) { adapter := NewAdapter("test-ns") if p := adapter.Provider(); p != domain.AgentProviderClaudeCode { t.Errorf("expected provider 'claudecode', got %q", p) } } func TestAdapter_Capabilities(t *testing.T) { adapter := NewAdapter("test-ns") caps := adapter.Capabilities() if caps.Provider != domain.AgentProviderClaudeCode { t.Errorf("expected provider claudecode") } if !caps.SupportsSessionContinuation { t.Error("expected session continuation support") } if caps.SupportsModelSelection { t.Error("expected no model selection support") } if !caps.SupportsToolControl { t.Error("expected tool control support") } if !caps.SupportsStreaming { t.Error("expected streaming support") } if len(caps.SupportedModels) == 0 { t.Error("expected at least one supported model") } } func TestAdapter_buildCommandArgs_Basic(t *testing.T) { adapter := NewAdapter("rdev") req := &domain.AgentRequest{ Prompt: "Hello, Claude", } args := adapter.buildCommandArgs("rdev", "pod-123", req) // Verify essential args are present argsStr := strings.Join(args, " ") if !strings.Contains(argsStr, "exec -n rdev pod-123") { t.Error("expected kubectl exec command") } if !strings.Contains(argsStr, "claude") { t.Error("expected claude command") } if !strings.Contains(argsStr, "-p") { t.Error("expected -p flag") } if !strings.Contains(argsStr, "--output-format stream-json") { t.Error("expected stream-json output format") } if !strings.Contains(argsStr, "--dangerously-skip-permissions") { t.Error("expected permission skip flag") } if !strings.Contains(argsStr, "Hello, Claude") { t.Error("expected prompt in args") } } func TestAdapter_buildCommandArgs_WithSessionResume(t *testing.T) { adapter := NewAdapter("rdev") req := &domain.AgentRequest{ Prompt: "Continue working", SessionID: "session-abc", } args := adapter.buildCommandArgs("rdev", "pod-123", req) argsStr := strings.Join(args, " ") if !strings.Contains(argsStr, "--resume session-abc") { t.Error("expected --resume flag with session ID") } } func TestAdapter_buildCommandArgs_WithAllowedTools(t *testing.T) { adapter := NewAdapter("rdev") req := &domain.AgentRequest{ Prompt: "Run tests", AllowedTools: []string{"Bash", "Read", "Edit"}, } args := adapter.buildCommandArgs("rdev", "pod-123", req) argsStr := strings.Join(args, " ") if !strings.Contains(argsStr, "--allowedTools Bash") { t.Error("expected --allowedTools Bash") } if !strings.Contains(argsStr, "--allowedTools Read") { t.Error("expected --allowedTools Read") } if !strings.Contains(argsStr, "--allowedTools Edit") { t.Error("expected --allowedTools Edit") } } func TestAdapter_buildCommandArgs_WithWorkingDir(t *testing.T) { adapter := NewAdapter("rdev") req := &domain.AgentRequest{ Prompt: "List files", WorkingDir: "/workspace/subdir", } args := adapter.buildCommandArgs("rdev", "pod-123", req) argsStr := strings.Join(args, " ") if !strings.Contains(argsStr, "--add-dir /workspace/subdir") { t.Error("expected --add-dir for non-default working directory") } } func TestAdapter_buildCommandArgs_DefaultWorkingDir(t *testing.T) { adapter := NewAdapter("rdev") req := &domain.AgentRequest{ Prompt: "List files", WorkingDir: "/workspace", // default } args := adapter.buildCommandArgs("rdev", "pod-123", req) argsStr := strings.Join(args, " ") // Should NOT have --add-dir for default workspace if strings.Contains(argsStr, "--add-dir") { t.Error("expected no --add-dir for default workspace") } } func TestAdapter_Execute_MissingPrompt(t *testing.T) { adapter := NewAdapter("rdev") req := &domain.AgentRequest{ Prompt: "", // missing Metadata: map[string]string{ "pod_name": "test-pod", }, } _, err := adapter.Execute(context.Background(), req, func(e domain.AgentEvent) {}) if err == nil { t.Error("expected error for missing prompt") } if !strings.Contains(err.Error(), "prompt is required") { t.Errorf("expected 'prompt is required' error, got: %v", err) } } func TestAdapter_Execute_MissingPodName(t *testing.T) { adapter := NewAdapter("rdev") req := &domain.AgentRequest{ Prompt: "Hello", Metadata: map[string]string{}, // no pod_name } result, _ := adapter.Execute(context.Background(), req, func(e domain.AgentEvent) {}) if result.Error == nil { t.Error("expected error for missing pod_name") } if !strings.Contains(result.Error.Error(), "pod_name") { t.Errorf("expected pod_name error, got: %v", result.Error) } } func TestAdapter_Cancel(t *testing.T) { adapter := NewAdapter("rdev") // Cancel non-existent session should not error err := adapter.Cancel(context.Background(), "nonexistent") if err != nil { t.Errorf("expected no error for non-existent session, got: %v", err) } } func TestAdapter_parseStreamOutput(t *testing.T) { adapter := NewAdapter("rdev") input := strings.NewReader(`{"type":"init","session_id":"test-123"} {"type":"message","role":"assistant","content":[{"type":"text","text":"Hello!"}]} {"type":"result","status":"success","duration_ms":100} `) var events []domain.AgentEvent handler := func(e domain.AgentEvent) { events = append(events, e) } var output strings.Builder resultMsg, err := adapter.parseStreamOutput(input, handler, &output) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(events) != 3 { t.Errorf("expected 3 events, got %d", len(events)) } // Verify event types if events[0].Type != domain.AgentEventOutput { t.Errorf("expected first event to be output (init)") } if events[1].Type != domain.AgentEventOutput { t.Errorf("expected second event to be output (message)") } if events[2].Type != domain.AgentEventComplete { t.Errorf("expected third event to be complete (result)") } // Verify result message if resultMsg == nil { t.Fatal("expected result message") } if !resultMsg.IsSuccess() { t.Error("expected success result") } // Verify output was collected if !strings.Contains(output.String(), "Hello!") { t.Error("expected output to contain assistant message") } } func TestAdapter_parseStreamOutput_PlainText(t *testing.T) { adapter := NewAdapter("rdev") // Non-JSON lines should be treated as plain output input := strings.NewReader(`Not JSON output Another plain line {"type":"result","status":"success"} `) var events []domain.AgentEvent handler := func(e domain.AgentEvent) { events = append(events, e) } var output strings.Builder _, err := adapter.parseStreamOutput(input, handler, &output) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(events) != 3 { t.Errorf("expected 3 events, got %d", len(events)) } // First two should be plain output events if events[0].Content != "Not JSON output" { t.Errorf("expected plain text content") } if events[0].Stream != "stdout" { t.Errorf("expected stdout stream for plain text") } } func TestGenerateSessionID(t *testing.T) { id1 := generateSessionID() id2 := generateSessionID() if id1 == id2 { t.Error("expected unique session IDs") } if !strings.HasPrefix(id1, "claude-") { t.Errorf("expected session ID to start with 'claude-', got %q", id1) } }