feat: complete acp-go-sdk integration with task state updates

- Use acp-go-sdk for all ACP communication in agentctl
- Mark agent as completed when Prompt() returns successfully
- Update task state to COMPLETED when agent completes
- Update task state to FAILED when agent fails and retries exhausted
- Enhanced E2E test to verify agent actually creates files
- E2E test now validates full flow: task creation -> agent work -> file creation -> task completion
This commit is contained in:
jcfs
2026-01-10 16:46:22 +00:00
parent 214b6ed2d8
commit 20f5f43fb3
143 changed files with 2750 additions and 296 deletions
@@ -0,0 +1,352 @@
package registry
import (
"encoding/json"
"os"
"path/filepath"
"testing"
"github.com/kandev/kandev/internal/common/logger"
)
func newTestLogger() *logger.Logger {
log, _ := logger.NewLogger(logger.LoggingConfig{
Level: "error",
Format: "json",
})
return log
}
func validAgentConfig(id, name string) *AgentTypeConfig {
return &AgentTypeConfig{
ID: id,
Name: name,
Description: "Test agent",
Image: "test/image",
Tag: "latest",
WorkingDir: "/workspace",
ResourceLimits: ResourceLimits{
MemoryMB: 1024,
CPUCores: 1.0,
TimeoutSeconds: 3600,
},
Capabilities: []string{"test"},
Enabled: true,
}
}
func TestNewRegistry(t *testing.T) {
log := newTestLogger()
reg := NewRegistry(log)
if reg == nil {
t.Fatal("expected non-nil registry")
}
if len(reg.agents) != 0 {
t.Errorf("expected empty agents map, got %d", len(reg.agents))
}
}
func TestRegistry_Register(t *testing.T) {
log := newTestLogger()
reg := NewRegistry(log)
config := validAgentConfig("test-agent", "Test Agent")
// Test successful registration
err := reg.Register(config)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Test duplicate registration
err = reg.Register(config)
if err == nil {
t.Error("expected error for duplicate registration")
}
}
func TestRegistry_RegisterValidation(t *testing.T) {
log := newTestLogger()
reg := NewRegistry(log)
tests := []struct {
name string
config *AgentTypeConfig
errMsg string
}{
{
name: "empty ID",
config: &AgentTypeConfig{Name: "test", Image: "img", ResourceLimits: ResourceLimits{MemoryMB: 1, CPUCores: 1, TimeoutSeconds: 1}},
errMsg: "agent type ID is required",
},
{
name: "empty name",
config: &AgentTypeConfig{ID: "test", Image: "img", ResourceLimits: ResourceLimits{MemoryMB: 1, CPUCores: 1, TimeoutSeconds: 1}},
errMsg: "agent type name is required",
},
{
name: "empty image",
config: &AgentTypeConfig{ID: "test", Name: "test", ResourceLimits: ResourceLimits{MemoryMB: 1, CPUCores: 1, TimeoutSeconds: 1}},
errMsg: "agent type image is required",
},
{
name: "zero memory",
config: &AgentTypeConfig{ID: "test", Name: "test", Image: "img", ResourceLimits: ResourceLimits{MemoryMB: 0, CPUCores: 1, TimeoutSeconds: 1}},
errMsg: "memory limit must be positive",
},
{
name: "zero CPU",
config: &AgentTypeConfig{ID: "test", Name: "test", Image: "img", ResourceLimits: ResourceLimits{MemoryMB: 1, CPUCores: 0, TimeoutSeconds: 1}},
errMsg: "CPU cores must be positive",
},
{
name: "zero timeout",
config: &AgentTypeConfig{ID: "test", Name: "test", Image: "img", ResourceLimits: ResourceLimits{MemoryMB: 1, CPUCores: 1, TimeoutSeconds: 0}},
errMsg: "timeout must be positive",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := reg.Register(tt.config)
if err == nil {
t.Error("expected error")
} else if err.Error() != tt.errMsg {
t.Errorf("expected error %q, got %q", tt.errMsg, err.Error())
}
})
}
}
func TestRegistry_Get(t *testing.T) {
log := newTestLogger()
reg := NewRegistry(log)
config := validAgentConfig("test-agent", "Test Agent")
_ = reg.Register(config)
// Test successful get
got, err := reg.Get("test-agent")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got.ID != config.ID {
t.Errorf("expected ID %q, got %q", config.ID, got.ID)
}
// Test not found
_, err = reg.Get("non-existent")
if err == nil {
t.Error("expected error for non-existent agent type")
}
}
func TestRegistry_Unregister(t *testing.T) {
log := newTestLogger()
reg := NewRegistry(log)
config := validAgentConfig("test-agent", "Test Agent")
_ = reg.Register(config)
// Test successful unregister
err := reg.Unregister("test-agent")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Verify it's gone
if reg.Exists("test-agent") {
t.Error("agent type should not exist after unregister")
}
// Test unregister non-existent
err = reg.Unregister("non-existent")
if err == nil {
t.Error("expected error for non-existent agent type")
}
}
func TestRegistry_List(t *testing.T) {
log := newTestLogger()
reg := NewRegistry(log)
// Empty list
list := reg.List()
if len(list) != 0 {
t.Errorf("expected empty list, got %d", len(list))
}
// Add agents
_ = reg.Register(validAgentConfig("agent-1", "Agent 1"))
_ = reg.Register(validAgentConfig("agent-2", "Agent 2"))
list = reg.List()
if len(list) != 2 {
t.Errorf("expected 2 agents, got %d", len(list))
}
}
func TestRegistry_ListEnabled(t *testing.T) {
log := newTestLogger()
reg := NewRegistry(log)
enabled := validAgentConfig("enabled-agent", "Enabled Agent")
enabled.Enabled = true
_ = reg.Register(enabled)
disabled := validAgentConfig("disabled-agent", "Disabled Agent")
disabled.Enabled = false
_ = reg.Register(disabled)
enabledList := reg.ListEnabled()
if len(enabledList) != 1 {
t.Errorf("expected 1 enabled agent, got %d", len(enabledList))
}
if enabledList[0].ID != "enabled-agent" {
t.Errorf("expected enabled-agent, got %s", enabledList[0].ID)
}
}
func TestRegistry_Exists(t *testing.T) {
log := newTestLogger()
reg := NewRegistry(log)
config := validAgentConfig("test-agent", "Test Agent")
_ = reg.Register(config)
if !reg.Exists("test-agent") {
t.Error("expected agent type to exist")
}
if reg.Exists("non-existent") {
t.Error("expected agent type to not exist")
}
}
func TestRegistry_LoadDefaults(t *testing.T) {
log := newTestLogger()
reg := NewRegistry(log)
reg.LoadDefaults()
// Check that default agents are loaded
defaults := DefaultAgents()
if len(defaults) == 0 {
t.Skip("no default agents configured")
}
for _, def := range defaults {
if !reg.Exists(def.ID) {
t.Errorf("default agent %q not loaded", def.ID)
}
}
}
func TestRegistry_LoadFromFile(t *testing.T) {
log := newTestLogger()
reg := NewRegistry(log)
// Create temp file with valid config
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "agents.json")
configs := []*AgentTypeConfig{
validAgentConfig("file-agent-1", "File Agent 1"),
validAgentConfig("file-agent-2", "File Agent 2"),
}
data, _ := json.Marshal(configs)
if err := os.WriteFile(configPath, data, 0644); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
// Test loading from file
err := reg.LoadFromFile(configPath)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !reg.Exists("file-agent-1") || !reg.Exists("file-agent-2") {
t.Error("agents from file not loaded")
}
}
func TestRegistry_LoadFromFile_NotFound(t *testing.T) {
log := newTestLogger()
reg := NewRegistry(log)
err := reg.LoadFromFile("/non/existent/path.json")
if err == nil {
t.Error("expected error for non-existent file")
}
}
func TestRegistry_LoadFromFile_InvalidJSON(t *testing.T) {
log := newTestLogger()
reg := NewRegistry(log)
tmpDir := t.TempDir()
configPath := filepath.Join(tmpDir, "invalid.json")
if err := os.WriteFile(configPath, []byte("invalid json"), 0644); err != nil {
t.Fatalf("failed to write test file: %v", err)
}
err := reg.LoadFromFile(configPath)
if err == nil {
t.Error("expected error for invalid JSON")
}
}
func TestAgentTypeConfig_ToAPIType(t *testing.T) {
config := validAgentConfig("test-agent", "Test Agent")
config.Description = "Test description"
config.Capabilities = []string{"cap1", "cap2"}
config.Env = map[string]string{"KEY": "value"}
apiType := config.ToAPIType()
if apiType.ID != config.ID {
t.Errorf("expected ID %q, got %q", config.ID, apiType.ID)
}
if apiType.Name != config.Name {
t.Errorf("expected Name %q, got %q", config.Name, apiType.Name)
}
if apiType.Description != config.Description {
t.Errorf("expected Description %q, got %q", config.Description, apiType.Description)
}
if apiType.DockerImage != config.Image {
t.Errorf("expected DockerImage %q, got %q", config.Image, apiType.DockerImage)
}
if len(apiType.Capabilities) != len(config.Capabilities) {
t.Errorf("expected %d capabilities, got %d", len(config.Capabilities), len(apiType.Capabilities))
}
if apiType.Enabled != config.Enabled {
t.Errorf("expected Enabled %v, got %v", config.Enabled, apiType.Enabled)
}
}
func TestValidateConfig(t *testing.T) {
// Valid config with default tag
config := &AgentTypeConfig{
ID: "test",
Name: "test",
Image: "test/image",
ResourceLimits: ResourceLimits{
MemoryMB: 1024,
CPUCores: 1.0,
TimeoutSeconds: 3600,
},
}
err := ValidateConfig(config)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Tag should be set to default
if config.Tag != "latest" {
t.Errorf("expected default tag 'latest', got %q", config.Tag)
}
}