1package claudetool
2
3import (
4 "context"
5 "encoding/json"
6 "testing"
7 "time"
8)
9
10// mockSubagentDB implements SubagentDB for testing.
11type mockSubagentDB struct {
12 conversations map[string]string // slug -> conversationID
13}
14
15func newMockSubagentDB() *mockSubagentDB {
16 return &mockSubagentDB{
17 conversations: make(map[string]string),
18 }
19}
20
21func (m *mockSubagentDB) GetOrCreateSubagentConversation(ctx context.Context, slug, parentID, cwd string) (string, string, error) {
22 key := parentID + ":" + slug
23 if id, ok := m.conversations[key]; ok {
24 return id, slug, nil
25 }
26 id := "subagent-" + slug
27 m.conversations[key] = id
28 return id, slug, nil
29}
30
31// mockSubagentRunner implements SubagentRunner for testing.
32type mockSubagentRunner struct {
33 response string
34 err error
35}
36
37func (m *mockSubagentRunner) RunSubagent(ctx context.Context, conversationID, prompt string, wait bool, timeout time.Duration) (string, error) {
38 if m.err != nil {
39 return "", m.err
40 }
41 return m.response, nil
42}
43
44func TestSubagentTool_SanitizeSlug(t *testing.T) {
45 tests := []struct {
46 input string
47 expected string
48 }{
49 {"test-slug", "test-slug"},
50 {"Test Slug", "test-slug"},
51 {"test_slug", "test-slug"},
52 {"test--slug", "test-slug"},
53 {"-test-slug-", "test-slug"},
54 {"test@slug!", "testslug"},
55 {"123-abc", "123-abc"},
56 {"", ""},
57 }
58
59 for _, tt := range tests {
60 t.Run(tt.input, func(t *testing.T) {
61 result := sanitizeSlug(tt.input)
62 if result != tt.expected {
63 t.Errorf("sanitizeSlug(%q) = %q, want %q", tt.input, result, tt.expected)
64 }
65 })
66 }
67}
68
69func TestSubagentTool_Run(t *testing.T) {
70 wd := NewMutableWorkingDir("/tmp")
71 db := newMockSubagentDB()
72 runner := &mockSubagentRunner{response: "Task completed successfully"}
73
74 tool := &SubagentTool{
75 DB: db,
76 ParentConversationID: "parent-123",
77 WorkingDir: wd,
78 Runner: runner,
79 }
80
81 input := subagentInput{
82 Slug: "test-task",
83 Prompt: "Do something useful",
84 }
85 inputJSON, _ := json.Marshal(input)
86
87 result := tool.Run(context.Background(), inputJSON)
88 if result.Error != nil {
89 t.Fatalf("unexpected error: %v", result.Error)
90 }
91
92 if len(result.LLMContent) == 0 {
93 t.Fatal("expected LLM content")
94 }
95
96 if result.LLMContent[0].Text == "" {
97 t.Error("expected non-empty response text")
98 }
99
100 // Check display data
101 if result.Display == nil {
102 t.Error("expected display data")
103 }
104 displayData, ok := result.Display.(SubagentDisplayData)
105 if !ok {
106 t.Error("display data should be SubagentDisplayData")
107 }
108 if displayData.Slug != "test-task" {
109 t.Errorf("expected slug 'test-task', got %q", displayData.Slug)
110 }
111}
112
113func TestSubagentTool_Validation(t *testing.T) {
114 wd := NewMutableWorkingDir("/tmp")
115 db := newMockSubagentDB()
116 runner := &mockSubagentRunner{response: "OK"}
117
118 tool := &SubagentTool{
119 DB: db,
120 ParentConversationID: "parent-123",
121 WorkingDir: wd,
122 Runner: runner,
123 }
124
125 // Test empty slug
126 t.Run("empty slug", func(t *testing.T) {
127 input := subagentInput{Slug: "", Prompt: "test"}
128 inputJSON, _ := json.Marshal(input)
129 result := tool.Run(context.Background(), inputJSON)
130 if result.Error == nil {
131 t.Error("expected error for empty slug")
132 }
133 })
134
135 // Test empty prompt
136 t.Run("empty prompt", func(t *testing.T) {
137 input := subagentInput{Slug: "test", Prompt: ""}
138 inputJSON, _ := json.Marshal(input)
139 result := tool.Run(context.Background(), inputJSON)
140 if result.Error == nil {
141 t.Error("expected error for empty prompt")
142 }
143 })
144
145 // Test invalid slug (only special chars)
146 t.Run("invalid slug", func(t *testing.T) {
147 input := subagentInput{Slug: "@#$%", Prompt: "test"}
148 inputJSON, _ := json.Marshal(input)
149 result := tool.Run(context.Background(), inputJSON)
150 if result.Error == nil {
151 t.Error("expected error for invalid slug")
152 }
153 })
154}