blob: 405e5b717980a7b72bdc1f2921656cf10b8b9e4f [file] [log] [blame]
iomodo76f9a2d2025-07-26 12:14:40 +04001package agent
2
3import (
4 "context"
5 "os"
6 "path/filepath"
7 "testing"
8 "time"
9
10 "github.com/iomodo/staff/git"
11 "github.com/iomodo/staff/llm"
12 "github.com/iomodo/staff/tm"
13 "github.com/iomodo/staff/tm/git_tm"
14 "github.com/stretchr/testify/assert"
15 "github.com/stretchr/testify/require"
16)
17
18// MockLLMProvider implements LLMProvider for testing
19type MockLLMProvider struct{}
20
21func (m *MockLLMProvider) ChatCompletion(ctx context.Context, req llm.ChatCompletionRequest) (*llm.ChatCompletionResponse, error) {
22 return &llm.ChatCompletionResponse{
23 ID: "mock-response-id",
24 Model: req.Model,
25 Choices: []llm.ChatCompletionChoice{
26 {
27 Index: 0,
28 Message: llm.Message{
29 Role: llm.RoleAssistant,
30 Content: "This is a mock response for testing purposes.",
31 },
32 FinishReason: "stop",
33 },
34 },
35 Usage: llm.Usage{
36 PromptTokens: 10,
37 CompletionTokens: 20,
38 TotalTokens: 30,
39 },
40 Provider: llm.ProviderOpenAI,
41 }, nil
42}
43
44func (m *MockLLMProvider) CreateEmbeddings(ctx context.Context, req llm.EmbeddingRequest) (*llm.EmbeddingResponse, error) {
45 return &llm.EmbeddingResponse{
46 Object: "list",
47 Data: []llm.Embedding{
48 {
49 Object: "embedding",
50 Embedding: []float64{0.1, 0.2, 0.3},
51 Index: 0,
52 },
53 },
54 Usage: llm.Usage{
55 PromptTokens: 5,
56 TotalTokens: 5,
57 },
58 Model: req.Model,
59 Provider: llm.ProviderOpenAI,
60 }, nil
61}
62
63func (m *MockLLMProvider) Close() error {
64 return nil
65}
66
67// MockLLMFactory implements ProviderFactory for testing
68type MockLLMFactory struct{}
69
70func (f *MockLLMFactory) CreateProvider(config llm.Config) (llm.LLMProvider, error) {
71 return &MockLLMProvider{}, nil
72}
73
74func (f *MockLLMFactory) SupportsProvider(provider llm.Provider) bool {
75 return provider == llm.ProviderOpenAI
76}
77
78func setupTestAgent(t *testing.T) (*Agent, func()) {
79 // Create temporary directories
80 tempDir, err := os.MkdirTemp("", "agent-test")
81 require.NoError(t, err)
82
83 tasksDir := filepath.Join(tempDir, "tasks")
84 workspaceDir := filepath.Join(tempDir, "workspace")
85 codeRepoDir := filepath.Join(tempDir, "code-repo")
86
87 // Create directories
88 require.NoError(t, os.MkdirAll(tasksDir, 0755))
89 require.NoError(t, os.MkdirAll(workspaceDir, 0755))
90 require.NoError(t, os.MkdirAll(codeRepoDir, 0755))
91
92 // Initialize git repositories
93 gitInterface := git.DefaultGit(tasksDir)
94 ctx := context.Background()
95
96 err = gitInterface.Init(ctx, tasksDir)
97 require.NoError(t, err)
98
99 // Set git user config
100 userConfig := git.UserConfig{
101 Name: "Test User",
102 Email: "test@example.com",
103 }
104 err = gitInterface.SetUserConfig(ctx, userConfig)
105 require.NoError(t, err)
106
107 // Create task manager
108 taskManager := git_tm.NewGitTaskManager(gitInterface, tasksDir)
109
110 // Create LLM config (using a mock configuration)
111 llmConfig := llm.Config{
112 Provider: llm.ProviderOpenAI,
113 APIKey: "test-key",
114 BaseURL: "https://api.openai.com/v1",
115 Timeout: 30 * time.Second,
116 }
117
118 // Create agent config
119 config := AgentConfig{
120 Name: "test-agent",
121 Role: "Test Engineer",
122 GitUsername: "test-agent",
123 GitEmail: "test-agent@test.com",
124 WorkingDir: workspaceDir,
125 LLMProvider: llm.ProviderOpenAI,
126 LLMModel: "gpt-3.5-turbo",
127 LLMConfig: llmConfig,
128 SystemPrompt: "You are a test agent. Provide simple, clear solutions.",
129 TaskManager: taskManager,
130 GitRepoPath: codeRepoDir,
131 GitRemote: "origin",
132 GitBranch: "main",
133 }
134
135 // Create agent with mock LLM provider
136 agent := &Agent{
137 Config: config,
138 llmProvider: &MockLLMProvider{},
139 gitInterface: git.DefaultGit(codeRepoDir),
140 ctx: context.Background(),
141 cancel: func() {},
142 }
143
144 cleanup := func() {
145 agent.Stop()
146 os.RemoveAll(tempDir)
147 }
148
149 return agent, cleanup
150}
151
152func TestNewAgent(t *testing.T) {
153 agent, cleanup := setupTestAgent(t)
154 defer cleanup()
155
156 assert.NotNil(t, agent)
157 assert.Equal(t, "test-agent", agent.Config.Name)
158 assert.Equal(t, "Test Engineer", agent.Config.Role)
159}
160
161func TestValidateConfig(t *testing.T) {
162 // Test valid config
163 validConfig := AgentConfig{
164 Name: "test",
165 Role: "test",
166 WorkingDir: "/tmp",
167 SystemPrompt: "test",
168 TaskManager: &git_tm.GitTaskManager{},
169 GitRepoPath: "/tmp",
170 }
171
172 err := validateConfig(validConfig)
173 assert.NoError(t, err)
174
175 // Test invalid configs
176 testCases := []struct {
177 name string
178 config AgentConfig
179 }{
180 {"empty name", AgentConfig{Role: "test", WorkingDir: "/tmp", SystemPrompt: "test", TaskManager: &git_tm.GitTaskManager{}, GitRepoPath: "/tmp"}},
181 {"empty role", AgentConfig{Name: "test", WorkingDir: "/tmp", SystemPrompt: "test", TaskManager: &git_tm.GitTaskManager{}, GitRepoPath: "/tmp"}},
182 {"empty working dir", AgentConfig{Name: "test", Role: "test", SystemPrompt: "test", TaskManager: &git_tm.GitTaskManager{}, GitRepoPath: "/tmp"}},
183 {"empty system prompt", AgentConfig{Name: "test", Role: "test", WorkingDir: "/tmp", SystemPrompt: "test", GitRepoPath: "/tmp"}},
184 {"nil task manager", AgentConfig{Name: "test", Role: "test", WorkingDir: "/tmp", SystemPrompt: "test", GitRepoPath: "/tmp"}},
185 {"empty git repo path", AgentConfig{Name: "test", Role: "test", WorkingDir: "/tmp", SystemPrompt: "test", TaskManager: &git_tm.GitTaskManager{}}},
186 }
187
188 for _, tc := range testCases {
189 t.Run(tc.name, func(t *testing.T) {
190 err := validateConfig(tc.config)
191 assert.Error(t, err)
192 })
193 }
194}
195
196func TestGenerateBranchName(t *testing.T) {
197 agent, cleanup := setupTestAgent(t)
198 defer cleanup()
199
200 task := &tm.Task{
201 ID: "task-123",
202 Title: "Implement User Authentication",
203 }
204
205 branchName := agent.generateBranchName(task)
206 assert.Contains(t, branchName, "task-123")
207 assert.Contains(t, branchName, "implement-user-authentication")
208}
209
210func TestBuildTaskPrompt(t *testing.T) {
211 agent, cleanup := setupTestAgent(t)
212 defer cleanup()
213
214 dueDate := time.Now().AddDate(0, 0, 7)
215 task := &tm.Task{
216 ID: "task-123",
217 Title: "Test Task",
218 Description: "This is a test task",
219 Priority: tm.PriorityHigh,
220 DueDate: &dueDate,
221 }
222
223 prompt := agent.buildTaskPrompt(task)
224 assert.Contains(t, prompt, "task-123")
225 assert.Contains(t, prompt, "Test Task")
226 assert.Contains(t, prompt, "This is a test task")
227 assert.Contains(t, prompt, "high")
228}
229
230func TestFormatSolution(t *testing.T) {
231 agent, cleanup := setupTestAgent(t)
232 defer cleanup()
233
234 task := &tm.Task{
235 ID: "task-123",
236 Title: "Test Task",
237 Description: "This is a test task description",
238 Priority: tm.PriorityMedium,
239 }
240
241 solution := "This is the solution to the task."
242 formatted := agent.formatSolution(task, solution)
243
244 assert.Contains(t, formatted, "# Task Solution: Test Task")
245 assert.Contains(t, formatted, "**Task ID:** task-123")
246 assert.Contains(t, formatted, "**Agent:** test-agent (Test Engineer)")
247 assert.Contains(t, formatted, "## Task Description")
248 assert.Contains(t, formatted, "This is a test task description")
249 assert.Contains(t, formatted, "## Solution")
250 assert.Contains(t, formatted, "This is the solution to the task.")
251 assert.Contains(t, formatted, "*This solution was generated by AI Agent*")
252}
253
254func TestAgentStop(t *testing.T) {
255 agent, cleanup := setupTestAgent(t)
256 defer cleanup()
257
258 // Test that Stop doesn't panic
259 assert.NotPanics(t, func() {
260 agent.Stop()
261 })
262}
263
264func TestGenerateBranchNameWithSpecialCharacters(t *testing.T) {
265 agent, cleanup := setupTestAgent(t)
266 defer cleanup()
267
268 testCases := []struct {
269 title string
270 expected string
271 }{
272 {
273 title: "Simple Task",
274 expected: "task/task-123-simple-task",
275 },
276 {
277 title: "Task with (parentheses) and [brackets]",
278 expected: "task/task-123-task-with-parentheses-and-brackets",
279 },
280 {
281 title: "Very Long Task Title That Should Be Truncated Because It Exceeds The Maximum Length Allowed For Branch Names",
282 expected: "task/task-123-very-long-task-title-that-should-be-truncated-beca",
283 },
284 }
285
286 for _, tc := range testCases {
287 t.Run(tc.title, func(t *testing.T) {
288 task := &tm.Task{
289 ID: "task-123",
290 Title: tc.title,
291 }
292
293 branchName := agent.generateBranchName(task)
294 assert.Equal(t, tc.expected, branchName)
295 })
296 }
297}
298
299func TestProcessTaskWithLLM(t *testing.T) {
300 agent, cleanup := setupTestAgent(t)
301 defer cleanup()
302
303 task := &tm.Task{
304 ID: "task-123",
305 Title: "Test Task",
306 Description: "This is a test task",
307 Priority: tm.PriorityHigh,
308 }
309
310 solution, err := agent.processTaskWithLLM(task)
311 assert.NoError(t, err)
312 assert.Contains(t, solution, "mock response")
313}
314
315func TestMockLLMProvider(t *testing.T) {
316 mockProvider := &MockLLMProvider{}
317
318 // Test ChatCompletion
319 req := llm.ChatCompletionRequest{
320 Model: "gpt-3.5-turbo",
321 Messages: []llm.Message{
322 {Role: llm.RoleUser, Content: "Hello"},
323 },
324 }
325
326 resp, err := mockProvider.ChatCompletion(context.Background(), req)
327 assert.NoError(t, err)
328 assert.NotNil(t, resp)
329 assert.Equal(t, "gpt-3.5-turbo", resp.Model)
330 assert.Len(t, resp.Choices, 1)
331 assert.Contains(t, resp.Choices[0].Message.Content, "mock response")
332
333 // Test CreateEmbeddings
334 embedReq := llm.EmbeddingRequest{
335 Input: "test",
336 Model: "text-embedding-ada-002",
337 }
338
339 embedResp, err := mockProvider.CreateEmbeddings(context.Background(), embedReq)
340 assert.NoError(t, err)
341 assert.NotNil(t, embedResp)
342 assert.Len(t, embedResp.Data, 1)
343 assert.Len(t, embedResp.Data[0].Embedding, 3)
344
345 // Test Close
346 err = mockProvider.Close()
347 assert.NoError(t, err)
348}