package handlers import ( "bytes" "context" "encoding/json" "fmt" "net/http" "net/http/httptest" "testing" "github.com/go-chi/chi/v5" "github.com/orchard9/rdev/internal/domain" "github.com/orchard9/rdev/internal/port" ) // mockCodeAgent implements port.CodeAgent for testing. type mockCodeAgent struct { name string provider domain.AgentProvider available bool capabilities domain.AgentCapabilities } func (m *mockCodeAgent) Name() string { return m.name } func (m *mockCodeAgent) Provider() domain.AgentProvider { return m.provider } func (m *mockCodeAgent) Execute(ctx context.Context, req *domain.AgentRequest, handler domain.AgentEventHandler) (*domain.AgentResult, error) { return &domain.AgentResult{}, nil } func (m *mockCodeAgent) Cancel(ctx context.Context, sessionID string) error { return nil } func (m *mockCodeAgent) Capabilities() domain.AgentCapabilities { return m.capabilities } func (m *mockCodeAgent) Available(ctx context.Context) bool { return m.available } // mockAgentRegistry implements port.CodeAgentRegistry for testing. type mockAgentRegistry struct { agents map[domain.AgentProvider]*mockCodeAgent defaultAgent domain.AgentProvider setDefaultErr error } func newMockAgentRegistry() *mockAgentRegistry { return &mockAgentRegistry{ agents: make(map[domain.AgentProvider]*mockCodeAgent), } } func (m *mockAgentRegistry) Register(agent port.CodeAgent) { m.agents[agent.Provider()] = agent.(*mockCodeAgent) if m.defaultAgent == "" { m.defaultAgent = agent.Provider() } } // registerAgent is a helper for tests to directly add mockCodeAgents func (m *mockAgentRegistry) registerAgent(agent *mockCodeAgent) { m.agents[agent.provider] = agent if m.defaultAgent == "" { m.defaultAgent = agent.provider } } func (m *mockAgentRegistry) Get(provider domain.AgentProvider) port.CodeAgent { agent, ok := m.agents[provider] if !ok { return nil } return agent } func (m *mockAgentRegistry) Default() port.CodeAgent { return m.Get(m.defaultAgent) } func (m *mockAgentRegistry) DefaultProvider() domain.AgentProvider { return m.defaultAgent } func (m *mockAgentRegistry) SetDefault(provider domain.AgentProvider) error { if m.setDefaultErr != nil { return m.setDefaultErr } if _, ok := m.agents[provider]; !ok { return fmt.Errorf("agent provider %q is not registered", provider) } m.defaultAgent = provider return nil } func (m *mockAgentRegistry) Available() []domain.AgentProvider { providers := make([]domain.AgentProvider, 0, len(m.agents)) for p := range m.agents { providers = append(providers, p) } return providers } func (m *mockAgentRegistry) AvailableAgents(ctx context.Context) []port.CodeAgent { var available []port.CodeAgent for _, agent := range m.agents { if agent.available { available = append(available, agent) } } return available } func (m *mockAgentRegistry) Count() int { return len(m.agents) } func TestAgentsHandler_List(t *testing.T) { registry := newMockAgentRegistry() registry.registerAgent(&mockCodeAgent{ name: "Claude Code", provider: domain.AgentProviderClaudeCode, available: true, capabilities: domain.AgentCapabilities{ Provider: domain.AgentProviderClaudeCode, SupportsSessionContinuation: true, SupportedModels: []string{"claude-sonnet-4-20250514"}, DefaultModel: "claude-sonnet-4-20250514", }, }) registry.registerAgent(&mockCodeAgent{ name: "OpenCode", provider: domain.AgentProviderOpenCode, available: false, capabilities: domain.AgentCapabilities{ Provider: domain.AgentProviderOpenCode, SupportsSessionContinuation: true, SupportsModelSelection: true, SupportedModels: []string{"gpt-4o", "claude-sonnet-4-20250514"}, DefaultModel: "claude-sonnet-4-20250514", }, }) handler := NewAgentsHandler(registry) req := httptest.NewRequest(http.MethodGet, "/agents", nil) w := httptest.NewRecorder() handler.List(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } // Unwrap from api.Response respBody := w.Body.Bytes() var apiResp struct { Data ListAgentsResponse `json:"data"` } if err := json.Unmarshal(respBody, &apiResp); err != nil { t.Fatalf("failed to decode api response: %v", err) } if apiResp.Data.TotalAgents != 2 { t.Errorf("expected 2 agents, got %d", apiResp.Data.TotalAgents) } if apiResp.Data.AvailableCount != 1 { t.Errorf("expected 1 available agent, got %d", apiResp.Data.AvailableCount) } if apiResp.Data.DefaultAgent != string(domain.AgentProviderClaudeCode) { t.Errorf("expected default agent to be claude-code, got %s", apiResp.Data.DefaultAgent) } } func TestAgentsHandler_List_NoRegistry(t *testing.T) { handler := NewAgentsHandler(nil) req := httptest.NewRequest(http.MethodGet, "/agents", nil) w := httptest.NewRecorder() handler.List(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } } func TestAgentsHandler_GetCapabilities(t *testing.T) { registry := newMockAgentRegistry() registry.registerAgent(&mockCodeAgent{ name: "Claude Code", provider: domain.AgentProviderClaudeCode, capabilities: domain.AgentCapabilities{ Provider: domain.AgentProviderClaudeCode, SupportsSessionContinuation: true, SupportsStreaming: true, SupportedModels: []string{"claude-sonnet-4-20250514"}, DefaultModel: "claude-sonnet-4-20250514", MaxPromptLength: 100000, }, }) handler := NewAgentsHandler(registry) // Set up chi router for URL params r := chi.NewRouter() r.Get("/agents/{provider}", handler.GetCapabilities) req := httptest.NewRequest(http.MethodGet, "/agents/claudecode", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String()) } var apiResp struct { Data AgentCapabilitiesDTO `json:"data"` } if err := json.Unmarshal(w.Body.Bytes(), &apiResp); err != nil { t.Fatalf("failed to decode response: %v", err) } if apiResp.Data.Provider != string(domain.AgentProviderClaudeCode) { t.Errorf("expected provider claudecode, got %s", apiResp.Data.Provider) } if !apiResp.Data.SupportsSessionContinuation { t.Error("expected session continuation support") } if !apiResp.Data.SupportsStreaming { t.Error("expected streaming support") } } func TestAgentsHandler_GetCapabilities_NotFound(t *testing.T) { registry := newMockAgentRegistry() handler := NewAgentsHandler(registry) r := chi.NewRouter() r.Get("/agents/{provider}", handler.GetCapabilities) req := httptest.NewRequest(http.MethodGet, "/agents/unknown", nil) w := httptest.NewRecorder() r.ServeHTTP(w, req) if w.Code != http.StatusNotFound { t.Errorf("expected status 404, got %d", w.Code) } } func TestAgentsHandler_SetDefault(t *testing.T) { registry := newMockAgentRegistry() registry.registerAgent(&mockCodeAgent{ name: "Claude Code", provider: domain.AgentProviderClaudeCode, }) registry.registerAgent(&mockCodeAgent{ name: "OpenCode", provider: domain.AgentProviderOpenCode, }) handler := NewAgentsHandler(registry) body := SetDefaultRequest{Provider: string(domain.AgentProviderOpenCode)} bodyBytes, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodPost, "/agents/default", bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.SetDefault(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d: %s", w.Code, w.Body.String()) } // Verify default was changed if registry.defaultAgent != domain.AgentProviderOpenCode { t.Errorf("expected default to be opencode, got %s", registry.defaultAgent) } } func TestAgentsHandler_SetDefault_InvalidProvider(t *testing.T) { registry := newMockAgentRegistry() registry.registerAgent(&mockCodeAgent{ name: "Claude Code", provider: domain.AgentProviderClaudeCode, }) handler := NewAgentsHandler(registry) body := SetDefaultRequest{Provider: "unknown"} bodyBytes, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodPost, "/agents/default", bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.SetDefault(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d", w.Code) } } func TestAgentsHandler_SetDefault_EmptyProvider(t *testing.T) { registry := newMockAgentRegistry() handler := NewAgentsHandler(registry) body := SetDefaultRequest{Provider: ""} bodyBytes, _ := json.Marshal(body) req := httptest.NewRequest(http.MethodPost, "/agents/default", bytes.NewReader(bodyBytes)) req.Header.Set("Content-Type", "application/json") w := httptest.NewRecorder() handler.SetDefault(w, req) if w.Code != http.StatusBadRequest { t.Errorf("expected status 400, got %d", w.Code) } } func TestAgentsHandler_Health(t *testing.T) { registry := newMockAgentRegistry() registry.registerAgent(&mockCodeAgent{ name: "Claude Code", provider: domain.AgentProviderClaudeCode, available: true, }) registry.registerAgent(&mockCodeAgent{ name: "OpenCode", provider: domain.AgentProviderOpenCode, available: false, }) handler := NewAgentsHandler(registry) req := httptest.NewRequest(http.MethodGet, "/agents/health", nil) w := httptest.NewRecorder() handler.Health(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } var apiResp struct { Data AgentHealthResponse `json:"data"` } if err := json.Unmarshal(w.Body.Bytes(), &apiResp); err != nil { t.Fatalf("failed to decode response: %v", err) } if apiResp.Data.TotalCount != 2 { t.Errorf("expected 2 agents, got %d", apiResp.Data.TotalCount) } if apiResp.Data.HealthyCount != 1 { t.Errorf("expected 1 healthy agent, got %d", apiResp.Data.HealthyCount) } if !apiResp.Data.DefaultHealth { t.Error("expected default agent to be healthy") } } func TestAgentsHandler_Health_NoRegistry(t *testing.T) { handler := NewAgentsHandler(nil) req := httptest.NewRequest(http.MethodGet, "/agents/health", nil) w := httptest.NewRecorder() handler.Health(w, req) if w.Code != http.StatusOK { t.Errorf("expected status 200, got %d", w.Code) } }