blob: 5c7b3374eba111f4ffa296efefc30135c53762dd [file] [log] [blame]
iomododea44b02025-07-29 12:55:25 +04001package task
iomodod60a5352025-07-28 12:56:22 +04002
3import (
4 "context"
iomodo43ec6ae2025-07-28 17:40:12 +04005 "os"
iomodod60a5352025-07-28 12:56:22 +04006 "strings"
7 "testing"
8 "time"
9
10 "github.com/iomodo/staff/llm"
11 "github.com/iomodo/staff/tm"
12)
13
14// MockLLMProvider implements a mock LLM provider for testing
15type MockLLMProvider struct {
16 responses []string
17 callCount int
18}
19
20func NewMockLLMProvider(responses []string) *MockLLMProvider {
21 return &MockLLMProvider{
22 responses: responses,
23 callCount: 0,
24 }
25}
26
27func (m *MockLLMProvider) ChatCompletion(ctx context.Context, req llm.ChatCompletionRequest) (*llm.ChatCompletionResponse, error) {
28 if m.callCount >= len(m.responses) {
29 return nil, nil
30 }
iomododea44b02025-07-29 12:55:25 +040031
iomodod60a5352025-07-28 12:56:22 +040032 response := m.responses[m.callCount]
33 m.callCount++
iomododea44b02025-07-29 12:55:25 +040034
iomodod60a5352025-07-28 12:56:22 +040035 return &llm.ChatCompletionResponse{
36 ID: "mock-response",
37 Object: "chat.completion",
38 Created: time.Now().Unix(),
39 Model: req.Model,
40 Choices: []llm.ChatCompletionChoice{
41 {
42 Index: 0,
43 Message: llm.Message{
44 Role: llm.RoleAssistant,
45 Content: response,
46 },
47 FinishReason: "stop",
48 },
49 },
50 Usage: llm.Usage{
51 PromptTokens: 100,
52 CompletionTokens: 300,
53 TotalTokens: 400,
54 },
55 }, nil
56}
57
58func (m *MockLLMProvider) CreateEmbeddings(ctx context.Context, req llm.EmbeddingRequest) (*llm.EmbeddingResponse, error) {
59 return &llm.EmbeddingResponse{
60 Object: "list",
61 Data: []llm.Embedding{
62 {
63 Object: "embedding",
64 Index: 0,
65 Embedding: make([]float64, 1536),
66 },
67 },
68 Model: req.Model,
69 Usage: llm.Usage{
70 PromptTokens: 50,
71 TotalTokens: 50,
72 },
73 }, nil
74}
75
76func (m *MockLLMProvider) Close() error {
77 return nil
78}
79
80func TestNewSubtaskService(t *testing.T) {
81 mockProvider := NewMockLLMProvider([]string{})
82 agentRoles := []string{"backend", "frontend", "qa"}
iomododea44b02025-07-29 12:55:25 +040083
iomodo62da94a2025-07-28 19:01:55 +040084 service := NewSubtaskService(mockProvider, nil, agentRoles, nil, "example", "repo", nil, nil)
iomododea44b02025-07-29 12:55:25 +040085
iomodod60a5352025-07-28 12:56:22 +040086 if service == nil {
87 t.Fatal("NewSubtaskService returned nil")
88 }
iomododea44b02025-07-29 12:55:25 +040089
iomodod60a5352025-07-28 12:56:22 +040090 if service.llmProvider != mockProvider {
91 t.Error("LLM provider not set correctly")
92 }
iomododea44b02025-07-29 12:55:25 +040093
iomodod60a5352025-07-28 12:56:22 +040094 if len(service.agentRoles) != 3 {
95 t.Errorf("Expected 3 agent roles, got %d", len(service.agentRoles))
96 }
97}
98
iomodo5c99a442025-07-28 14:23:52 +040099func TestShouldGenerateSubtasks(t *testing.T) {
100 // Test decision to generate subtasks
101 decisionResponse := `{
102 "needs_subtasks": true,
103 "reasoning": "Complex task requiring multiple skills",
104 "complexity_score": 8,
105 "required_skills": ["backend", "frontend", "database"]
106}`
107
108 mockProvider := NewMockLLMProvider([]string{decisionResponse})
109 agentRoles := []string{"backend", "frontend", "qa"}
iomodo62da94a2025-07-28 19:01:55 +0400110 service := NewSubtaskService(mockProvider, nil, agentRoles, nil, "example", "repo", nil, nil)
iomododea44b02025-07-29 12:55:25 +0400111
iomodo5c99a442025-07-28 14:23:52 +0400112 // Test the parseSubtaskDecision method directly since ShouldGenerateSubtasks is used by manager
113 decision, err := service.parseSubtaskDecision(decisionResponse)
114 if err != nil {
115 t.Fatalf("parseSubtaskDecision failed: %v", err)
116 }
iomododea44b02025-07-29 12:55:25 +0400117
iomodo5c99a442025-07-28 14:23:52 +0400118 if !decision.NeedsSubtasks {
119 t.Error("Expected decision to need subtasks")
120 }
iomododea44b02025-07-29 12:55:25 +0400121
iomodo5c99a442025-07-28 14:23:52 +0400122 if decision.ComplexityScore != 8 {
123 t.Errorf("Expected complexity score 8, got %d", decision.ComplexityScore)
124 }
iomododea44b02025-07-29 12:55:25 +0400125
iomodo5c99a442025-07-28 14:23:52 +0400126 if len(decision.RequiredSkills) != 3 {
127 t.Errorf("Expected 3 required skills, got %d", len(decision.RequiredSkills))
128 }
129}
130
iomodod60a5352025-07-28 12:56:22 +0400131func TestAnalyzeTaskForSubtasks(t *testing.T) {
132 jsonResponse := `{
133 "analysis_summary": "This task requires breaking down into multiple components",
134 "subtasks": [
135 {
136 "title": "Backend Development",
137 "description": "Implement server-side logic",
138 "priority": "high",
139 "assigned_to": "backend",
140 "estimated_hours": 16,
iomodo5c99a442025-07-28 14:23:52 +0400141 "dependencies": [],
142 "required_skills": ["go", "api_development"]
iomodod60a5352025-07-28 12:56:22 +0400143 },
144 {
145 "title": "Frontend Development",
146 "description": "Build user interface",
147 "priority": "medium",
148 "assigned_to": "frontend",
149 "estimated_hours": 12,
iomodo5c99a442025-07-28 14:23:52 +0400150 "dependencies": ["0"],
151 "required_skills": ["react", "typescript"]
152 }
153 ],
154 "agent_creations": [
155 {
156 "role": "security_specialist",
157 "skills": ["security_audit", "penetration_testing"],
158 "description": "Specialized agent for security tasks",
159 "justification": "Authentication requires security expertise"
iomodod60a5352025-07-28 12:56:22 +0400160 }
161 ],
162 "recommended_approach": "Start with backend then frontend",
163 "estimated_total_hours": 28,
164 "risk_assessment": "Medium complexity with API integration risks"
165}`
166
167 mockProvider := NewMockLLMProvider([]string{jsonResponse})
iomodo5c99a442025-07-28 14:23:52 +0400168 agentRoles := []string{"backend", "frontend", "qa", "ceo"} // Include CEO for agent creation
iomodo62da94a2025-07-28 19:01:55 +0400169 service := NewSubtaskService(mockProvider, nil, agentRoles, nil, "example", "repo", nil, nil)
iomododea44b02025-07-29 12:55:25 +0400170
iomodod60a5352025-07-28 12:56:22 +0400171 task := &tm.Task{
172 ID: "test-task-123",
173 Title: "Build authentication system",
174 Description: "Implement user login and registration",
175 Priority: tm.PriorityHigh,
176 Status: tm.StatusToDo,
177 CreatedAt: time.Now(),
178 UpdatedAt: time.Now(),
179 }
iomododea44b02025-07-29 12:55:25 +0400180
iomodod60a5352025-07-28 12:56:22 +0400181 analysis, err := service.AnalyzeTaskForSubtasks(context.Background(), task)
182 if err != nil {
183 t.Fatalf("AnalyzeTaskForSubtasks failed: %v", err)
184 }
iomododea44b02025-07-29 12:55:25 +0400185
iomodod60a5352025-07-28 12:56:22 +0400186 if analysis.ParentTaskID != task.ID {
187 t.Errorf("Expected parent task ID %s, got %s", task.ID, analysis.ParentTaskID)
188 }
iomododea44b02025-07-29 12:55:25 +0400189
iomodod60a5352025-07-28 12:56:22 +0400190 if analysis.AnalysisSummary == "" {
191 t.Error("Analysis summary should not be empty")
192 }
iomododea44b02025-07-29 12:55:25 +0400193
iomodo5c99a442025-07-28 14:23:52 +0400194 // Should have 3 subtasks (1 for agent creation + 2 original)
195 if len(analysis.Subtasks) != 3 {
196 t.Errorf("Expected 3 subtasks (including agent creation), got %d", len(analysis.Subtasks))
197 t.Logf("Subtasks: %+v", analysis.Subtasks)
198 return // Exit early if count is wrong to avoid index errors
iomodod60a5352025-07-28 12:56:22 +0400199 }
iomododea44b02025-07-29 12:55:25 +0400200
iomodo5c99a442025-07-28 14:23:52 +0400201 // Test agent creation was processed
202 if len(analysis.AgentCreations) != 1 {
203 t.Errorf("Expected 1 agent creation, got %d", len(analysis.AgentCreations))
204 } else {
205 agentCreation := analysis.AgentCreations[0]
206 if agentCreation.Role != "security_specialist" {
207 t.Errorf("Expected role 'security_specialist', got %s", agentCreation.Role)
208 }
209 if len(agentCreation.Skills) != 2 {
210 t.Errorf("Expected 2 skills, got %d", len(agentCreation.Skills))
211 }
212 }
213
214 // We already checked the count above
215
216 // Test first subtask (agent creation)
217 subtask0 := analysis.Subtasks[0]
218 if !strings.Contains(subtask0.Title, "Security_specialist") {
219 t.Errorf("Expected agent creation subtask for security_specialist, got %s", subtask0.Title)
220 }
221 if subtask0.AssignedTo != "ceo" {
222 t.Errorf("Expected agent creation assigned to 'ceo', got %s", subtask0.AssignedTo)
223 }
224
225 // Test second subtask (original backend task, now at index 1)
226 subtask1 := analysis.Subtasks[1]
iomodod60a5352025-07-28 12:56:22 +0400227 if subtask1.Title != "Backend Development" {
228 t.Errorf("Expected title 'Backend Development', got %s", subtask1.Title)
229 }
230 if subtask1.Priority != tm.PriorityHigh {
231 t.Errorf("Expected high priority, got %s", subtask1.Priority)
232 }
233 if subtask1.AssignedTo != "backend" {
234 t.Errorf("Expected assigned_to 'backend', got %s", subtask1.AssignedTo)
235 }
236 if subtask1.EstimatedHours != 16 {
237 t.Errorf("Expected 16 hours, got %d", subtask1.EstimatedHours)
238 }
iomodo5c99a442025-07-28 14:23:52 +0400239 if len(subtask1.RequiredSkills) != 2 {
240 t.Errorf("Expected 2 required skills, got %d", len(subtask1.RequiredSkills))
241 }
iomododea44b02025-07-29 12:55:25 +0400242
iomodo5c99a442025-07-28 14:23:52 +0400243 // Test third subtask (original frontend task, now at index 2 with updated dependencies)
244 subtask2 := analysis.Subtasks[2]
iomodod60a5352025-07-28 12:56:22 +0400245 if subtask2.Title != "Frontend Development" {
246 t.Errorf("Expected title 'Frontend Development', got %s", subtask2.Title)
247 }
248 if subtask2.Priority != tm.PriorityMedium {
249 t.Errorf("Expected medium priority, got %s", subtask2.Priority)
250 }
iomodo5c99a442025-07-28 14:23:52 +0400251 // Dependencies should be updated to account for the new agent creation subtask
252 if len(subtask2.Dependencies) != 1 || subtask2.Dependencies[0] != "1" {
253 t.Errorf("Expected dependencies [1] (updated for agent creation), got %v", subtask2.Dependencies)
254 }
255 if len(subtask2.RequiredSkills) != 2 {
256 t.Errorf("Expected 2 required skills, got %d", len(subtask2.RequiredSkills))
iomodod60a5352025-07-28 12:56:22 +0400257 }
iomododea44b02025-07-29 12:55:25 +0400258
iomodo5c99a442025-07-28 14:23:52 +0400259 // Total hours should include agent creation time (4 hours)
iomodod60a5352025-07-28 12:56:22 +0400260 if analysis.EstimatedTotalHours != 28 {
261 t.Errorf("Expected 28 total hours, got %d", analysis.EstimatedTotalHours)
262 }
263}
264
265func TestAnalyzeTaskForSubtasks_InvalidJSON(t *testing.T) {
266 invalidResponse := "This is not valid JSON"
iomododea44b02025-07-29 12:55:25 +0400267
iomodod60a5352025-07-28 12:56:22 +0400268 mockProvider := NewMockLLMProvider([]string{invalidResponse})
269 agentRoles := []string{"backend", "frontend"}
iomodo62da94a2025-07-28 19:01:55 +0400270 service := NewSubtaskService(mockProvider, nil, agentRoles, nil, "example", "repo", nil, nil)
iomododea44b02025-07-29 12:55:25 +0400271
iomodod60a5352025-07-28 12:56:22 +0400272 task := &tm.Task{
273 ID: "test-task-123",
274 Title: "Test task",
275 }
iomododea44b02025-07-29 12:55:25 +0400276
iomodod60a5352025-07-28 12:56:22 +0400277 _, err := service.AnalyzeTaskForSubtasks(context.Background(), task)
278 if err == nil {
279 t.Error("Expected error for invalid JSON, got nil")
280 }
iomododea44b02025-07-29 12:55:25 +0400281
iomodod60a5352025-07-28 12:56:22 +0400282 if !strings.Contains(err.Error(), "no JSON found") {
283 t.Errorf("Expected 'no JSON found' error, got: %v", err)
284 }
285}
286
287func TestAnalyzeTaskForSubtasks_InvalidAgentRole(t *testing.T) {
288 jsonResponse := `{
289 "analysis_summary": "Test analysis",
290 "subtasks": [
291 {
292 "title": "Invalid Assignment",
293 "description": "Test subtask",
294 "priority": "high",
295 "assigned_to": "invalid_role",
296 "estimated_hours": 8,
297 "dependencies": []
298 }
299 ],
300 "recommended_approach": "Test approach",
301 "estimated_total_hours": 8
302}`
303
304 mockProvider := NewMockLLMProvider([]string{jsonResponse})
305 agentRoles := []string{"backend", "frontend"}
iomodo62da94a2025-07-28 19:01:55 +0400306 service := NewSubtaskService(mockProvider, nil, agentRoles, nil, "example", "repo", nil, nil)
iomododea44b02025-07-29 12:55:25 +0400307
iomodod60a5352025-07-28 12:56:22 +0400308 task := &tm.Task{
309 ID: "test-task-123",
310 Title: "Test task",
311 }
iomododea44b02025-07-29 12:55:25 +0400312
iomodod60a5352025-07-28 12:56:22 +0400313 analysis, err := service.AnalyzeTaskForSubtasks(context.Background(), task)
314 if err != nil {
315 t.Fatalf("AnalyzeTaskForSubtasks failed: %v", err)
316 }
iomododea44b02025-07-29 12:55:25 +0400317
iomodod60a5352025-07-28 12:56:22 +0400318 // Should fix invalid agent assignment to first available role
319 if analysis.Subtasks[0].AssignedTo != "backend" {
320 t.Errorf("Expected fixed assignment 'backend', got %s", analysis.Subtasks[0].AssignedTo)
321 }
322}
323
324func TestGenerateSubtaskPR(t *testing.T) {
325 mockProvider := NewMockLLMProvider([]string{})
iomodo62da94a2025-07-28 19:01:55 +0400326 service := NewSubtaskService(mockProvider, nil, []string{"backend"}, nil, "example", "repo", nil, nil)
iomododea44b02025-07-29 12:55:25 +0400327
iomodod60a5352025-07-28 12:56:22 +0400328 analysis := &tm.SubtaskAnalysis{
329 ParentTaskID: "task-123",
330 AnalysisSummary: "Test analysis summary",
331 RecommendedApproach: "Test approach",
332 EstimatedTotalHours: 40,
333 RiskAssessment: "Low risk",
334 Subtasks: []tm.SubtaskProposal{
335 {
336 Title: "Test Subtask",
337 Description: "Test description",
338 Priority: tm.PriorityHigh,
339 AssignedTo: "backend",
340 EstimatedHours: 8,
341 Dependencies: []string{},
342 },
343 },
344 }
iomododea44b02025-07-29 12:55:25 +0400345
iomodo443b20a2025-07-28 15:24:05 +0400346 // Test that PR generation fails when no PR provider is configured
347 _, err := service.GenerateSubtaskPR(context.Background(), analysis)
348 if err == nil {
349 t.Error("Expected error when PR provider not configured, got nil")
iomodod60a5352025-07-28 12:56:22 +0400350 }
iomododea44b02025-07-29 12:55:25 +0400351
iomodo443b20a2025-07-28 15:24:05 +0400352 if !strings.Contains(err.Error(), "PR provider not configured") {
353 t.Errorf("Expected 'PR provider not configured' error, got: %v", err)
iomodod60a5352025-07-28 12:56:22 +0400354 }
355}
356
357func TestBuildSubtaskAnalysisPrompt(t *testing.T) {
358 mockProvider := NewMockLLMProvider([]string{})
359 agentRoles := []string{"backend", "frontend", "qa"}
iomodo62da94a2025-07-28 19:01:55 +0400360 service := NewSubtaskService(mockProvider, nil, agentRoles, nil, "example", "repo", nil, nil)
iomododea44b02025-07-29 12:55:25 +0400361
iomodod60a5352025-07-28 12:56:22 +0400362 task := &tm.Task{
363 Title: "Build authentication system",
364 Description: "Implement user login and registration with OAuth",
365 Priority: tm.PriorityHigh,
366 Status: tm.StatusToDo,
367 }
iomododea44b02025-07-29 12:55:25 +0400368
iomodod60a5352025-07-28 12:56:22 +0400369 prompt := service.buildSubtaskAnalysisPrompt(task)
iomododea44b02025-07-29 12:55:25 +0400370
iomodod60a5352025-07-28 12:56:22 +0400371 if !strings.Contains(prompt, task.Title) {
372 t.Error("Prompt should contain task title")
373 }
iomododea44b02025-07-29 12:55:25 +0400374
iomodod60a5352025-07-28 12:56:22 +0400375 if !strings.Contains(prompt, task.Description) {
376 t.Error("Prompt should contain task description")
377 }
iomododea44b02025-07-29 12:55:25 +0400378
iomodod60a5352025-07-28 12:56:22 +0400379 if !strings.Contains(prompt, string(task.Priority)) {
380 t.Error("Prompt should contain task priority")
381 }
iomododea44b02025-07-29 12:55:25 +0400382
iomodod60a5352025-07-28 12:56:22 +0400383 if !strings.Contains(prompt, string(task.Status)) {
384 t.Error("Prompt should contain task status")
385 }
386}
387
388func TestGetSubtaskAnalysisSystemPrompt(t *testing.T) {
389 mockProvider := NewMockLLMProvider([]string{})
390 agentRoles := []string{"backend", "frontend", "qa", "devops"}
iomodo62da94a2025-07-28 19:01:55 +0400391 service := NewSubtaskService(mockProvider, nil, agentRoles, nil, "example", "repo", nil, nil)
iomododea44b02025-07-29 12:55:25 +0400392
iomodod60a5352025-07-28 12:56:22 +0400393 systemPrompt := service.getSubtaskAnalysisSystemPrompt()
iomododea44b02025-07-29 12:55:25 +0400394
iomodod60a5352025-07-28 12:56:22 +0400395 if !strings.Contains(systemPrompt, "backend") {
396 t.Error("System prompt should contain backend role")
397 }
iomododea44b02025-07-29 12:55:25 +0400398
iomodod60a5352025-07-28 12:56:22 +0400399 if !strings.Contains(systemPrompt, "frontend") {
400 t.Error("System prompt should contain frontend role")
401 }
iomododea44b02025-07-29 12:55:25 +0400402
iomodod60a5352025-07-28 12:56:22 +0400403 if !strings.Contains(systemPrompt, "JSON") {
404 t.Error("System prompt should mention JSON format")
405 }
iomododea44b02025-07-29 12:55:25 +0400406
iomodod60a5352025-07-28 12:56:22 +0400407 if !strings.Contains(systemPrompt, "subtasks") {
408 t.Error("System prompt should mention subtasks")
409 }
410}
411
412func TestIsValidAgentRole(t *testing.T) {
413 mockProvider := NewMockLLMProvider([]string{})
414 agentRoles := []string{"backend", "frontend", "qa"}
iomodo62da94a2025-07-28 19:01:55 +0400415 service := NewSubtaskService(mockProvider, nil, agentRoles, nil, "example", "repo", nil, nil)
iomododea44b02025-07-29 12:55:25 +0400416
iomodod60a5352025-07-28 12:56:22 +0400417 if !service.isValidAgentRole("backend") {
418 t.Error("'backend' should be a valid agent role")
419 }
iomododea44b02025-07-29 12:55:25 +0400420
iomodod60a5352025-07-28 12:56:22 +0400421 if !service.isValidAgentRole("frontend") {
422 t.Error("'frontend' should be a valid agent role")
423 }
iomododea44b02025-07-29 12:55:25 +0400424
iomodod60a5352025-07-28 12:56:22 +0400425 if service.isValidAgentRole("invalid") {
426 t.Error("'invalid' should not be a valid agent role")
427 }
iomododea44b02025-07-29 12:55:25 +0400428
iomodod60a5352025-07-28 12:56:22 +0400429 if service.isValidAgentRole("") {
430 t.Error("Empty string should not be a valid agent role")
431 }
432}
433
434func TestParseSubtaskAnalysis_Priority(t *testing.T) {
435 mockProvider := NewMockLLMProvider([]string{})
iomodo62da94a2025-07-28 19:01:55 +0400436 service := NewSubtaskService(mockProvider, nil, []string{"backend"}, nil, "example", "repo", nil, nil)
iomododea44b02025-07-29 12:55:25 +0400437
iomodod60a5352025-07-28 12:56:22 +0400438 tests := []struct {
439 input string
440 expected tm.TaskPriority
441 }{
442 {"high", tm.PriorityHigh},
443 {"HIGH", tm.PriorityHigh},
444 {"High", tm.PriorityHigh},
445 {"low", tm.PriorityLow},
446 {"LOW", tm.PriorityLow},
447 {"Low", tm.PriorityLow},
448 {"medium", tm.PriorityMedium},
449 {"MEDIUM", tm.PriorityMedium},
450 {"Medium", tm.PriorityMedium},
451 {"invalid", tm.PriorityMedium}, // default
452 {"", tm.PriorityMedium}, // default
453 }
iomododea44b02025-07-29 12:55:25 +0400454
iomodod60a5352025-07-28 12:56:22 +0400455 for _, test := range tests {
456 jsonResponse := `{
457 "analysis_summary": "Test",
458 "subtasks": [{
459 "title": "Test",
460 "description": "Test",
461 "priority": "` + test.input + `",
462 "assigned_to": "backend",
463 "estimated_hours": 8,
464 "dependencies": []
465 }],
466 "recommended_approach": "Test",
467 "estimated_total_hours": 8
468}`
iomododea44b02025-07-29 12:55:25 +0400469
iomodod60a5352025-07-28 12:56:22 +0400470 analysis, err := service.parseSubtaskAnalysis(jsonResponse, "test-task")
471 if err != nil {
472 t.Fatalf("parseSubtaskAnalysis failed for priority '%s': %v", test.input, err)
473 }
iomododea44b02025-07-29 12:55:25 +0400474
iomodod60a5352025-07-28 12:56:22 +0400475 if len(analysis.Subtasks) != 1 {
476 t.Fatalf("Expected 1 subtask, got %d", len(analysis.Subtasks))
477 }
iomododea44b02025-07-29 12:55:25 +0400478
iomodod60a5352025-07-28 12:56:22 +0400479 if analysis.Subtasks[0].Priority != test.expected {
iomododea44b02025-07-29 12:55:25 +0400480 t.Errorf("For priority '%s', expected %s, got %s",
iomodod60a5352025-07-28 12:56:22 +0400481 test.input, test.expected, analysis.Subtasks[0].Priority)
482 }
483 }
484}
485
iomodo43ec6ae2025-07-28 17:40:12 +0400486func TestGenerateSubtaskFile(t *testing.T) {
487 mockProvider := NewMockLLMProvider([]string{})
iomodo62da94a2025-07-28 19:01:55 +0400488 service := NewSubtaskService(mockProvider, nil, []string{"backend"}, nil, "example", "repo", nil, nil)
iomododea44b02025-07-29 12:55:25 +0400489
iomodo43ec6ae2025-07-28 17:40:12 +0400490 subtask := tm.SubtaskProposal{
491 Title: "Implement API endpoints",
492 Description: "Create REST API endpoints for user management",
493 Priority: tm.PriorityHigh,
494 AssignedTo: "backend",
495 EstimatedHours: 12,
496 Dependencies: []string{"0"},
497 RequiredSkills: []string{"go", "rest_api"},
498 }
iomododea44b02025-07-29 12:55:25 +0400499
iomodo43ec6ae2025-07-28 17:40:12 +0400500 taskID := "parent-task-subtask-1"
501 parentTaskID := "parent-task"
iomododea44b02025-07-29 12:55:25 +0400502
iomodo43ec6ae2025-07-28 17:40:12 +0400503 content := service.generateSubtaskFile(subtask, taskID, parentTaskID)
iomododea44b02025-07-29 12:55:25 +0400504
iomodo43ec6ae2025-07-28 17:40:12 +0400505 // Verify YAML frontmatter
506 if !strings.Contains(content, "id: parent-task-subtask-1") {
507 t.Error("Generated file should contain task ID in frontmatter")
508 }
509 if !strings.Contains(content, "title: Implement API endpoints") {
510 t.Error("Generated file should contain task title in frontmatter")
511 }
512 if !strings.Contains(content, "assignee: backend") {
513 t.Error("Generated file should contain assignee in frontmatter")
514 }
515 if !strings.Contains(content, "status: todo") {
516 t.Error("Generated file should have 'todo' status")
517 }
518 if !strings.Contains(content, "priority: high") {
519 t.Error("Generated file should contain priority in frontmatter")
520 }
521 if !strings.Contains(content, "parent_task_id: parent-task") {
522 t.Error("Generated file should contain parent task ID")
523 }
524 if !strings.Contains(content, "estimated_hours: 12") {
525 t.Error("Generated file should contain estimated hours")
526 }
iomododea44b02025-07-29 12:55:25 +0400527
iomodo43ec6ae2025-07-28 17:40:12 +0400528 // Verify dependencies are converted properly
529 if !strings.Contains(content, "dependencies:") {
530 t.Error("Generated file should contain dependencies section")
531 }
532 if !strings.Contains(content, "- parent-task-subtask-1") {
533 t.Error("Dependencies should be converted to subtask IDs")
534 }
iomododea44b02025-07-29 12:55:25 +0400535
iomodo43ec6ae2025-07-28 17:40:12 +0400536 // Verify required skills
537 if !strings.Contains(content, "required_skills:") {
538 t.Error("Generated file should contain required skills section")
539 }
540 if !strings.Contains(content, "- go") {
541 t.Error("Generated file should contain required skills")
542 }
iomododea44b02025-07-29 12:55:25 +0400543
iomodo43ec6ae2025-07-28 17:40:12 +0400544 // Verify markdown content
545 if !strings.Contains(content, "# Task Description") {
546 t.Error("Generated file should contain markdown task description")
547 }
548 if !strings.Contains(content, "Create REST API endpoints for user management") {
549 t.Error("Generated file should contain task description in body")
550 }
551}
552
553func TestUpdateParentTaskAsCompleted(t *testing.T) {
554 mockProvider := NewMockLLMProvider([]string{})
iomodo62da94a2025-07-28 19:01:55 +0400555 service := NewSubtaskService(mockProvider, nil, []string{"backend"}, nil, "example", "repo", nil, nil)
iomododea44b02025-07-29 12:55:25 +0400556
iomodo43ec6ae2025-07-28 17:40:12 +0400557 // Create a temporary task file for testing
558 taskContent := `---
559id: test-task-123
560title: Test Task
561description: A test task for validation
562assignee: backend
563status: todo
564priority: high
565created_at: 2024-01-01T10:00:00Z
566updated_at: 2024-01-01T10:00:00Z
567completed_at: null
568---
569
570# Task Description
571
572A test task for validation
573`
iomododea44b02025-07-29 12:55:25 +0400574
iomodo43ec6ae2025-07-28 17:40:12 +0400575 // Create temporary file
576 tmpFile, err := os.CreateTemp("", "test-task-*.md")
577 if err != nil {
578 t.Fatalf("Failed to create temp file: %v", err)
579 }
580 defer os.Remove(tmpFile.Name())
iomododea44b02025-07-29 12:55:25 +0400581
iomodo43ec6ae2025-07-28 17:40:12 +0400582 if err := os.WriteFile(tmpFile.Name(), []byte(taskContent), 0644); err != nil {
583 t.Fatalf("Failed to write temp file: %v", err)
584 }
iomododea44b02025-07-29 12:55:25 +0400585
iomodo43ec6ae2025-07-28 17:40:12 +0400586 // Create analysis with subtasks
587 analysis := &tm.SubtaskAnalysis{
588 ParentTaskID: "test-task-123",
589 EstimatedTotalHours: 20,
590 Subtasks: []tm.SubtaskProposal{
591 {
592 Title: "Subtask 1",
593 AssignedTo: "backend",
594 },
595 {
iomododea44b02025-07-29 12:55:25 +0400596 Title: "Subtask 2",
iomodo43ec6ae2025-07-28 17:40:12 +0400597 AssignedTo: "frontend",
598 },
599 },
600 }
iomododea44b02025-07-29 12:55:25 +0400601
iomodo43ec6ae2025-07-28 17:40:12 +0400602 // Update the parent task
603 err = service.updateParentTaskAsCompleted(tmpFile.Name(), analysis)
604 if err != nil {
605 t.Fatalf("updateParentTaskAsCompleted failed: %v", err)
606 }
iomododea44b02025-07-29 12:55:25 +0400607
iomodo43ec6ae2025-07-28 17:40:12 +0400608 // Read the updated file
609 updatedContent, err := os.ReadFile(tmpFile.Name())
610 if err != nil {
611 t.Fatalf("Failed to read updated file: %v", err)
612 }
iomododea44b02025-07-29 12:55:25 +0400613
iomodo43ec6ae2025-07-28 17:40:12 +0400614 updatedString := string(updatedContent)
iomododea44b02025-07-29 12:55:25 +0400615
iomodo43ec6ae2025-07-28 17:40:12 +0400616 // Verify the status was changed to completed
617 if !strings.Contains(updatedString, "status: completed") {
618 t.Error("Updated file should contain 'status: completed'")
619 }
iomododea44b02025-07-29 12:55:25 +0400620
iomodo43ec6ae2025-07-28 17:40:12 +0400621 // Verify completed_at was set (should not be null)
622 if strings.Contains(updatedString, "completed_at: null") {
623 t.Error("Updated file should have completed_at timestamp, not null")
624 }
iomododea44b02025-07-29 12:55:25 +0400625
iomodo43ec6ae2025-07-28 17:40:12 +0400626 // Verify subtask information was added
627 if !strings.Contains(updatedString, "## Subtasks Created") {
628 t.Error("Updated file should contain subtasks information")
629 }
iomododea44b02025-07-29 12:55:25 +0400630
iomodo43ec6ae2025-07-28 17:40:12 +0400631 if !strings.Contains(updatedString, "test-task-123-subtask-1") {
632 t.Error("Updated file should reference created subtask IDs")
633 }
iomododea44b02025-07-29 12:55:25 +0400634
iomodo43ec6ae2025-07-28 17:40:12 +0400635 if !strings.Contains(updatedString, "**Total Estimated Hours:** 20") {
636 t.Error("Updated file should contain total estimated hours")
637 }
638}
639
iomodod60a5352025-07-28 12:56:22 +0400640func TestClose(t *testing.T) {
641 mockProvider := NewMockLLMProvider([]string{})
iomodo62da94a2025-07-28 19:01:55 +0400642 service := NewSubtaskService(mockProvider, nil, []string{"backend"}, nil, "example", "repo", nil, nil)
iomododea44b02025-07-29 12:55:25 +0400643
iomodod60a5352025-07-28 12:56:22 +0400644 err := service.Close()
645 if err != nil {
646 t.Errorf("Close should not return error, got: %v", err)
647 }
648}
649
650// Benchmark tests
651func BenchmarkAnalyzeTaskForSubtasks(b *testing.B) {
652 jsonResponse := `{
653 "analysis_summary": "Benchmark test",
654 "subtasks": [
655 {
656 "title": "Benchmark Subtask",
657 "description": "Benchmark description",
658 "priority": "high",
659 "assigned_to": "backend",
660 "estimated_hours": 8,
661 "dependencies": []
662 }
663 ],
664 "recommended_approach": "Benchmark approach",
665 "estimated_total_hours": 8
666}`
667
668 mockProvider := NewMockLLMProvider([]string{jsonResponse})
iomodo62da94a2025-07-28 19:01:55 +0400669 service := NewSubtaskService(mockProvider, nil, []string{"backend", "frontend"}, nil, "example", "repo", nil, nil)
iomododea44b02025-07-29 12:55:25 +0400670
iomodod60a5352025-07-28 12:56:22 +0400671 task := &tm.Task{
672 ID: "benchmark-task",
673 Title: "Benchmark Task",
674 Description: "Task for benchmarking",
675 Priority: tm.PriorityHigh,
676 }
iomododea44b02025-07-29 12:55:25 +0400677
iomodod60a5352025-07-28 12:56:22 +0400678 b.ResetTimer()
679 for i := 0; i < b.N; i++ {
680 // Reset mock provider for each iteration
681 mockProvider.callCount = 0
682 _, err := service.AnalyzeTaskForSubtasks(context.Background(), task)
683 if err != nil {
684 b.Fatalf("AnalyzeTaskForSubtasks failed: %v", err)
685 }
686 }
iomododea44b02025-07-29 12:55:25 +0400687}