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>
262 lines
6.8 KiB
Go
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())
|
|
}
|
|
}
|