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