Files
kandev/apps/backend/internal/agent/lifecycle/profile_resolver_test.go
T
João Salavisa 85ecf7debb feat(agentctl): protocol adapter abstraction with agent profile support (#15)
* feat(agentctl): implement protocol adapter abstraction

- Add AgentAdapter interface for protocol-agnostic agent communication
- Create ACPAdapter implementation for ACP protocol (JSON-RPC over stdin/stdout)
- Add Protocol field to agent registry (AgentTypeConfig)
- Update process.Manager to use adapters instead of hardcoded ACP
- Update API layer to use adapter interface
- Pass protocol from registry to agentctl via CLI flag and env var
- Support protocol configuration in both Docker and standalone modes

This refactoring allows agentctl to support multiple protocols (ACP, REST, MCP)
via a pluggable adapter pattern. The backend remains protocol-agnostic,
always communicating with agentctl's HTTP API.

* refactor(agentctl): consolidate types and clean up code

- Create shared types package (internal/agentctl/types) for permission types
- Create shared protocol package (pkg/agent) for Protocol type
- Remove duplicate permission types from acp/client.go and adapter.go
- Make adapter the single source of truth for session ID
- Remove unused Command field from adapter.Config
- Remove passthrough wrapper function handleAdapterPermissionRequest
- Clean up extra empty lines in process/manager.go and acp_adapter.go
- Re-export permission types from adapter package for convenience

* fix(agent-session): rename AgentType to AgentProfileID for consistency

- Rename AgentSession.AgentType to AgentProfileID in models
- Update agent_sessions table schema column from agent_type to agent_profile_id
- Update all SQL queries in sqlite.go to use agent_profile_id
- Fix ToAPI() method to return agent_profile_id key
- Remove misleading comments in executor.go

Also adds comprehensive tests for affected areas:
- adapter package unit tests (SessionUpdate, PlanEntry, AgentInfo, Config)
- profile resolver unit tests with mock repository
- AgentSession CRUD tests in sqlite_test.go
2026-01-14 18:16:26 +00:00

218 lines
6.3 KiB
Go

package lifecycle
import (
"context"
"errors"
"testing"
"github.com/kandev/kandev/internal/agent/settings/models"
"github.com/kandev/kandev/internal/agent/settings/store"
)
// MockRepository implements store.Repository for testing
type MockRepository struct {
GetAgentFn func(ctx context.Context, id string) (*models.Agent, error)
GetAgentByNameFn func(ctx context.Context, name string) (*models.Agent, error)
GetAgentProfileFn func(ctx context.Context, id string) (*models.AgentProfile, error)
ListAgentsFn func(ctx context.Context) ([]*models.Agent, error)
ListAgentProfilesFn func(ctx context.Context, agentID string) ([]*models.AgentProfile, error)
}
var _ store.Repository = (*MockRepository)(nil)
func (m *MockRepository) CreateAgent(ctx context.Context, agent *models.Agent) error {
return nil
}
func (m *MockRepository) GetAgent(ctx context.Context, id string) (*models.Agent, error) {
if m.GetAgentFn != nil {
return m.GetAgentFn(ctx, id)
}
return nil, errors.New("agent not found")
}
func (m *MockRepository) GetAgentByName(ctx context.Context, name string) (*models.Agent, error) {
if m.GetAgentByNameFn != nil {
return m.GetAgentByNameFn(ctx, name)
}
return nil, errors.New("agent not found")
}
func (m *MockRepository) UpdateAgent(ctx context.Context, agent *models.Agent) error {
return nil
}
func (m *MockRepository) DeleteAgent(ctx context.Context, id string) error {
return nil
}
func (m *MockRepository) ListAgents(ctx context.Context) ([]*models.Agent, error) {
if m.ListAgentsFn != nil {
return m.ListAgentsFn(ctx)
}
return []*models.Agent{}, nil
}
func (m *MockRepository) CreateAgentProfile(ctx context.Context, profile *models.AgentProfile) error {
return nil
}
func (m *MockRepository) UpdateAgentProfile(ctx context.Context, profile *models.AgentProfile) error {
return nil
}
func (m *MockRepository) DeleteAgentProfile(ctx context.Context, id string) error {
return nil
}
func (m *MockRepository) GetAgentProfile(ctx context.Context, id string) (*models.AgentProfile, error) {
if m.GetAgentProfileFn != nil {
return m.GetAgentProfileFn(ctx, id)
}
return nil, errors.New("profile not found")
}
func (m *MockRepository) ListAgentProfiles(ctx context.Context, agentID string) ([]*models.AgentProfile, error) {
if m.ListAgentProfilesFn != nil {
return m.ListAgentProfilesFn(ctx, agentID)
}
return []*models.AgentProfile{}, nil
}
func (m *MockRepository) Close() error {
return nil
}
func TestNewStoreProfileResolver(t *testing.T) {
mockRepo := &MockRepository{}
resolver := NewStoreProfileResolver(mockRepo)
if resolver == nil {
t.Fatal("expected non-nil resolver")
}
if resolver.store != mockRepo {
t.Error("expected resolver to use the provided store")
}
}
func TestStoreProfileResolver_ResolveProfile_Success(t *testing.T) {
mockRepo := &MockRepository{
GetAgentProfileFn: func(ctx context.Context, id string) (*models.AgentProfile, error) {
return &models.AgentProfile{
ID: "profile-123",
AgentID: "agent-456",
Name: "My Profile",
Model: "claude-3.5-sonnet",
AutoApprove: true,
DangerouslySkipPermissions: false,
Plan: "pro",
}, nil
},
GetAgentFn: func(ctx context.Context, id string) (*models.Agent, error) {
return &models.Agent{
ID: "agent-456",
Name: "claude",
}, nil
},
}
resolver := NewStoreProfileResolver(mockRepo)
ctx := context.Background()
info, err := resolver.ResolveProfile(ctx, "profile-123")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if info == nil {
t.Fatal("expected non-nil profile info")
}
if info.ProfileID != "profile-123" {
t.Errorf("expected ProfileID 'profile-123', got '%s'", info.ProfileID)
}
if info.ProfileName != "My Profile" {
t.Errorf("expected ProfileName 'My Profile', got '%s'", info.ProfileName)
}
if info.AgentID != "agent-456" {
t.Errorf("expected AgentID 'agent-456', got '%s'", info.AgentID)
}
if info.AgentName != "claude" {
t.Errorf("expected AgentName 'claude', got '%s'", info.AgentName)
}
if info.Model != "claude-3.5-sonnet" {
t.Errorf("expected Model 'claude-3.5-sonnet', got '%s'", info.Model)
}
if info.AutoApprove != true {
t.Error("expected AutoApprove to be true")
}
if info.DangerouslySkipPermissions != false {
t.Error("expected DangerouslySkipPermissions to be false")
}
if info.Plan != "pro" {
t.Errorf("expected Plan 'pro', got '%s'", info.Plan)
}
}
func TestStoreProfileResolver_ResolveProfile_ProfileNotFound(t *testing.T) {
mockRepo := &MockRepository{
GetAgentProfileFn: func(ctx context.Context, id string) (*models.AgentProfile, error) {
return nil, errors.New("profile not found")
},
}
resolver := NewStoreProfileResolver(mockRepo)
ctx := context.Background()
info, err := resolver.ResolveProfile(ctx, "non-existent-profile")
if err == nil {
t.Fatal("expected error for non-existent profile")
}
if info != nil {
t.Error("expected nil profile info on error")
}
if !errors.Is(err, errors.Unwrap(err)) && err.Error() == "" {
t.Error("expected error to contain message")
}
// Verify the error message contains "profile not found"
expectedMsg := "profile not found"
if err.Error()[:len(expectedMsg)] != expectedMsg {
t.Errorf("expected error message to start with '%s', got '%s'", expectedMsg, err.Error())
}
}
func TestStoreProfileResolver_ResolveProfile_AgentNotFound(t *testing.T) {
mockRepo := &MockRepository{
GetAgentProfileFn: func(ctx context.Context, id string) (*models.AgentProfile, error) {
return &models.AgentProfile{
ID: "profile-123",
AgentID: "non-existent-agent",
Name: "My Profile",
Model: "gpt-4",
}, nil
},
GetAgentFn: func(ctx context.Context, id string) (*models.Agent, error) {
return nil, errors.New("agent not found")
},
}
resolver := NewStoreProfileResolver(mockRepo)
ctx := context.Background()
info, err := resolver.ResolveProfile(ctx, "profile-123")
if err == nil {
t.Fatal("expected error when agent not found")
}
if info != nil {
t.Error("expected nil profile info on error")
}
// Verify the error message contains "agent not found for profile"
expectedMsg := "agent not found for profile"
if err.Error()[:len(expectedMsg)] != expectedMsg {
t.Errorf("expected error message to start with '%s', got '%s'", expectedMsg, err.Error())
}
}