rdev/internal/adapter/codeagent/registry_test.go
jordan 39df51defd feat: Add multi-provider code agent interface with Claude Code and OpenCode adapters
Implements weeks 1-4 of the multi-provider architecture:

Week 1 - Foundation:
- Add domain models (AgentProvider, AgentRequest, AgentEvent, AgentResult)
- Define CodeAgent port interface with Execute, Cancel, Capabilities
- Create thread-safe provider registry with first-registered default

Week 2 - Claude Code Adapter:
- Extract kubectl exec logic into CodeAgent implementation
- Parse stream-json output format (init, message, tool_use, result)
- Support session continuation via --resume flag

Week 3 - OpenCode Adapter:
- HTTP/SSE client for opencode serve API
- Session management (create, send message, abort)
- Event streaming with documented buffer rationale

Week 4 - Quality & Polish:
- Fix race condition in OpenCode Cancel method
- Add AgentRequest.Validate() with ErrPromptRequired, ErrInvalidTimeout
- Document DefaultAvailabilityTimeout constants
- Add HTTP error context for debugging

Also includes:
- Work queue system with PostgreSQL adapter
- Credential store for infrastructure secrets
- Project templates with Woodpecker CI integration
- Comprehensive test coverage

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-27 09:25:51 -07:00

262 lines
6.8 KiB
Go

package codeagent
import (
"context"
"testing"
"github.com/orchard9/rdev/internal/domain"
"github.com/orchard9/rdev/internal/port"
)
// mockAgent is a test implementation of port.CodeAgent.
type mockAgent struct {
provider domain.AgentProvider
name string
available bool
}
func (m *mockAgent) Name() string { return m.name }
func (m *mockAgent) Provider() domain.AgentProvider { return m.provider }
func (m *mockAgent) Available(ctx context.Context) bool { return m.available }
func (m *mockAgent) Execute(ctx context.Context, req *domain.AgentRequest, handler domain.AgentEventHandler) (*domain.AgentResult, error) {
return &domain.AgentResult{ExitCode: 0}, nil
}
func (m *mockAgent) Cancel(ctx context.Context, sessionID string) error {
return nil
}
func (m *mockAgent) Capabilities() domain.AgentCapabilities {
return domain.AgentCapabilities{Provider: m.provider}
}
var _ port.CodeAgent = (*mockAgent)(nil)
func TestRegistry_RegisterAndGet(t *testing.T) {
reg := NewRegistry()
agent := &mockAgent{
provider: domain.AgentProviderClaudeCode,
name: "Claude Code",
available: true,
}
reg.Register(agent)
got := reg.Get(domain.AgentProviderClaudeCode)
if got == nil {
t.Fatal("expected agent, got nil")
}
if got.Name() != "Claude Code" {
t.Errorf("expected name 'Claude Code', got %q", got.Name())
}
}
func TestRegistry_GetUnregistered(t *testing.T) {
reg := NewRegistry()
got := reg.Get(domain.AgentProviderOpenCode)
if got != nil {
t.Errorf("expected nil for unregistered provider, got %v", got)
}
}
func TestRegistry_DefaultFirstRegistered(t *testing.T) {
reg := NewRegistry()
claude := &mockAgent{provider: domain.AgentProviderClaudeCode, name: "Claude", available: true}
opencode := &mockAgent{provider: domain.AgentProviderOpenCode, name: "OpenCode", available: true}
reg.Register(claude)
reg.Register(opencode)
def := reg.Default()
if def == nil {
t.Fatal("expected default agent, got nil")
}
if def.Provider() != domain.AgentProviderClaudeCode {
t.Errorf("expected first registered (claudecode) as default, got %v", def.Provider())
}
}
func TestRegistry_SetDefault(t *testing.T) {
reg := NewRegistry()
claude := &mockAgent{provider: domain.AgentProviderClaudeCode, name: "Claude", available: true}
opencode := &mockAgent{provider: domain.AgentProviderOpenCode, name: "OpenCode", available: true}
reg.Register(claude)
reg.Register(opencode)
err := reg.SetDefault(domain.AgentProviderOpenCode)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
def := reg.Default()
if def.Provider() != domain.AgentProviderOpenCode {
t.Errorf("expected opencode as default, got %v", def.Provider())
}
}
func TestRegistry_SetDefaultUnregistered(t *testing.T) {
reg := NewRegistry()
claude := &mockAgent{provider: domain.AgentProviderClaudeCode, name: "Claude", available: true}
reg.Register(claude)
err := reg.SetDefault(domain.AgentProviderOpenCode)
if err == nil {
t.Error("expected error setting unregistered default")
}
}
func TestRegistry_Available(t *testing.T) {
reg := NewRegistry()
claude := &mockAgent{provider: domain.AgentProviderClaudeCode, name: "Claude", available: true}
opencode := &mockAgent{provider: domain.AgentProviderOpenCode, name: "OpenCode", available: true}
reg.Register(claude)
reg.Register(opencode)
providers := reg.Available()
if len(providers) != 2 {
t.Errorf("expected 2 providers, got %d", len(providers))
}
// Check both are present (order not guaranteed)
found := make(map[domain.AgentProvider]bool)
for _, p := range providers {
found[p] = true
}
if !found[domain.AgentProviderClaudeCode] || !found[domain.AgentProviderOpenCode] {
t.Errorf("expected both providers in list, got %v", providers)
}
}
func TestRegistry_AvailableAgents(t *testing.T) {
reg := NewRegistry()
claude := &mockAgent{provider: domain.AgentProviderClaudeCode, name: "Claude", available: true}
opencode := &mockAgent{provider: domain.AgentProviderOpenCode, name: "OpenCode", available: false}
reg.Register(claude)
reg.Register(opencode)
ctx := context.Background()
agents := reg.AvailableAgents(ctx)
if len(agents) != 1 {
t.Errorf("expected 1 available agent, got %d", len(agents))
}
if agents[0].Provider() != domain.AgentProviderClaudeCode {
t.Errorf("expected claude as available, got %v", agents[0].Provider())
}
}
func TestRegistry_DefaultEmpty(t *testing.T) {
reg := NewRegistry()
def := reg.Default()
if def != nil {
t.Errorf("expected nil default for empty registry, got %v", def)
}
}
func TestRegistry_Count(t *testing.T) {
reg := NewRegistry()
if reg.Count() != 0 {
t.Errorf("expected 0 count, got %d", reg.Count())
}
reg.Register(&mockAgent{provider: domain.AgentProviderClaudeCode, available: true})
if reg.Count() != 1 {
t.Errorf("expected 1 count, got %d", reg.Count())
}
reg.Register(&mockAgent{provider: domain.AgentProviderOpenCode, available: true})
if reg.Count() != 2 {
t.Errorf("expected 2 count, got %d", reg.Count())
}
}
func TestRegistry_DefaultProvider(t *testing.T) {
reg := NewRegistry()
// Empty registry
if p := reg.DefaultProvider(); p != "" {
t.Errorf("expected empty default provider, got %q", p)
}
reg.Register(&mockAgent{provider: domain.AgentProviderClaudeCode, available: true})
if p := reg.DefaultProvider(); p != domain.AgentProviderClaudeCode {
t.Errorf("expected claudecode, got %q", p)
}
}
func TestRegistry_ConcurrentAccess(t *testing.T) {
reg := NewRegistry()
// Pre-register one agent
reg.Register(&mockAgent{provider: domain.AgentProviderClaudeCode, available: true})
done := make(chan struct{})
const goroutines = 10
const iterations = 100
// Concurrent reads and writes
for i := 0; i < goroutines; i++ {
go func(id int) {
defer func() { done <- struct{}{} }()
for j := 0; j < iterations; j++ {
// Mix of operations
switch j % 5 {
case 0:
reg.Get(domain.AgentProviderClaudeCode)
case 1:
reg.Default()
case 2:
reg.Available()
case 3:
reg.Count()
case 4:
reg.AvailableAgents(context.Background())
}
}
}(i)
}
// Wait for all goroutines
for i := 0; i < goroutines; i++ {
<-done
}
// Verify state is consistent
if reg.Count() != 1 {
t.Errorf("expected count 1 after concurrent access, got %d", reg.Count())
}
}
func TestRegistry_ReRegisterOverwrites(t *testing.T) {
reg := NewRegistry()
agent1 := &mockAgent{provider: domain.AgentProviderClaudeCode, name: "Claude v1", available: true}
agent2 := &mockAgent{provider: domain.AgentProviderClaudeCode, name: "Claude v2", available: true}
reg.Register(agent1)
reg.Register(agent2) // Should overwrite
got := reg.Get(domain.AgentProviderClaudeCode)
if got.Name() != "Claude v2" {
t.Errorf("expected 'Claude v2' after re-register, got %q", got.Name())
}
// Count should still be 1
if reg.Count() != 1 {
t.Errorf("expected count 1 after re-register, got %d", reg.Count())
}
}