Add subtasks to the PR
Change-Id: I386be06949a54dee518e9b6a23344d76099b88ba
diff --git a/server/config.yaml b/server/config.yaml
index 7baf0f9..5a5b6dd 100644
--- a/server/config.yaml
+++ b/server/config.yaml
@@ -8,7 +8,7 @@
github:
token: "${GITHUB_TOKEN}"
- owner: "shota" # Replace with your GitHub username
+ owner: "iomodo" # Replace with your GitHub username
repo: "staff" # Replace with your repository name
git:
diff --git a/server/git/github.go b/server/git/github.go
index 8f15c68..63424f1 100644
--- a/server/git/github.go
+++ b/server/git/github.go
@@ -123,7 +123,12 @@
return nil, fmt.Errorf("failed to marshal request body: %w", err)
}
+ // Debug logging for request data
+ fmt.Printf("DEBUG: Creating PR with data: %s\n", string(jsonBody))
+
url := fmt.Sprintf("%s/repos/%s/%s/pulls", g.config.BaseURL, g.owner, g.repo)
+ fmt.Printf("DEBUG: POST URL: %s\n", url)
+
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonBody))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
@@ -140,7 +145,10 @@
defer resp.Body.Close()
if resp.StatusCode != http.StatusCreated {
- return nil, fmt.Errorf("GitHub API error: %d", resp.StatusCode)
+ // Read the error response body for detailed error information
+ var errorBody bytes.Buffer
+ _, _ = errorBody.ReadFrom(resp.Body)
+ return nil, fmt.Errorf("GitHub API error: %d - %s", resp.StatusCode, errorBody.String())
}
var githubPR githubPullRequest
diff --git a/server/subtasks/service.go b/server/subtasks/service.go
index 5f99f74..c1a1bc9 100644
--- a/server/subtasks/service.go
+++ b/server/subtasks/service.go
@@ -421,6 +421,7 @@
// Generate branch name for subtask proposal
branchName := fmt.Sprintf("subtasks/%s-proposal", analysis.ParentTaskID)
+ log.Printf("Creating subtask PR with branch: %s", branchName)
// Create Git branch and commit subtask proposal
if err := s.createSubtaskBranch(ctx, analysis, branchName); err != nil {
@@ -430,17 +431,31 @@
// Generate PR content
prContent := s.generateSubtaskPRContent(analysis)
title := fmt.Sprintf("Subtask Proposal: %s", analysis.ParentTaskID)
+
+ // Validate PR content
+ if title == "" {
+ return "", fmt.Errorf("PR title cannot be empty")
+ }
+ if prContent == "" {
+ return "", fmt.Errorf("PR description cannot be empty")
+ }
+
+ // Determine base branch (try main first, fallback to master)
+ baseBranch := s.determineBaseBranch(ctx)
+ log.Printf("Using base branch: %s", baseBranch)
// Create the pull request
options := git.PullRequestOptions{
Title: title,
Description: prContent,
HeadBranch: branchName,
- BaseBranch: "main",
+ BaseBranch: baseBranch,
Labels: []string{"subtasks", "proposal", "ai-generated"},
Draft: false,
}
+ log.Printf("Creating PR with options: title=%s, head=%s, base=%s", options.Title, options.HeadBranch, options.BaseBranch)
+
pr, err := s.prProvider.CreatePullRequest(ctx, options)
if err != nil {
return "", fmt.Errorf("failed to create PR: %w", err)
@@ -452,15 +467,220 @@
return prURL, nil
}
+// determineBaseBranch determines the correct base branch (main or master)
+func (s *SubtaskService) determineBaseBranch(ctx context.Context) string {
+ if s.cloneManager == nil {
+ return "main" // default fallback
+ }
+
+ // Get clone path to check branches
+ clonePath, err := s.cloneManager.GetAgentClonePath("subtask-service")
+ if err != nil {
+ log.Printf("Warning: failed to get clone path for base branch detection: %v", err)
+ return "main"
+ }
+
+ // Check if main branch exists
+ gitCmd := func(args ...string) *exec.Cmd {
+ return exec.CommandContext(ctx, "git", append([]string{"-C", clonePath}, args...)...)
+ }
+
+ // Try to checkout main branch
+ cmd := gitCmd("show-ref", "refs/remotes/origin/main")
+ if err := cmd.Run(); err == nil {
+ return "main"
+ }
+
+ // Try to checkout master branch
+ cmd = gitCmd("show-ref", "refs/remotes/origin/master")
+ if err := cmd.Run(); err == nil {
+ return "master"
+ }
+
+ // Default to main if neither can be detected
+ log.Printf("Warning: Could not determine base branch, defaulting to 'main'")
+ return "main"
+}
+
+// generateSubtaskFile creates the content for an individual subtask file
+func (s *SubtaskService) generateSubtaskFile(subtask tm.SubtaskProposal, taskID, parentTaskID string) string {
+ var content strings.Builder
+
+ // Generate YAML frontmatter
+ content.WriteString("---\n")
+ content.WriteString(fmt.Sprintf("id: %s\n", taskID))
+ content.WriteString(fmt.Sprintf("title: %s\n", subtask.Title))
+ content.WriteString(fmt.Sprintf("description: %s\n", subtask.Description))
+ content.WriteString(fmt.Sprintf("assignee: %s\n", subtask.AssignedTo))
+ content.WriteString(fmt.Sprintf("owner_id: %s\n", subtask.AssignedTo))
+ content.WriteString(fmt.Sprintf("owner_name: %s\n", subtask.AssignedTo))
+ content.WriteString(fmt.Sprintf("status: todo\n"))
+ content.WriteString(fmt.Sprintf("priority: %s\n", strings.ToLower(string(subtask.Priority))))
+ content.WriteString(fmt.Sprintf("parent_task_id: %s\n", parentTaskID))
+ content.WriteString(fmt.Sprintf("estimated_hours: %d\n", subtask.EstimatedHours))
+ content.WriteString(fmt.Sprintf("created_at: %s\n", time.Now().Format(time.RFC3339)))
+ content.WriteString(fmt.Sprintf("updated_at: %s\n", time.Now().Format(time.RFC3339)))
+ content.WriteString("completed_at: null\n")
+ content.WriteString("archived_at: null\n")
+
+ // Add dependencies if any
+ if len(subtask.Dependencies) > 0 {
+ content.WriteString("dependencies:\n")
+ for _, dep := range subtask.Dependencies {
+ // Convert dependency index to actual subtask ID
+ if depIndex := s.parseDependencyIndex(dep); depIndex >= 0 {
+ depTaskID := fmt.Sprintf("%s-subtask-%d", parentTaskID, depIndex+1)
+ content.WriteString(fmt.Sprintf(" - %s\n", depTaskID))
+ }
+ }
+ }
+
+ // Add required skills if any
+ if len(subtask.RequiredSkills) > 0 {
+ content.WriteString("required_skills:\n")
+ for _, skill := range subtask.RequiredSkills {
+ content.WriteString(fmt.Sprintf(" - %s\n", skill))
+ }
+ }
+
+ content.WriteString("---\n\n")
+
+ // Add markdown content
+ content.WriteString("# Task Description\n\n")
+ content.WriteString(fmt.Sprintf("%s\n\n", subtask.Description))
+
+ if subtask.EstimatedHours > 0 {
+ content.WriteString("## Estimated Effort\n\n")
+ content.WriteString(fmt.Sprintf("**Estimated Hours:** %d\n\n", subtask.EstimatedHours))
+ }
+
+ if len(subtask.RequiredSkills) > 0 {
+ content.WriteString("## Required Skills\n\n")
+ for _, skill := range subtask.RequiredSkills {
+ content.WriteString(fmt.Sprintf("- %s\n", skill))
+ }
+ content.WriteString("\n")
+ }
+
+ if len(subtask.Dependencies) > 0 {
+ content.WriteString("## Dependencies\n\n")
+ content.WriteString("This task depends on the completion of:\n\n")
+ for _, dep := range subtask.Dependencies {
+ if depIndex := s.parseDependencyIndex(dep); depIndex >= 0 {
+ depTaskID := fmt.Sprintf("%s-subtask-%d", parentTaskID, depIndex+1)
+ content.WriteString(fmt.Sprintf("- %s\n", depTaskID))
+ }
+ }
+ content.WriteString("\n")
+ }
+
+ content.WriteString("## Notes\n\n")
+ content.WriteString(fmt.Sprintf("This subtask was generated from parent task: %s\n", parentTaskID))
+ content.WriteString("Generated by Staff AI Agent System\n\n")
+
+ return content.String()
+}
+
+// updateParentTaskAsCompleted updates the parent task file to mark it as completed
+func (s *SubtaskService) updateParentTaskAsCompleted(taskFilePath string, analysis *tm.SubtaskAnalysis) error {
+ // Read the existing parent task file
+ content, err := os.ReadFile(taskFilePath)
+ if err != nil {
+ return fmt.Errorf("failed to read parent task file: %w", err)
+ }
+
+ taskContent := string(content)
+
+ // Find the YAML frontmatter boundaries
+ lines := strings.Split(taskContent, "\n")
+ var frontmatterStart, frontmatterEnd int = -1, -1
+
+ for i, line := range lines {
+ if line == "---" {
+ if frontmatterStart == -1 {
+ frontmatterStart = i
+ } else {
+ frontmatterEnd = i
+ break
+ }
+ }
+ }
+
+ if frontmatterStart == -1 || frontmatterEnd == -1 {
+ return fmt.Errorf("invalid task file format: missing YAML frontmatter")
+ }
+
+ // Update the frontmatter
+ now := time.Now().Format(time.RFC3339)
+ var updatedLines []string
+
+ // Add lines before frontmatter
+ updatedLines = append(updatedLines, lines[:frontmatterStart+1]...)
+
+ // Process frontmatter lines
+ for i := frontmatterStart + 1; i < frontmatterEnd; i++ {
+ line := lines[i]
+ if strings.HasPrefix(line, "status:") {
+ updatedLines = append(updatedLines, "status: completed")
+ } else if strings.HasPrefix(line, "updated_at:") {
+ updatedLines = append(updatedLines, fmt.Sprintf("updated_at: %s", now))
+ } else if strings.HasPrefix(line, "completed_at:") {
+ updatedLines = append(updatedLines, fmt.Sprintf("completed_at: %s", now))
+ } else {
+ updatedLines = append(updatedLines, line)
+ }
+ }
+
+ // Add closing frontmatter and rest of content
+ updatedLines = append(updatedLines, lines[frontmatterEnd:]...)
+
+ // Add subtask information to the task description
+ if frontmatterEnd+1 < len(lines) {
+ // Add subtask information
+ subtaskInfo := fmt.Sprintf("\n\n## Subtasks Created\n\nThis task has been broken down into %d subtasks:\n\n", len(analysis.Subtasks))
+ for i, subtask := range analysis.Subtasks {
+ subtaskID := fmt.Sprintf("%s-subtask-%d", analysis.ParentTaskID, i+1)
+ subtaskInfo += fmt.Sprintf("- **%s**: %s (assigned to %s)\n", subtaskID, subtask.Title, subtask.AssignedTo)
+ }
+ subtaskInfo += fmt.Sprintf("\n**Total Estimated Hours:** %d\n", analysis.EstimatedTotalHours)
+ subtaskInfo += fmt.Sprintf("**Completed:** %s - Task broken down into actionable subtasks\n", now)
+
+ // Insert subtask info before any existing body content
+ updatedContent := strings.Join(updatedLines[:len(updatedLines)], "\n") + subtaskInfo
+
+ // Write the updated content back to the file
+ if err := os.WriteFile(taskFilePath, []byte(updatedContent), 0644); err != nil {
+ return fmt.Errorf("failed to write updated parent task file: %w", err)
+ }
+ }
+
+ log.Printf("Updated parent task %s to completed status", analysis.ParentTaskID)
+ return nil
+}
+
// generateSubtaskPRContent creates markdown content for the subtask proposal PR
func (s *SubtaskService) generateSubtaskPRContent(analysis *tm.SubtaskAnalysis) string {
var content strings.Builder
- content.WriteString(fmt.Sprintf("# Subtask Proposal for Task %s\n\n", analysis.ParentTaskID))
+ content.WriteString(fmt.Sprintf("# Subtasks Created for Task %s\n\n", analysis.ParentTaskID))
+ content.WriteString(fmt.Sprintf("This PR creates **%d individual task files** in `/operations/tasks/` ready for agent assignment.\n\n", len(analysis.Subtasks)))
+ content.WriteString(fmt.Sprintf("✅ **Parent task `%s` has been marked as completed** - the complex task has been successfully broken down into actionable subtasks.\n\n", analysis.ParentTaskID))
content.WriteString(fmt.Sprintf("## Analysis Summary\n%s\n\n", analysis.AnalysisSummary))
content.WriteString(fmt.Sprintf("## Recommended Approach\n%s\n\n", analysis.RecommendedApproach))
content.WriteString(fmt.Sprintf("**Estimated Total Hours:** %d\n\n", analysis.EstimatedTotalHours))
+ // List the created task files
+ content.WriteString("## Created Task Files\n\n")
+ for i, subtask := range analysis.Subtasks {
+ taskID := fmt.Sprintf("%s-subtask-%d", analysis.ParentTaskID, i+1)
+ content.WriteString(fmt.Sprintf("### %d. `%s.md`\n", i+1, taskID))
+ content.WriteString(fmt.Sprintf("- **Title:** %s\n", subtask.Title))
+ content.WriteString(fmt.Sprintf("- **Assigned to:** %s\n", subtask.AssignedTo))
+ content.WriteString(fmt.Sprintf("- **Priority:** %s\n", subtask.Priority))
+ content.WriteString(fmt.Sprintf("- **Estimated Hours:** %d\n", subtask.EstimatedHours))
+ content.WriteString(fmt.Sprintf("- **Description:** %s\n\n", subtask.Description))
+ }
+
if analysis.RiskAssessment != "" {
content.WriteString(fmt.Sprintf("## Risk Assessment\n%s\n\n", analysis.RiskAssessment))
}
@@ -524,35 +744,76 @@
log.Printf("Warning: failed to pull latest changes: %v", err)
}
+ // Delete branch if it exists (cleanup from previous attempts)
+ cmd = gitCmd("branch", "-D", branchName)
+ _ = cmd.Run() // Ignore error if branch doesn't exist
+
+ // Also delete remote tracking branch if it exists
+ cmd = gitCmd("push", "origin", "--delete", branchName)
+ _ = cmd.Run() // Ignore error if branch doesn't exist
+
// Create and checkout new branch
cmd = gitCmd("checkout", "-b", branchName)
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to create branch: %w", err)
}
- // Create subtask proposal file
- proposalDir := filepath.Join(clonePath, "tasks", "subtasks")
- if err := os.MkdirAll(proposalDir, 0755); err != nil {
- return fmt.Errorf("failed to create proposal directory: %w", err)
+ // Create individual task files for each subtask
+ tasksDir := filepath.Join(clonePath, "operations", "tasks")
+ if err := os.MkdirAll(tasksDir, 0755); err != nil {
+ return fmt.Errorf("failed to create tasks directory: %w", err)
}
- proposalFile := filepath.Join(proposalDir, fmt.Sprintf("%s-proposal.md", analysis.ParentTaskID))
- proposalContent := s.generateSubtaskProposalFile(analysis)
+ var stagedFiles []string
- if err := os.WriteFile(proposalFile, []byte(proposalContent), 0644); err != nil {
- return fmt.Errorf("failed to write proposal file: %w", err)
+ // Update parent task to mark as completed
+ parentTaskFile := filepath.Join(tasksDir, fmt.Sprintf("%s.md", analysis.ParentTaskID))
+ if err := s.updateParentTaskAsCompleted(parentTaskFile, analysis); err != nil {
+ return fmt.Errorf("failed to update parent task: %w", err)
+ }
+
+ // Track parent task file for staging
+ parentRelativeFile := filepath.Join("operations", "tasks", fmt.Sprintf("%s.md", analysis.ParentTaskID))
+ stagedFiles = append(stagedFiles, parentRelativeFile)
+ log.Printf("Updated parent task file: %s", parentRelativeFile)
+
+ // Create a file for each subtask
+ for i, subtask := range analysis.Subtasks {
+ taskID := fmt.Sprintf("%s-subtask-%d", analysis.ParentTaskID, i+1)
+ taskFile := filepath.Join(tasksDir, fmt.Sprintf("%s.md", taskID))
+ taskContent := s.generateSubtaskFile(subtask, taskID, analysis.ParentTaskID)
+
+ if err := os.WriteFile(taskFile, []byte(taskContent), 0644); err != nil {
+ return fmt.Errorf("failed to write subtask file %s: %w", taskID, err)
+ }
+
+ // Track file for staging
+ relativeFile := filepath.Join("operations", "tasks", fmt.Sprintf("%s.md", taskID))
+ stagedFiles = append(stagedFiles, relativeFile)
+ log.Printf("Created subtask file: %s", relativeFile)
}
- // Stage the file
- relativeFile := filepath.Join("tasks", "subtasks", fmt.Sprintf("%s-proposal.md", analysis.ParentTaskID))
- cmd = gitCmd("add", relativeFile)
- if err := cmd.Run(); err != nil {
- return fmt.Errorf("failed to stage files: %w", err)
+ // Stage all subtask files
+ for _, file := range stagedFiles {
+ cmd = gitCmd("add", file)
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("failed to stage file %s: %w", file, err)
+ }
}
// Commit changes
- commitMsg := fmt.Sprintf("Subtask proposal for task %s\n\nGenerated by Staff AI Agent System\nProposed %d subtasks with %d new agents",
- analysis.ParentTaskID, len(analysis.Subtasks), len(analysis.AgentCreations))
+ commitMsg := fmt.Sprintf("Create %d subtasks for task %s and mark parent as completed\n\nGenerated by Staff AI Agent System\n\nFiles modified:\n- %s.md (marked as completed)\n\nCreated individual task files:\n",
+ len(analysis.Subtasks), analysis.ParentTaskID, analysis.ParentTaskID)
+
+ // Add list of created files to commit message
+ for i := range analysis.Subtasks {
+ taskID := fmt.Sprintf("%s-subtask-%d", analysis.ParentTaskID, i+1)
+ commitMsg += fmt.Sprintf("- %s.md\n", taskID)
+ }
+
+ if len(analysis.AgentCreations) > 0 {
+ commitMsg += fmt.Sprintf("\nProposed %d new agents for specialized skills", len(analysis.AgentCreations))
+ }
cmd = gitCmd("commit", "-m", commitMsg)
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to commit: %w", err)
@@ -568,51 +829,6 @@
return nil
}
-// generateSubtaskProposalFile creates the content for the subtask proposal file
-func (s *SubtaskService) generateSubtaskProposalFile(analysis *tm.SubtaskAnalysis) string {
- var content strings.Builder
-
- content.WriteString(fmt.Sprintf("# Subtask Proposal for Task %s\n\n", analysis.ParentTaskID))
- content.WriteString(fmt.Sprintf("**Generated:** %s\n\n", time.Now().Format(time.RFC3339)))
- content.WriteString(fmt.Sprintf("## Analysis Summary\n%s\n\n", analysis.AnalysisSummary))
-
- if len(analysis.AgentCreations) > 0 {
- content.WriteString("## Proposed New Agents\n\n")
- for i, agent := range analysis.AgentCreations {
- content.WriteString(fmt.Sprintf("### %d. %s Agent\n", i+1, strings.Title(agent.Role)))
- content.WriteString(fmt.Sprintf("- **Skills:** %s\n", strings.Join(agent.Skills, ", ")))
- content.WriteString(fmt.Sprintf("- **Description:** %s\n", agent.Description))
- content.WriteString(fmt.Sprintf("- **Justification:** %s\n\n", agent.Justification))
- }
- }
-
- content.WriteString("## Proposed Subtasks\n\n")
- for i, subtask := range analysis.Subtasks {
- content.WriteString(fmt.Sprintf("### %d. %s\n", i+1, subtask.Title))
- content.WriteString(fmt.Sprintf("- **Assigned to:** %s\n", subtask.AssignedTo))
- content.WriteString(fmt.Sprintf("- **Priority:** %s\n", subtask.Priority))
- content.WriteString(fmt.Sprintf("- **Estimated Hours:** %d\n", subtask.EstimatedHours))
- if len(subtask.RequiredSkills) > 0 {
- content.WriteString(fmt.Sprintf("- **Required Skills:** %s\n", strings.Join(subtask.RequiredSkills, ", ")))
- }
- if len(subtask.Dependencies) > 0 {
- content.WriteString(fmt.Sprintf("- **Dependencies:** %s\n", strings.Join(subtask.Dependencies, ", ")))
- }
- content.WriteString(fmt.Sprintf("- **Description:** %s\n\n", subtask.Description))
- }
-
- content.WriteString(fmt.Sprintf("## Recommended Approach\n%s\n\n", analysis.RecommendedApproach))
- content.WriteString(fmt.Sprintf("**Estimated Total Hours:** %d\n\n", analysis.EstimatedTotalHours))
-
- if analysis.RiskAssessment != "" {
- content.WriteString(fmt.Sprintf("## Risk Assessment\n%s\n\n", analysis.RiskAssessment))
- }
-
- content.WriteString("---\n\n")
- content.WriteString("*This proposal was generated by the Staff AI Agent System. Review and approve to proceed with subtask creation.*\n")
-
- return content.String()
-}
// Close cleans up the service
func (s *SubtaskService) Close() error {
diff --git a/server/subtasks/service_test.go b/server/subtasks/service_test.go
index 158bcdc..0a8e473 100644
--- a/server/subtasks/service_test.go
+++ b/server/subtasks/service_test.go
@@ -2,6 +2,7 @@
import (
"context"
+ "os"
"strings"
"testing"
"time"
@@ -482,6 +483,160 @@
}
}
+func TestGenerateSubtaskFile(t *testing.T) {
+ mockProvider := NewMockLLMProvider([]string{})
+ service := NewSubtaskService(mockProvider, nil, []string{"backend"}, nil, "example", "repo", nil)
+
+ subtask := tm.SubtaskProposal{
+ Title: "Implement API endpoints",
+ Description: "Create REST API endpoints for user management",
+ Priority: tm.PriorityHigh,
+ AssignedTo: "backend",
+ EstimatedHours: 12,
+ Dependencies: []string{"0"},
+ RequiredSkills: []string{"go", "rest_api"},
+ }
+
+ taskID := "parent-task-subtask-1"
+ parentTaskID := "parent-task"
+
+ content := service.generateSubtaskFile(subtask, taskID, parentTaskID)
+
+ // Verify YAML frontmatter
+ if !strings.Contains(content, "id: parent-task-subtask-1") {
+ t.Error("Generated file should contain task ID in frontmatter")
+ }
+ if !strings.Contains(content, "title: Implement API endpoints") {
+ t.Error("Generated file should contain task title in frontmatter")
+ }
+ if !strings.Contains(content, "assignee: backend") {
+ t.Error("Generated file should contain assignee in frontmatter")
+ }
+ if !strings.Contains(content, "status: todo") {
+ t.Error("Generated file should have 'todo' status")
+ }
+ if !strings.Contains(content, "priority: high") {
+ t.Error("Generated file should contain priority in frontmatter")
+ }
+ if !strings.Contains(content, "parent_task_id: parent-task") {
+ t.Error("Generated file should contain parent task ID")
+ }
+ if !strings.Contains(content, "estimated_hours: 12") {
+ t.Error("Generated file should contain estimated hours")
+ }
+
+ // Verify dependencies are converted properly
+ if !strings.Contains(content, "dependencies:") {
+ t.Error("Generated file should contain dependencies section")
+ }
+ if !strings.Contains(content, "- parent-task-subtask-1") {
+ t.Error("Dependencies should be converted to subtask IDs")
+ }
+
+ // Verify required skills
+ if !strings.Contains(content, "required_skills:") {
+ t.Error("Generated file should contain required skills section")
+ }
+ if !strings.Contains(content, "- go") {
+ t.Error("Generated file should contain required skills")
+ }
+
+ // Verify markdown content
+ if !strings.Contains(content, "# Task Description") {
+ t.Error("Generated file should contain markdown task description")
+ }
+ if !strings.Contains(content, "Create REST API endpoints for user management") {
+ t.Error("Generated file should contain task description in body")
+ }
+}
+
+func TestUpdateParentTaskAsCompleted(t *testing.T) {
+ mockProvider := NewMockLLMProvider([]string{})
+ service := NewSubtaskService(mockProvider, nil, []string{"backend"}, nil, "example", "repo", nil)
+
+ // Create a temporary task file for testing
+ taskContent := `---
+id: test-task-123
+title: Test Task
+description: A test task for validation
+assignee: backend
+status: todo
+priority: high
+created_at: 2024-01-01T10:00:00Z
+updated_at: 2024-01-01T10:00:00Z
+completed_at: null
+---
+
+# Task Description
+
+A test task for validation
+`
+
+ // Create temporary file
+ tmpFile, err := os.CreateTemp("", "test-task-*.md")
+ if err != nil {
+ t.Fatalf("Failed to create temp file: %v", err)
+ }
+ defer os.Remove(tmpFile.Name())
+
+ if err := os.WriteFile(tmpFile.Name(), []byte(taskContent), 0644); err != nil {
+ t.Fatalf("Failed to write temp file: %v", err)
+ }
+
+ // Create analysis with subtasks
+ analysis := &tm.SubtaskAnalysis{
+ ParentTaskID: "test-task-123",
+ EstimatedTotalHours: 20,
+ Subtasks: []tm.SubtaskProposal{
+ {
+ Title: "Subtask 1",
+ AssignedTo: "backend",
+ },
+ {
+ Title: "Subtask 2",
+ AssignedTo: "frontend",
+ },
+ },
+ }
+
+ // Update the parent task
+ err = service.updateParentTaskAsCompleted(tmpFile.Name(), analysis)
+ if err != nil {
+ t.Fatalf("updateParentTaskAsCompleted failed: %v", err)
+ }
+
+ // Read the updated file
+ updatedContent, err := os.ReadFile(tmpFile.Name())
+ if err != nil {
+ t.Fatalf("Failed to read updated file: %v", err)
+ }
+
+ updatedString := string(updatedContent)
+
+ // Verify the status was changed to completed
+ if !strings.Contains(updatedString, "status: completed") {
+ t.Error("Updated file should contain 'status: completed'")
+ }
+
+ // Verify completed_at was set (should not be null)
+ if strings.Contains(updatedString, "completed_at: null") {
+ t.Error("Updated file should have completed_at timestamp, not null")
+ }
+
+ // Verify subtask information was added
+ if !strings.Contains(updatedString, "## Subtasks Created") {
+ t.Error("Updated file should contain subtasks information")
+ }
+
+ if !strings.Contains(updatedString, "test-task-123-subtask-1") {
+ t.Error("Updated file should reference created subtask IDs")
+ }
+
+ if !strings.Contains(updatedString, "**Total Estimated Hours:** 20") {
+ t.Error("Updated file should contain total estimated hours")
+ }
+}
+
func TestClose(t *testing.T) {
mockProvider := NewMockLLMProvider([]string{})
service := NewSubtaskService(mockProvider, nil, []string{"backend"}, nil, "example", "repo", nil)