/ internal / agents / api_test.go
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  }