api_test.go
1 package agents 2 3 import ( 4 "os" 5 "path/filepath" 6 "testing" 7 8 "github.com/Kocoro-lab/ShanClaw/internal/skills" 9 ) 10 11 func TestAgentToAPI_Minimal(t *testing.T) { 12 a := &Agent{Name: "test", Prompt: "hello"} 13 api := a.ToAPI() 14 if api.Name != "test" { 15 t.Errorf("name = %q", api.Name) 16 } 17 if api.Memory != nil { 18 t.Error("expected nil memory") 19 } 20 if api.Config != nil { 21 t.Error("expected nil config") 22 } 23 } 24 25 func TestAgentToAPI_Full(t *testing.T) { 26 a := &Agent{ 27 Name: "test", 28 Prompt: "hello", 29 Memory: "some memory", 30 Config: &AgentConfig{ 31 Tools: &AgentToolsFilter{Allow: []string{"bash"}}, 32 }, 33 Commands: map[string]string{"review": "do review"}, 34 Skills: []*skills.Skill{{Name: "check", Description: "check things", Prompt: "check it"}}, 35 } 36 api := a.ToAPI() 37 if api.Memory == nil || *api.Memory != "some memory" { 38 t.Error("expected memory") 39 } 40 if api.Config == nil || api.Config.Tools == nil { 41 t.Error("expected config with tools") 42 } 43 if len(api.Commands) != 1 { 44 t.Error("expected 1 command") 45 } 46 if len(api.Skills) != 1 { 47 t.Error("expected 1 skill") 48 } 49 } 50 51 func TestWriteAndLoadAgent(t *testing.T) { 52 // Layout: shannonDir/agents/<name>/ + shannonDir/skills/<skill>/ 53 // LoadAgent derives shannonDir from filepath.Dir(agentsDir) and loads 54 // skills from shannonDir/skills/, filtered by _attached.yaml manifest. 55 shannonDir := t.TempDir() 56 agentsDir := filepath.Join(shannonDir, "agents") 57 name := "test-agent" 58 59 if err := WriteAgentPrompt(agentsDir, name, "You are test."); err != nil { 60 t.Fatalf("WriteAgentPrompt: %v", err) 61 } 62 if err := WriteAgentCommand(agentsDir, name, "greet", "Say hello"); err != nil { 63 t.Fatalf("WriteAgentCommand: %v", err) 64 } 65 66 // Write skill to global skills dir (where LoadAgent looks) 67 globalSkillDir := filepath.Join(shannonDir, "skills", "check") 68 if err := os.MkdirAll(globalSkillDir, 0700); err != nil { 69 t.Fatal(err) 70 } 71 skillContent := "---\nname: check\ndescription: check things\n---\ncheck things\n" 72 if err := os.WriteFile(filepath.Join(globalSkillDir, "SKILL.md"), []byte(skillContent), 0600); err != nil { 73 t.Fatal(err) 74 } 75 76 // Attach the skill via manifest 77 if err := WriteAttachedSkills(agentsDir, name, []string{"check"}); err != nil { 78 t.Fatalf("WriteAttachedSkills: %v", err) 79 } 80 81 a, err := LoadAgent(agentsDir, name) 82 if err != nil { 83 t.Fatalf("LoadAgent: %v", err) 84 } 85 if a.Prompt != "You are test." { 86 t.Errorf("prompt = %q", a.Prompt) 87 } 88 if a.Commands["greet"] != "Say hello" { 89 t.Errorf("command = %q", a.Commands["greet"]) 90 } 91 found := false 92 for _, s := range a.Skills { 93 if s.Name == "check" { 94 found = true 95 break 96 } 97 } 98 if !found { 99 t.Errorf("agent skill 'check' not found in skills (got %d skills)", len(a.Skills)) 100 } 101 } 102 103 func TestDeleteAgentDir(t *testing.T) { 104 dir := t.TempDir() 105 WriteAgentPrompt(dir, "doomed", "bye") 106 if err := DeleteAgentDir(dir, "doomed"); err != nil { 107 t.Fatalf("DeleteAgentDir: %v", err) 108 } 109 if _, err := os.Stat(filepath.Join(dir, "doomed")); !os.IsNotExist(err) { 110 t.Error("expected directory removed") 111 } 112 } 113 114 func TestAgentCreateRequest_Validate(t *testing.T) { 115 // Missing name 116 r := &AgentCreateRequest{Prompt: "hi"} 117 if err := r.Validate(); err == nil { 118 t.Error("expected error for empty name") 119 } 120 // Missing prompt 121 r = &AgentCreateRequest{Name: "test"} 122 if err := r.Validate(); err == nil { 123 t.Error("expected error for empty prompt") 124 } 125 // Both allow and deny 126 r = &AgentCreateRequest{ 127 Name: "test", Prompt: "hi", 128 Config: &AgentConfigAPI{Tools: &AgentToolsFilter{Allow: []string{"a"}, Deny: []string{"b"}}}, 129 } 130 if err := r.Validate(); err == nil { 131 t.Error("expected error for both allow+deny") 132 } 133 // Valid 134 r = &AgentCreateRequest{Name: "test", Prompt: "hi"} 135 if err := r.Validate(); err != nil { 136 t.Errorf("unexpected error: %v", err) 137 } 138 139 r = &AgentCreateRequest{ 140 Name: "bad-skill", 141 Prompt: "hi", 142 Skills: []*skills.Skill{nil}, 143 } 144 if err := r.Validate(); err == nil { 145 t.Error("expected error for null skill entry") 146 } 147 } 148 149 func TestAgentConfigAPI_WatchHeartbeatRoundTrip(t *testing.T) { 150 agent := &Agent{ 151 Name: "test", 152 Prompt: "test prompt", 153 Config: &AgentConfig{ 154 Watch: []WatchEntry{{Path: "~/Code", Glob: "*.go"}}, 155 Heartbeat: &HeartbeatConfig{ 156 Every: "30m", 157 }, 158 }, 159 } 160 api := agent.ToAPI() 161 if api.Config == nil { 162 t.Fatal("expected config") 163 } 164 if len(api.Config.Watch) != 1 { 165 t.Fatalf("expected 1 watch entry, got %d", len(api.Config.Watch)) 166 } 167 if api.Config.Heartbeat == nil { 168 t.Fatal("expected heartbeat config") 169 } 170 if api.Config.Heartbeat.Every != "30m" { 171 t.Errorf("expected 30m, got %s", api.Config.Heartbeat.Every) 172 } 173 }