Add subagent tests
Change-Id: I331dd0cb8805b54384923ff305bfc6f3406de4d8
diff --git a/server/subtasks/service_test.go b/server/subtasks/service_test.go
new file mode 100644
index 0000000..1d94211
--- /dev/null
+++ b/server/subtasks/service_test.go
@@ -0,0 +1,455 @@
+package subtasks
+
+import (
+ "context"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/iomodo/staff/llm"
+ "github.com/iomodo/staff/tm"
+)
+
+// MockLLMProvider implements a mock LLM provider for testing
+type MockLLMProvider struct {
+ responses []string
+ callCount int
+}
+
+func NewMockLLMProvider(responses []string) *MockLLMProvider {
+ return &MockLLMProvider{
+ responses: responses,
+ callCount: 0,
+ }
+}
+
+func (m *MockLLMProvider) ChatCompletion(ctx context.Context, req llm.ChatCompletionRequest) (*llm.ChatCompletionResponse, error) {
+ if m.callCount >= len(m.responses) {
+ return nil, nil
+ }
+
+ response := m.responses[m.callCount]
+ m.callCount++
+
+ return &llm.ChatCompletionResponse{
+ ID: "mock-response",
+ Object: "chat.completion",
+ Created: time.Now().Unix(),
+ Model: req.Model,
+ Choices: []llm.ChatCompletionChoice{
+ {
+ Index: 0,
+ Message: llm.Message{
+ Role: llm.RoleAssistant,
+ Content: response,
+ },
+ FinishReason: "stop",
+ },
+ },
+ Usage: llm.Usage{
+ PromptTokens: 100,
+ CompletionTokens: 300,
+ TotalTokens: 400,
+ },
+ }, nil
+}
+
+func (m *MockLLMProvider) CreateEmbeddings(ctx context.Context, req llm.EmbeddingRequest) (*llm.EmbeddingResponse, error) {
+ return &llm.EmbeddingResponse{
+ Object: "list",
+ Data: []llm.Embedding{
+ {
+ Object: "embedding",
+ Index: 0,
+ Embedding: make([]float64, 1536),
+ },
+ },
+ Model: req.Model,
+ Usage: llm.Usage{
+ PromptTokens: 50,
+ TotalTokens: 50,
+ },
+ }, nil
+}
+
+func (m *MockLLMProvider) Close() error {
+ return nil
+}
+
+func TestNewSubtaskService(t *testing.T) {
+ mockProvider := NewMockLLMProvider([]string{})
+ agentRoles := []string{"backend", "frontend", "qa"}
+
+ service := NewSubtaskService(mockProvider, nil, agentRoles)
+
+ if service == nil {
+ t.Fatal("NewSubtaskService returned nil")
+ }
+
+ if service.llmProvider != mockProvider {
+ t.Error("LLM provider not set correctly")
+ }
+
+ if len(service.agentRoles) != 3 {
+ t.Errorf("Expected 3 agent roles, got %d", len(service.agentRoles))
+ }
+}
+
+func TestAnalyzeTaskForSubtasks(t *testing.T) {
+ jsonResponse := `{
+ "analysis_summary": "This task requires breaking down into multiple components",
+ "subtasks": [
+ {
+ "title": "Backend Development",
+ "description": "Implement server-side logic",
+ "priority": "high",
+ "assigned_to": "backend",
+ "estimated_hours": 16,
+ "dependencies": []
+ },
+ {
+ "title": "Frontend Development",
+ "description": "Build user interface",
+ "priority": "medium",
+ "assigned_to": "frontend",
+ "estimated_hours": 12,
+ "dependencies": ["0"]
+ }
+ ],
+ "recommended_approach": "Start with backend then frontend",
+ "estimated_total_hours": 28,
+ "risk_assessment": "Medium complexity with API integration risks"
+}`
+
+ mockProvider := NewMockLLMProvider([]string{jsonResponse})
+ agentRoles := []string{"backend", "frontend", "qa"}
+ service := NewSubtaskService(mockProvider, nil, agentRoles)
+
+ task := &tm.Task{
+ ID: "test-task-123",
+ Title: "Build authentication system",
+ Description: "Implement user login and registration",
+ Priority: tm.PriorityHigh,
+ Status: tm.StatusToDo,
+ CreatedAt: time.Now(),
+ UpdatedAt: time.Now(),
+ }
+
+ analysis, err := service.AnalyzeTaskForSubtasks(context.Background(), task)
+ if err != nil {
+ t.Fatalf("AnalyzeTaskForSubtasks failed: %v", err)
+ }
+
+ if analysis.ParentTaskID != task.ID {
+ t.Errorf("Expected parent task ID %s, got %s", task.ID, analysis.ParentTaskID)
+ }
+
+ if analysis.AnalysisSummary == "" {
+ t.Error("Analysis summary should not be empty")
+ }
+
+ if len(analysis.Subtasks) != 2 {
+ t.Errorf("Expected 2 subtasks, got %d", len(analysis.Subtasks))
+ }
+
+ // Test first subtask
+ subtask1 := analysis.Subtasks[0]
+ if subtask1.Title != "Backend Development" {
+ t.Errorf("Expected title 'Backend Development', got %s", subtask1.Title)
+ }
+ if subtask1.Priority != tm.PriorityHigh {
+ t.Errorf("Expected high priority, got %s", subtask1.Priority)
+ }
+ if subtask1.AssignedTo != "backend" {
+ t.Errorf("Expected assigned_to 'backend', got %s", subtask1.AssignedTo)
+ }
+ if subtask1.EstimatedHours != 16 {
+ t.Errorf("Expected 16 hours, got %d", subtask1.EstimatedHours)
+ }
+
+ // Test second subtask
+ subtask2 := analysis.Subtasks[1]
+ if subtask2.Title != "Frontend Development" {
+ t.Errorf("Expected title 'Frontend Development', got %s", subtask2.Title)
+ }
+ if subtask2.Priority != tm.PriorityMedium {
+ t.Errorf("Expected medium priority, got %s", subtask2.Priority)
+ }
+ if len(subtask2.Dependencies) != 1 || subtask2.Dependencies[0] != "0" {
+ t.Errorf("Expected dependencies [0], got %v", subtask2.Dependencies)
+ }
+
+ if analysis.EstimatedTotalHours != 28 {
+ t.Errorf("Expected 28 total hours, got %d", analysis.EstimatedTotalHours)
+ }
+}
+
+func TestAnalyzeTaskForSubtasks_InvalidJSON(t *testing.T) {
+ invalidResponse := "This is not valid JSON"
+
+ mockProvider := NewMockLLMProvider([]string{invalidResponse})
+ agentRoles := []string{"backend", "frontend"}
+ service := NewSubtaskService(mockProvider, nil, agentRoles)
+
+ task := &tm.Task{
+ ID: "test-task-123",
+ Title: "Test task",
+ }
+
+ _, err := service.AnalyzeTaskForSubtasks(context.Background(), task)
+ if err == nil {
+ t.Error("Expected error for invalid JSON, got nil")
+ }
+
+ if !strings.Contains(err.Error(), "no JSON found") {
+ t.Errorf("Expected 'no JSON found' error, got: %v", err)
+ }
+}
+
+func TestAnalyzeTaskForSubtasks_InvalidAgentRole(t *testing.T) {
+ jsonResponse := `{
+ "analysis_summary": "Test analysis",
+ "subtasks": [
+ {
+ "title": "Invalid Assignment",
+ "description": "Test subtask",
+ "priority": "high",
+ "assigned_to": "invalid_role",
+ "estimated_hours": 8,
+ "dependencies": []
+ }
+ ],
+ "recommended_approach": "Test approach",
+ "estimated_total_hours": 8
+}`
+
+ mockProvider := NewMockLLMProvider([]string{jsonResponse})
+ agentRoles := []string{"backend", "frontend"}
+ service := NewSubtaskService(mockProvider, nil, agentRoles)
+
+ task := &tm.Task{
+ ID: "test-task-123",
+ Title: "Test task",
+ }
+
+ analysis, err := service.AnalyzeTaskForSubtasks(context.Background(), task)
+ if err != nil {
+ t.Fatalf("AnalyzeTaskForSubtasks failed: %v", err)
+ }
+
+ // Should fix invalid agent assignment to first available role
+ if analysis.Subtasks[0].AssignedTo != "backend" {
+ t.Errorf("Expected fixed assignment 'backend', got %s", analysis.Subtasks[0].AssignedTo)
+ }
+}
+
+func TestGenerateSubtaskPR(t *testing.T) {
+ mockProvider := NewMockLLMProvider([]string{})
+ service := NewSubtaskService(mockProvider, nil, []string{"backend"})
+
+ analysis := &tm.SubtaskAnalysis{
+ ParentTaskID: "task-123",
+ AnalysisSummary: "Test analysis summary",
+ RecommendedApproach: "Test approach",
+ EstimatedTotalHours: 40,
+ RiskAssessment: "Low risk",
+ Subtasks: []tm.SubtaskProposal{
+ {
+ Title: "Test Subtask",
+ Description: "Test description",
+ Priority: tm.PriorityHigh,
+ AssignedTo: "backend",
+ EstimatedHours: 8,
+ Dependencies: []string{},
+ },
+ },
+ }
+
+ prURL, err := service.GenerateSubtaskPR(context.Background(), analysis)
+ if err != nil {
+ t.Fatalf("GenerateSubtaskPR failed: %v", err)
+ }
+
+ expectedURL := "https://github.com/example/repo/pull/subtasks-task-123"
+ if prURL != expectedURL {
+ t.Errorf("Expected PR URL %s, got %s", expectedURL, prURL)
+ }
+}
+
+func TestBuildSubtaskAnalysisPrompt(t *testing.T) {
+ mockProvider := NewMockLLMProvider([]string{})
+ agentRoles := []string{"backend", "frontend", "qa"}
+ service := NewSubtaskService(mockProvider, nil, agentRoles)
+
+ task := &tm.Task{
+ Title: "Build authentication system",
+ Description: "Implement user login and registration with OAuth",
+ Priority: tm.PriorityHigh,
+ Status: tm.StatusToDo,
+ }
+
+ prompt := service.buildSubtaskAnalysisPrompt(task)
+
+ if !strings.Contains(prompt, task.Title) {
+ t.Error("Prompt should contain task title")
+ }
+
+ if !strings.Contains(prompt, task.Description) {
+ t.Error("Prompt should contain task description")
+ }
+
+ if !strings.Contains(prompt, string(task.Priority)) {
+ t.Error("Prompt should contain task priority")
+ }
+
+ if !strings.Contains(prompt, string(task.Status)) {
+ t.Error("Prompt should contain task status")
+ }
+}
+
+func TestGetSubtaskAnalysisSystemPrompt(t *testing.T) {
+ mockProvider := NewMockLLMProvider([]string{})
+ agentRoles := []string{"backend", "frontend", "qa", "devops"}
+ service := NewSubtaskService(mockProvider, nil, agentRoles)
+
+ systemPrompt := service.getSubtaskAnalysisSystemPrompt()
+
+ if !strings.Contains(systemPrompt, "backend") {
+ t.Error("System prompt should contain backend role")
+ }
+
+ if !strings.Contains(systemPrompt, "frontend") {
+ t.Error("System prompt should contain frontend role")
+ }
+
+ if !strings.Contains(systemPrompt, "JSON") {
+ t.Error("System prompt should mention JSON format")
+ }
+
+ if !strings.Contains(systemPrompt, "subtasks") {
+ t.Error("System prompt should mention subtasks")
+ }
+}
+
+func TestIsValidAgentRole(t *testing.T) {
+ mockProvider := NewMockLLMProvider([]string{})
+ agentRoles := []string{"backend", "frontend", "qa"}
+ service := NewSubtaskService(mockProvider, nil, agentRoles)
+
+ if !service.isValidAgentRole("backend") {
+ t.Error("'backend' should be a valid agent role")
+ }
+
+ if !service.isValidAgentRole("frontend") {
+ t.Error("'frontend' should be a valid agent role")
+ }
+
+ if service.isValidAgentRole("invalid") {
+ t.Error("'invalid' should not be a valid agent role")
+ }
+
+ if service.isValidAgentRole("") {
+ t.Error("Empty string should not be a valid agent role")
+ }
+}
+
+func TestParseSubtaskAnalysis_Priority(t *testing.T) {
+ mockProvider := NewMockLLMProvider([]string{})
+ service := NewSubtaskService(mockProvider, nil, []string{"backend"})
+
+ tests := []struct {
+ input string
+ expected tm.TaskPriority
+ }{
+ {"high", tm.PriorityHigh},
+ {"HIGH", tm.PriorityHigh},
+ {"High", tm.PriorityHigh},
+ {"low", tm.PriorityLow},
+ {"LOW", tm.PriorityLow},
+ {"Low", tm.PriorityLow},
+ {"medium", tm.PriorityMedium},
+ {"MEDIUM", tm.PriorityMedium},
+ {"Medium", tm.PriorityMedium},
+ {"invalid", tm.PriorityMedium}, // default
+ {"", tm.PriorityMedium}, // default
+ }
+
+ for _, test := range tests {
+ jsonResponse := `{
+ "analysis_summary": "Test",
+ "subtasks": [{
+ "title": "Test",
+ "description": "Test",
+ "priority": "` + test.input + `",
+ "assigned_to": "backend",
+ "estimated_hours": 8,
+ "dependencies": []
+ }],
+ "recommended_approach": "Test",
+ "estimated_total_hours": 8
+}`
+
+ analysis, err := service.parseSubtaskAnalysis(jsonResponse, "test-task")
+ if err != nil {
+ t.Fatalf("parseSubtaskAnalysis failed for priority '%s': %v", test.input, err)
+ }
+
+ if len(analysis.Subtasks) != 1 {
+ t.Fatalf("Expected 1 subtask, got %d", len(analysis.Subtasks))
+ }
+
+ if analysis.Subtasks[0].Priority != test.expected {
+ t.Errorf("For priority '%s', expected %s, got %s",
+ test.input, test.expected, analysis.Subtasks[0].Priority)
+ }
+ }
+}
+
+func TestClose(t *testing.T) {
+ mockProvider := NewMockLLMProvider([]string{})
+ service := NewSubtaskService(mockProvider, nil, []string{"backend"})
+
+ err := service.Close()
+ if err != nil {
+ t.Errorf("Close should not return error, got: %v", err)
+ }
+}
+
+// Benchmark tests
+func BenchmarkAnalyzeTaskForSubtasks(b *testing.B) {
+ jsonResponse := `{
+ "analysis_summary": "Benchmark test",
+ "subtasks": [
+ {
+ "title": "Benchmark Subtask",
+ "description": "Benchmark description",
+ "priority": "high",
+ "assigned_to": "backend",
+ "estimated_hours": 8,
+ "dependencies": []
+ }
+ ],
+ "recommended_approach": "Benchmark approach",
+ "estimated_total_hours": 8
+}`
+
+ mockProvider := NewMockLLMProvider([]string{jsonResponse})
+ service := NewSubtaskService(mockProvider, nil, []string{"backend", "frontend"})
+
+ task := &tm.Task{
+ ID: "benchmark-task",
+ Title: "Benchmark Task",
+ Description: "Task for benchmarking",
+ Priority: tm.PriorityHigh,
+ }
+
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ // Reset mock provider for each iteration
+ mockProvider.callCount = 0
+ _, err := service.AnalyzeTaskForSubtasks(context.Background(), task)
+ if err != nil {
+ b.Fatalf("AnalyzeTaskForSubtasks failed: %v", err)
+ }
+ }
+}
\ No newline at end of file
diff --git a/server/tests/.testignore b/server/tests/.testignore
new file mode 100644
index 0000000..e96d735
--- /dev/null
+++ b/server/tests/.testignore
@@ -0,0 +1 @@
+# This directory contains standalone test executables, not Go tests
\ No newline at end of file
diff --git a/server/tests/test_fake_llm.go b/server/tests/test_fake_llm.go
deleted file mode 100644
index a322599..0000000
--- a/server/tests/test_fake_llm.go
+++ /dev/null
@@ -1,60 +0,0 @@
-package main
-
-import (
- "context"
- "fmt"
- "log"
-
- "github.com/iomodo/staff/llm"
- _ "github.com/iomodo/staff/llm/providers" // Auto-register providers
-)
-
-func main() {
- // Create fake LLM config
- config := llm.Config{
- Provider: llm.ProviderFake,
- APIKey: "fake-key",
- BaseURL: "fake://test",
- }
-
- // Create provider
- provider, err := llm.CreateProvider(config)
- if err != nil {
- log.Fatalf("Failed to create provider: %v", err)
- }
- defer provider.Close()
-
- // Test chat completion
- req := llm.ChatCompletionRequest{
- Model: "fake-gpt-4",
- Messages: []llm.Message{
- {
- Role: llm.RoleSystem,
- Content: "You are a helpful AI assistant.",
- },
- {
- Role: llm.RoleUser,
- Content: "Create a solution for implementing user authentication",
- },
- },
- MaxTokens: &[]int{4000}[0],
- Temperature: &[]float64{0.3}[0],
- }
-
- fmt.Println("Testing Fake LLM Provider...")
- fmt.Println("==========================")
-
- resp, err := provider.ChatCompletion(context.Background(), req)
- if err != nil {
- log.Fatalf("Chat completion failed: %v", err)
- }
-
- fmt.Printf("Response ID: %s\n", resp.ID)
- fmt.Printf("Model: %s\n", resp.Model)
- fmt.Printf("Provider: %s\n", resp.Provider)
- fmt.Printf("Usage: %+v\n", resp.Usage)
- fmt.Println("\nGenerated Solution:")
- fmt.Println("===================")
- fmt.Println(resp.Choices[0].Message.Content)
- fmt.Println("\nā
Fake LLM Provider is working correctly!")
-}
\ No newline at end of file
diff --git a/server/tests/test_subtasks.go b/server/tests/test_subtasks.go
deleted file mode 100644
index d91c2fd..0000000
--- a/server/tests/test_subtasks.go
+++ /dev/null
@@ -1,94 +0,0 @@
-package main
-
-import (
- "context"
- "fmt"
- "log"
- "time"
-
- "github.com/iomodo/staff/llm"
- _ "github.com/iomodo/staff/llm/providers"
- "github.com/iomodo/staff/subtasks"
- "github.com/iomodo/staff/tm"
-)
-
-func main() {
- fmt.Println("Testing Subtask Generation...")
- fmt.Println("==============================")
-
- // Create fake LLM provider
- config := llm.Config{
- Provider: llm.ProviderFake,
- APIKey: "fake-key",
- BaseURL: "fake://test",
- }
-
- provider, err := llm.CreateProvider(config)
- if err != nil {
- log.Fatalf("Failed to create provider: %v", err)
- }
- defer provider.Close()
-
- // Create a mock task
- task := &tm.Task{
- ID: "task-test-123",
- Title: "Build comprehensive user authentication system",
- Description: "Implement a complete user authentication system with registration, login, password reset, multi-factor authentication, OAuth integration with Google and GitHub, session management, role-based access control, and admin dashboard for user management. System should support enterprise SSO integration and have comprehensive audit logging for security compliance.",
- Priority: tm.PriorityHigh,
- Status: tm.StatusToDo,
- CreatedAt: time.Now(),
- UpdatedAt: time.Now(),
- }
-
- // Create subtask service with available agent roles
- agentRoles := []string{"ceo", "backend-engineer", "frontend-engineer", "qa", "devops"}
- subtaskService := subtasks.NewSubtaskService(provider, nil, agentRoles)
-
- fmt.Printf("š Analyzing task: %s\n", task.Title)
- fmt.Printf("š Description: %s\n\n", task.Description[:100]+"...")
-
- // Analyze task for subtasks
- fmt.Println("š¤ Running LLM analysis...")
- analysis, err := subtaskService.AnalyzeTaskForSubtasks(context.Background(), task)
- if err != nil {
- log.Fatalf("Failed to analyze task: %v", err)
- }
-
- fmt.Printf("ā
Analysis completed!\n\n")
- fmt.Printf("š **Analysis Summary:**\n%s\n\n", analysis.AnalysisSummary)
- fmt.Printf("šÆ **Recommended Approach:**\n%s\n\n", analysis.RecommendedApproach)
- fmt.Printf("ā±ļø **Estimated Total Hours:** %d\n\n", analysis.EstimatedTotalHours)
-
- if analysis.RiskAssessment != "" {
- fmt.Printf("ā ļø **Risk Assessment:**\n%s\n\n", analysis.RiskAssessment)
- }
-
- fmt.Printf("š **Proposed Subtasks (%d):**\n", len(analysis.Subtasks))
- fmt.Println("=================================")
-
- for i, subtask := range analysis.Subtasks {
- fmt.Printf("\n%d. **%s**\n", i+1, subtask.Title)
- fmt.Printf(" - Assigned to: %s\n", subtask.AssignedTo)
- fmt.Printf(" - Priority: %s\n", subtask.Priority)
- fmt.Printf(" - Hours: %d\n", subtask.EstimatedHours)
- if len(subtask.Dependencies) > 0 {
- fmt.Printf(" - Dependencies: %v\n", subtask.Dependencies)
- }
- fmt.Printf(" - Description: %s\n", subtask.Description)
- }
-
- // Generate PR content
- fmt.Println("\nš Generating PR content...")
- prURL, err := subtaskService.GenerateSubtaskPR(context.Background(), analysis)
- if err != nil {
- log.Fatalf("Failed to generate PR: %v", err)
- }
-
- fmt.Printf("\nā
**Subtask PR Generated:** %s\n", prURL)
- fmt.Println("\nš **Subtask generation test completed successfully!**")
- fmt.Println("\nš” **Next Steps:**")
- fmt.Println(" 1. Review the generated subtasks")
- fmt.Println(" 2. Approve the PR to create actual subtasks")
- fmt.Println(" 3. Assign subtasks to appropriate agents")
- fmt.Println(" 4. Monitor subtask completion progress")
-}
\ No newline at end of file