85ecf7debb
* 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
218 lines
6.3 KiB
Go
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())
|
|
}
|
|
}
|
|
|