rdev/internal/handlers/agents_test.go
jordan bc47e426b0 feat: Add CI pipeline proxy, DNS alias management, and worker executor system
- Add ListPipelines/GetPipeline to CIProvider port with Woodpecker adapter
- Add DNS alias endpoints: GET/POST/DELETE /projects/{id}/domains
- Implement worker executor daemon, build executor, and git operations
- Add build service, worker service, and build audit tracking
- Add worker registry with PostgreSQL adapter and migration
- Add multi-provider code agent interface (Claude Code + OpenCode)
- Add create-and-build combo endpoint
- Update landing-page cookbook to reflect all gaps closed
- Fix tech debt: unified validation, auth scopes, error wrapping, slog patterns

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 21:05:28 -07:00

373 lines
10 KiB
Go

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)
}
}