package handlers import ( "bytes" "context" "os" "runtime" "slices" "strconv" "strings" "testing" "time" "github.com/google/uuid" "github.com/stormlightlabs/noteleaf/internal/models" "github.com/stormlightlabs/noteleaf/internal/shared" "github.com/stormlightlabs/noteleaf/internal/ui" ) func TestTaskHandler(t *testing.T) { ctx := context.Background() t.Run("New", func(t *testing.T) { t.Run("creates handler successfully", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() handler, err := NewTaskHandler() if err != nil { t.Fatalf("NewTaskHandler failed: %v", err) } if handler == nil { t.Fatal("Handler should not be nil") } defer handler.Close() if handler.db == nil { t.Error("Handler database should not be nil") } if handler.config == nil { t.Error("Handler config should not be nil") } if handler.repos == nil { t.Error("Handler repos should not be nil") } }) t.Run("handles database initialization error", func(t *testing.T) { originalXDG := os.Getenv("XDG_CONFIG_HOME") originalHome := os.Getenv("HOME") if runtime.GOOS == "windows" { originalAppData := os.Getenv("APPDATA") os.Unsetenv("APPDATA") defer os.Setenv("APPDATA", originalAppData) } else { os.Unsetenv("XDG_CONFIG_HOME") os.Unsetenv("HOME") defer os.Setenv("XDG_CONFIG_HOME", originalXDG) defer os.Setenv("HOME", originalHome) } handler, err := NewTaskHandler() if err == nil { if handler != nil { handler.Close() } t.Error("Expected error when database initialization fails") } }) }) t.Run("Create", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() handler := CreateHandler(t, NewTaskHandler) t.Run("creates task successfully", func(t *testing.T) { desc := "Buy groceries and cook dinner" err := handler.Create(ctx, desc, "", "", "", "", "", "", "", "", "", "", []string{}) shared.AssertNoError(t, err, "CreateTask should succeed") tasks, err := handler.repos.Tasks.GetPending(ctx) shared.AssertNoError(t, err, "Failed to get pending tasks") if len(tasks) != 1 { t.Errorf("Expected 1 task, got %d", len(tasks)) } task := tasks[0] expectedDesc := "Buy groceries and cook dinner" if task.Description != expectedDesc { t.Errorf("Expected description '%s', got '%s'", expectedDesc, task.Description) } if task.Status != "pending" { t.Errorf("Expected status 'pending', got '%s'", task.Status) } if task.UUID == "" { t.Error("Task should have a UUID") } }) t.Run("fails with empty description", func(t *testing.T) { desc := "" err := handler.Create(ctx, desc, "", "", "", "", "", "", "", "", "", "", []string{}) shared.AssertError(t, err, "Expected error for empty description") shared.AssertContains(t, err.Error(), "task description required", "Error message should mention required description") }) t.Run("creates task with flags", func(t *testing.T) { description := "Task with flags" priority := "A" project := "test-project" due := "2024-12-31" tags := []string{"urgent", "work"} err := handler.Create(ctx, description, priority, project, "test-context", due, "", "", "", "", "", "", tags) if err != nil { t.Errorf("CreateTask with flags failed: %v", err) } tasks, err := handler.repos.Tasks.GetPending(ctx) if err != nil { t.Fatalf("Failed to get pending tasks: %v", err) } if len(tasks) < 1 { t.Errorf("Expected at least 1 task, got %d", len(tasks)) } var task *models.Task for _, t := range tasks { if t.Description == "Task with flags" { task = t break } } if task == nil { t.Fatal("Could not find created task") } if task.Priority != priority { t.Errorf("Expected priority '%s', got '%s'", priority, task.Priority) } if task.Project != project { t.Errorf("Expected project '%s', got '%s'", project, task.Project) } if task.Due == nil { t.Error("Expected due date to be set") } else if task.Due.Format("2006-01-02") != due { t.Errorf("Expected due date '%s', got '%s'", due, task.Due.Format("2006-01-02")) } if len(task.Tags) != len(tags) { t.Errorf("Expected %d tags, got %d", len(tags), len(task.Tags)) } else { for i, tag := range tags { if task.Tags[i] != tag { t.Errorf("Expected tag '%s' at index %d, got '%s'", tag, i, task.Tags[i]) } } } }) t.Run("fails with invalid due date format", func(t *testing.T) { desc := "Task with invalid date" invalidDue := "invalid-date" err := handler.Create(ctx, desc, "", "", "", invalidDue, "", "", "", "", "", "", []string{}) if err == nil { t.Error("Expected error for invalid due date format") } if !strings.Contains(err.Error(), "invalid due date format") { t.Errorf("Expected error about invalid date format, got: %v", err) } }) t.Run("fails when repository Create returns error", func(t *testing.T) { ctx, cancel := context.WithCancel(ctx) cancel() err := handler.Create(ctx, "Test task", "", "", "", "", "", "", "", "", "", "", []string{}) if err == nil { t.Error("Expected error when repository Create fails") } if !strings.Contains(err.Error(), "failed to create task") { t.Errorf("Expected 'failed to create task' error, got: %v", err) } }) }) t.Run("List", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() handler, err := NewTaskHandler() if err != nil { t.Fatalf("Failed to create handler: %v", err) } defer handler.Close() task1 := &models.Task{ UUID: uuid.New().String(), Description: "Task 1", Status: "pending", Priority: "A", Project: "work", } _, err = handler.repos.Tasks.Create(ctx, task1) if err != nil { t.Fatalf("Failed to create task1: %v", err) } task2 := &models.Task{ UUID: uuid.New().String(), Description: "Task 2", Status: "completed", } _, err = handler.repos.Tasks.Create(ctx, task2) if err != nil { t.Fatalf("Failed to create task2: %v", err) } t.Run("lists pending tasks by default (static mode)", func(t *testing.T) { err := handler.List(ctx, true, false, "", "", "", "", "") if err != nil { t.Errorf("ListTasks failed: %v", err) } }) t.Run("filters by status (static mode)", func(t *testing.T) { err := handler.List(ctx, true, false, "completed", "", "", "", "") if err != nil { t.Errorf("ListTasks with status filter failed: %v", err) } }) t.Run("filters by priority (static mode)", func(t *testing.T) { err := handler.List(ctx, true, false, "", "A", "", "", "") if err != nil { t.Errorf("ListTasks with priority filter failed: %v", err) } }) t.Run("filters by project (static mode)", func(t *testing.T) { err := handler.List(ctx, true, false, "", "", "work", "", "") if err != nil { t.Errorf("ListTasks with project filter failed: %v", err) } }) t.Run("show all tasks (static mode)", func(t *testing.T) { err := handler.List(ctx, true, true, "", "", "", "", "") if err != nil { t.Errorf("ListTasks with show all failed: %v", err) } }) t.Run("interactive mode path", func(t *testing.T) { if err := TestTaskInteractiveList(t, handler, false, "", "", ""); err != nil { t.Errorf("Interactive task list test failed: %v", err) } }) t.Run("interactive mode path with filters", func(t *testing.T) { if err := TestTaskInteractiveList(t, handler, false, "pending", "A", "work"); err != nil { t.Errorf("Interactive task list test with filters failed: %v", err) } }) t.Run("interactive mode path show all", func(t *testing.T) { if err := TestTaskInteractiveList(t, handler, true, "", "", ""); err != nil { t.Errorf("Interactive task list test with show all failed: %v", err) } }) }) t.Run("Update", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() handler, err := NewTaskHandler() if err != nil { t.Fatalf("Failed to create handler: %v", err) } defer handler.Close() task := &models.Task{ UUID: uuid.New().String(), Description: "Original description", Status: "pending", } id, err := handler.repos.Tasks.Create(ctx, task) if err != nil { t.Fatalf("Failed to create task: %v", err) } t.Run("updates task by ID", func(t *testing.T) { taskID := strconv.FormatInt(id, 10) err := handler.Update(ctx, taskID, "Updated description", "", "", "", "", "", "", "", "", []string{}, []string{}, "", "") if err != nil { t.Errorf("UpdateTask failed: %v", err) } updatedTask, err := handler.repos.Tasks.Get(ctx, id) if err != nil { t.Fatalf("Failed to get updated task: %v", err) } if updatedTask.Description != "Updated description" { t.Errorf("Expected description 'Updated description', got '%s'", updatedTask.Description) } }) t.Run("updates task by UUID", func(t *testing.T) { taskID := task.UUID err := handler.Update(ctx, taskID, "", "completed", "", "", "", "", "", "", "", []string{}, []string{}, "", "") if err != nil { t.Errorf("UpdateTask by UUID failed: %v", err) } updatedTask, err := handler.repos.Tasks.GetByUUID(ctx, task.UUID) if err != nil { t.Fatalf("Failed to get updated task by UUID: %v", err) } if updatedTask.Status != "completed" { t.Errorf("Expected status 'completed', got '%s'", updatedTask.Status) } }) t.Run("updates multiple fields", func(t *testing.T) { taskID := strconv.FormatInt(id, 10) err := handler.Update(ctx, taskID, "Multiple updates", "", "B", "test", "office", "2024-12-31", "", "", "", []string{}, []string{}, "", "") if err != nil { t.Errorf("UpdateTask with multiple fields failed: %v", err) } updatedTask, err := handler.repos.Tasks.Get(ctx, id) if err != nil { t.Fatalf("Failed to get updated task: %v", err) } if updatedTask.Description != "Multiple updates" { t.Errorf("Expected description 'Multiple updates', got '%s'", updatedTask.Description) } if updatedTask.Priority != "B" { t.Errorf("Expected priority 'B', got '%s'", updatedTask.Priority) } if updatedTask.Project != "test" { t.Errorf("Expected project 'test', got '%s'", updatedTask.Project) } if updatedTask.Due == nil { t.Error("Expected due date to be set") } }) t.Run("adds and removes tags", func(t *testing.T) { taskID := strconv.FormatInt(id, 10) err := handler.Update(ctx, taskID, "", "", "", "", "", "", "", "", "", []string{"work", "urgent"}, []string{}, "", "") if err != nil { t.Errorf("UpdateTask with add tags failed: %v", err) } updatedTask, err := handler.repos.Tasks.Get(ctx, id) if err != nil { t.Fatalf("Failed to get updated task: %v", err) } if len(updatedTask.Tags) != 2 { t.Errorf("Expected 2 tags, got %d", len(updatedTask.Tags)) } taskID = strconv.FormatInt(id, 10) err = handler.Update(ctx, taskID, "", "", "", "", "", "", "", "", "", []string{}, []string{"urgent"}, "", "") if err != nil { t.Errorf("UpdateTask with remove tag failed: %v", err) } updatedTask, err = handler.repos.Tasks.Get(ctx, id) if err != nil { t.Fatalf("Failed to get updated task: %v", err) } if len(updatedTask.Tags) != 1 { t.Errorf("Expected 1 tag after removal, got %d", len(updatedTask.Tags)) } if updatedTask.Tags[0] != "work" { t.Errorf("Expected remaining tag 'work', got '%s'", updatedTask.Tags[0]) } }) t.Run("fails with missing task ID", func(t *testing.T) { err := handler.Update(ctx, "", "", "", "", "", "", "", "", "", "", []string{}, []string{}, "", "") if err == nil { t.Error("Expected error for missing task ID") } if !strings.Contains(err.Error(), "failed to find task") { t.Errorf("Expected error about task not found, got: %v", err) } }) t.Run("fails with invalid task ID", func(t *testing.T) { taskID := "99999" err := handler.Update(ctx, taskID, "test", "", "", "", "", "", "", "", "", []string{}, []string{}, "", "") if err == nil { t.Error("Expected error for invalid task ID") } if !strings.Contains(err.Error(), "failed to find task") { t.Errorf("Expected error about task not found, got: %v", err) } }) t.Run("fails when repository Get fails", func(t *testing.T) { cancelCtx, cancel := context.WithCancel(context.Background()) cancel() err := handler.Update(cancelCtx, "1", "test", "", "", "", "", "", "", "", "", []string{}, []string{}, "", "") if err == nil { t.Error("Expected error when repository Get fails") } if !strings.Contains(err.Error(), "failed to find task") { t.Errorf("Expected 'failed to find task' error, got: %v", err) } }) t.Run("fails when repository operations fail with canceled context", func(t *testing.T) { task := &models.Task{ UUID: uuid.New().String(), Description: "Test task", Status: "pending", } id, err := handler.repos.Tasks.Create(ctx, task) if err != nil { t.Fatalf("Failed to create task: %v", err) } cancelCtx, cancel := context.WithCancel(context.Background()) cancel() taskID := strconv.FormatInt(id, 10) err = handler.Update(cancelCtx, taskID, "Updated", "", "", "", "", "", "", "", "", []string{}, []string{}, "", "") if err == nil { t.Error("Expected error with canceled context") } }) }) t.Run("Delete", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() handler := CreateHandler(t, NewTaskHandler) task := &models.Task{ UUID: uuid.New().String(), Description: "Task to delete", Status: "pending", } id, err := handler.repos.Tasks.Create(ctx, task) if err != nil { t.Fatalf("Failed to create task: %v", err) } t.Run("deletes task by ID", func(t *testing.T) { args := []string{strconv.FormatInt(id, 10)} err := handler.Delete(ctx, args) if err != nil { t.Errorf("DeleteTask failed: %v", err) } _, err = handler.repos.Tasks.Get(ctx, id) if err == nil { t.Error("Expected error when getting deleted task") } }) t.Run("deletes task by UUID", func(t *testing.T) { task2 := &models.Task{ UUID: uuid.New().String(), Description: "Task to delete by UUID", Status: "pending", } _, err := handler.repos.Tasks.Create(ctx, task2) if err != nil { t.Fatalf("Failed to create task2: %v", err) } args := []string{task2.UUID} err = handler.Delete(ctx, args) if err != nil { t.Errorf("DeleteTask by UUID failed: %v", err) } _, err = handler.repos.Tasks.GetByUUID(ctx, task2.UUID) if err == nil { t.Error("Expected error when getting deleted task by UUID") } }) t.Run("fails with missing task ID", func(t *testing.T) { args := []string{} err := handler.Delete(ctx, args) if err == nil { t.Error("Expected error for missing task ID") } if !strings.Contains(err.Error(), "task ID required") { t.Errorf("Expected error about required task ID, got: %v", err) } }) t.Run("fails with invalid task ID", func(t *testing.T) { args := []string{"99999"} err := handler.Delete(ctx, args) if err == nil { t.Error("Expected error for invalid task ID") } if !strings.Contains(err.Error(), "failed to find task") { t.Errorf("Expected error about task not found, got: %v", err) } }) }) t.Run("View", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() handler, err := NewTaskHandler() if err != nil { t.Fatalf("Failed to create handler: %v", err) } defer handler.Close() now := time.Now() task := &models.Task{ UUID: uuid.New().String(), Description: "Task to view", Status: "pending", Priority: "A", Project: "test", Tags: []string{"work", "important"}, Entry: now, Modified: now, } id, err := handler.repos.Tasks.Create(ctx, task) if err != nil { t.Fatalf("Failed to create task: %v", err) } t.Run("views task by ID", func(t *testing.T) { args := []string{strconv.FormatInt(id, 10)} err := handler.View(ctx, args, "detailed", false, false) if err != nil { t.Errorf("ViewTask failed: %v", err) } }) t.Run("views task by UUID", func(t *testing.T) { args := []string{task.UUID} err := handler.View(ctx, args, "detailed", false, false) if err != nil { t.Errorf("ViewTask by UUID failed: %v", err) } }) t.Run("fails with missing task ID", func(t *testing.T) { args := []string{} err := handler.View(ctx, args, "detailed", false, false) if err == nil { t.Error("Expected error for missing task ID") } if !strings.Contains(err.Error(), "task ID required") { t.Errorf("Expected error about required task ID, got: %v", err) } }) t.Run("fails with invalid task ID", func(t *testing.T) { args := []string{"99999"} err := handler.View(ctx, args, "detailed", false, false) if err == nil { t.Error("Expected error for invalid task ID") } if !strings.Contains(err.Error(), "failed to find task") { t.Errorf("Expected error about task not found, got: %v", err) } }) t.Run("uses brief format", func(t *testing.T) { args := []string{strconv.FormatInt(id, 10)} err := handler.View(ctx, args, "brief", false, false) if err != nil { t.Errorf("ViewTask with brief format failed: %v", err) } }) t.Run("hides metadata", func(t *testing.T) { args := []string{strconv.FormatInt(id, 10)} err := handler.View(ctx, args, "detailed", false, true) if err != nil { t.Errorf("ViewTask with no-metadata failed: %v", err) } }) t.Run("outputs JSON", func(t *testing.T) { args := []string{strconv.FormatInt(id, 10)} err := handler.View(ctx, args, "detailed", true, false) if err != nil { t.Errorf("ViewTask with JSON output failed: %v", err) } }) }) t.Run("Done", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() handler, err := NewTaskHandler() if err != nil { t.Fatalf("Failed to create handler: %v", err) } defer handler.Close() task := &models.Task{ UUID: uuid.New().String(), Description: "Task to complete", Status: "pending", } id, err := handler.repos.Tasks.Create(ctx, task) if err != nil { t.Fatalf("Failed to create task: %v", err) } t.Run("marks task as done by ID", func(t *testing.T) { args := []string{strconv.FormatInt(id, 10)} err := handler.Done(ctx, args) if err != nil { t.Errorf("DoneTask failed: %v", err) } completedTask, err := handler.repos.Tasks.Get(ctx, id) if err != nil { t.Fatalf("Failed to get completed task: %v", err) } if completedTask.Status != "completed" { t.Errorf("Expected status 'completed', got '%s'", completedTask.Status) } if completedTask.End == nil { t.Error("Expected end time to be set") } }) t.Run("handles already completed task", func(t *testing.T) { task2 := &models.Task{ UUID: uuid.New().String(), Description: "Already completed task", Status: "completed", } id2, err := handler.repos.Tasks.Create(ctx, task2) if err != nil { t.Fatalf("Failed to create task2: %v", err) } args := []string{strconv.FormatInt(id2, 10)} err = handler.Done(ctx, args) if err != nil { t.Errorf("DoneTask on completed task failed: %v", err) } }) t.Run("marks task as done by UUID", func(t *testing.T) { task3 := &models.Task{ UUID: uuid.New().String(), Description: "Task to complete by UUID", Status: "pending", } _, err := handler.repos.Tasks.Create(ctx, task3) if err != nil { t.Fatalf("Failed to create task3: %v", err) } args := []string{task3.UUID} err = handler.Done(ctx, args) if err != nil { t.Errorf("DoneTask by UUID failed: %v", err) } completedTask, err := handler.repos.Tasks.GetByUUID(ctx, task3.UUID) if err != nil { t.Fatalf("Failed to get completed task by UUID: %v", err) } if completedTask.Status != "completed" { t.Errorf("Expected status 'completed', got '%s'", completedTask.Status) } if completedTask.End == nil { t.Error("Expected end time to be set") } }) t.Run("fails with missing task ID", func(t *testing.T) { args := []string{} err := handler.Done(ctx, args) if err == nil { t.Error("Expected error for missing task ID") } if !strings.Contains(err.Error(), "task ID required") { t.Errorf("Expected error about required task ID, got: %v", err) } }) t.Run("fails with invalid task ID", func(t *testing.T) { args := []string{"99999"} err := handler.Done(ctx, args) if err == nil { t.Error("Expected error for invalid task ID") } if !strings.Contains(err.Error(), "failed to find task") { t.Errorf("Expected error about task not found, got: %v", err) } }) }) t.Run("Helper", func(t *testing.T) { t.Run("removeString function", func(t *testing.T) { slice := []string{"a", "b", "c", "b"} result := removeString(slice, "b") if len(result) != 2 { t.Errorf("Expected 2 items after removing 'b', got %d", len(result)) } if slices.Contains(result, "b") { t.Error("Expected 'b' to be removed from slice") } if !slices.Contains(result, "a") || !slices.Contains(result, "c") { t.Error("Expected 'a' and 'c' to remain in slice") } }) t.Run("parseDescription extracts project", func(t *testing.T) { parsed := parseDescription("Buy groceries +shopping") if parsed.Description != "Buy groceries" { t.Errorf("Expected description 'Buy groceries', got '%s'", parsed.Description) } if parsed.Project != "shopping" { t.Errorf("Expected project 'shopping', got '%s'", parsed.Project) } }) t.Run("parseDescription extracts context", func(t *testing.T) { parsed := parseDescription("Call boss @work") if parsed.Description != "Call boss" { t.Errorf("Expected description 'Call boss', got '%s'", parsed.Description) } if parsed.Context != "work" { t.Errorf("Expected context 'work', got '%s'", parsed.Context) } }) t.Run("parseDescription extracts tags", func(t *testing.T) { parsed := parseDescription("Fix bug #urgent #backend") if parsed.Description != "Fix bug" { t.Errorf("Expected description 'Fix bug', got '%s'", parsed.Description) } if len(parsed.Tags) != 2 { t.Errorf("Expected 2 tags, got %d", len(parsed.Tags)) } if parsed.Tags[0] != "urgent" || parsed.Tags[1] != "backend" { t.Errorf("Expected tags 'urgent' and 'backend', got %v", parsed.Tags) } }) t.Run("parseDescription extracts due date", func(t *testing.T) { parsed := parseDescription("Submit report due:2024-12-31") if parsed.Description != "Submit report" { t.Errorf("Expected description 'Submit report', got '%s'", parsed.Description) } if parsed.Due != "2024-12-31" { t.Errorf("Expected due '2024-12-31', got '%s'", parsed.Due) } }) t.Run("parseDescription extracts recurrence", func(t *testing.T) { parsed := parseDescription("Weekly meeting recur:FREQ=WEEKLY") if parsed.Description != "Weekly meeting" { t.Errorf("Expected description 'Weekly meeting', got '%s'", parsed.Description) } if parsed.Recur != "FREQ=WEEKLY" { t.Errorf("Expected recur 'FREQ=WEEKLY', got '%s'", parsed.Recur) } }) t.Run("parseDescription extracts until date", func(t *testing.T) { parsed := parseDescription("Daily standup until:2024-12-31") if parsed.Description != "Daily standup" { t.Errorf("Expected description 'Daily standup', got '%s'", parsed.Description) } if parsed.Until != "2024-12-31" { t.Errorf("Expected until '2024-12-31', got '%s'", parsed.Until) } }) t.Run("parseDescription extracts parent UUID", func(t *testing.T) { parentUUID := "550e8400-e29b-41d4-a716-446655440000" text := "Subtask parent:" + parentUUID parsed := parseDescription(text) if parsed.Description != "Subtask" { t.Errorf("Expected description 'Subtask', got '%s'", parsed.Description) } if parsed.ParentUUID != parentUUID { t.Errorf("Expected parent UUID '%s', got '%s'", parentUUID, parsed.ParentUUID) } }) t.Run("parseDescription extracts dependencies", func(t *testing.T) { uuid1 := "550e8400-e29b-41d4-a716-446655440000" uuid2 := "660e8400-e29b-41d4-a716-446655440001" text := "Task with deps depends:" + uuid1 + "," + uuid2 parsed := parseDescription(text) if parsed.Description != "Task with deps" { t.Errorf("Expected description 'Task with deps', got '%s'", parsed.Description) } if len(parsed.DependsOn) != 2 { t.Errorf("Expected 2 dependencies, got %d", len(parsed.DependsOn)) } if parsed.DependsOn[0] != uuid1 || parsed.DependsOn[1] != uuid2 { t.Errorf("Expected dependencies [%s, %s], got %v", uuid1, uuid2, parsed.DependsOn) } }) t.Run("parseDescription extracts all metadata", func(t *testing.T) { text := "Complex task +project @context #tag1 #tag2 due:2024-12-31 recur:FREQ=DAILY until:2025-01-31" parsed := parseDescription(text) if parsed.Description != "Complex task" { t.Errorf("Expected description 'Complex task', got '%s'", parsed.Description) } if parsed.Project != "project" { t.Errorf("Expected project 'project', got '%s'", parsed.Project) } if parsed.Context != "context" { t.Errorf("Expected context 'context', got '%s'", parsed.Context) } if len(parsed.Tags) != 2 { t.Errorf("Expected 2 tags, got %d", len(parsed.Tags)) } if parsed.Due != "2024-12-31" { t.Errorf("Expected due '2024-12-31', got '%s'", parsed.Due) } if parsed.Recur != "FREQ=DAILY" { t.Errorf("Expected recur 'FREQ=DAILY', got '%s'", parsed.Recur) } if parsed.Until != "2025-01-31" { t.Errorf("Expected until '2025-01-31', got '%s'", parsed.Until) } }) t.Run("parseDescription handles plain text without metadata", func(t *testing.T) { parsed := parseDescription("Just a simple task") if parsed.Description != "Just a simple task" { t.Errorf("Expected description 'Just a simple task', got '%s'", parsed.Description) } if parsed.Project != "" { t.Errorf("Expected empty project, got '%s'", parsed.Project) } if parsed.Context != "" { t.Errorf("Expected empty context, got '%s'", parsed.Context) } if len(parsed.Tags) != 0 { t.Errorf("Expected no tags, got %d", len(parsed.Tags)) } }) }) t.Run("Print", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() handler, err := NewTaskHandler() if err != nil { t.Fatalf("Failed to create handler: %v", err) } defer handler.Close() now := time.Now() due := now.Add(24 * time.Hour) task := &models.Task{ ID: 1, UUID: uuid.New().String(), Description: "Test task", Status: "pending", Priority: "A", Project: "test", Tags: []string{"work", "urgent"}, Due: &due, Entry: now, Modified: now, } t.Run("printTask outputs basic fields", func(t *testing.T) { var buf bytes.Buffer oldStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w outputChan := make(chan string, 1) go func() { buf.ReadFrom(r) outputChan <- buf.String() }() printTask(task) w.Close() os.Stdout = oldStdout output := <-outputChan if !strings.Contains(output, "Test task") { t.Error("Output should contain task description") } if !strings.Contains(output, "[A]") { t.Error("Output should contain priority") } if !strings.Contains(output, "+test") { t.Error("Output should contain project") } }) t.Run("printTask outputs context", func(t *testing.T) { taskWithContext := &models.Task{ ID: 1, UUID: uuid.New().String(), Description: "Test task", Status: "pending", Context: "work", } var buf bytes.Buffer oldStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w outputChan := make(chan string, 1) go func() { buf.ReadFrom(r) outputChan <- buf.String() }() printTask(taskWithContext) w.Close() os.Stdout = oldStdout output := <-outputChan if !strings.Contains(output, "@work") { t.Error("Output should contain context") } }) t.Run("printTask outputs recur indicator", func(t *testing.T) { taskWithRecur := &models.Task{ ID: 1, UUID: uuid.New().String(), Description: "Recurring task", Status: "pending", Recur: "FREQ=DAILY", } var buf bytes.Buffer oldStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w outputChan := make(chan string, 1) go func() { buf.ReadFrom(r) outputChan <- buf.String() }() printTask(taskWithRecur) w.Close() os.Stdout = oldStdout output := <-outputChan if !strings.Contains(output, "\u21bb") { t.Error("Output should contain recurrence indicator") } }) t.Run("printTask outputs dependency count", func(t *testing.T) { taskWithDeps := &models.Task{ ID: 1, UUID: uuid.New().String(), Description: "Task with dependencies", Status: "pending", DependsOn: []string{"uuid1", "uuid2"}, } var buf bytes.Buffer oldStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w outputChan := make(chan string, 1) go func() { buf.ReadFrom(r) outputChan <- buf.String() }() printTask(taskWithDeps) w.Close() os.Stdout = oldStdout output := <-outputChan if !strings.Contains(output, "\u29372") { t.Error("Output should contain dependency count") } }) t.Run("printTaskDetail outputs context", func(t *testing.T) { taskWithContext := &models.Task{ ID: 1, UUID: uuid.New().String(), Description: "Test task", Status: "pending", Context: "office", Entry: now, Modified: now, } var buf bytes.Buffer oldStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w outputChan := make(chan string, 1) go func() { buf.ReadFrom(r) outputChan <- buf.String() }() printTaskDetail(taskWithContext, false) w.Close() os.Stdout = oldStdout output := <-outputChan if !strings.Contains(output, "Context: office") { t.Error("Output should contain context field") } }) t.Run("printTaskDetail outputs recurrence", func(t *testing.T) { taskWithRecur := &models.Task{ ID: 1, UUID: uuid.New().String(), Description: "Recurring task", Status: "pending", Recur: "FREQ=WEEKLY", Entry: now, Modified: now, } var buf bytes.Buffer oldStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w outputChan := make(chan string, 1) go func() { buf.ReadFrom(r) outputChan <- buf.String() }() printTaskDetail(taskWithRecur, false) w.Close() os.Stdout = oldStdout output := <-outputChan if !strings.Contains(output, "Recurrence: FREQ=WEEKLY") { t.Error("Output should contain recurrence field") } }) t.Run("printTaskDetail outputs until date", func(t *testing.T) { until := now.Add(30 * 24 * time.Hour) taskWithUntil := &models.Task{ ID: 1, UUID: uuid.New().String(), Description: "Task with until", Status: "pending", Until: &until, Entry: now, Modified: now, } var buf bytes.Buffer oldStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w outputChan := make(chan string, 1) go func() { buf.ReadFrom(r) outputChan <- buf.String() }() printTaskDetail(taskWithUntil, false) w.Close() os.Stdout = oldStdout output := <-outputChan if !strings.Contains(output, "Recur Until:") { t.Error("Output should contain recur until field") } }) t.Run("printTaskDetail outputs parent UUID", func(t *testing.T) { parentUUID := "550e8400-e29b-41d4-a716-446655440000" taskWithParent := &models.Task{ ID: 1, UUID: uuid.New().String(), Description: "Subtask", Status: "pending", ParentUUID: &parentUUID, Entry: now, Modified: now, } var buf bytes.Buffer oldStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w outputChan := make(chan string, 1) go func() { buf.ReadFrom(r) outputChan <- buf.String() }() printTaskDetail(taskWithParent, false) w.Close() os.Stdout = oldStdout output := <-outputChan if !strings.Contains(output, "Parent Task:") { t.Error("Output should contain parent task field") } if !strings.Contains(output, parentUUID) { t.Error("Output should contain parent UUID") } }) t.Run("printTaskDetail outputs dependencies", func(t *testing.T) { taskWithDeps := &models.Task{ ID: 1, UUID: uuid.New().String(), Description: "Task with deps", Status: "pending", DependsOn: []string{"uuid1", "uuid2"}, Entry: now, Modified: now, } var buf bytes.Buffer oldStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w outputChan := make(chan string, 1) go func() { buf.ReadFrom(r) outputChan <- buf.String() }() printTaskDetail(taskWithDeps, false) w.Close() os.Stdout = oldStdout output := <-outputChan if !strings.Contains(output, "Depends On:") { t.Error("Output should contain depends on field") } if !strings.Contains(output, "uuid1") || !strings.Contains(output, "uuid2") { t.Error("Output should contain dependency UUIDs") } }) t.Run("printTaskDetail outputs start time", func(t *testing.T) { start := now.Add(-1 * time.Hour) taskWithStart := &models.Task{ ID: 1, UUID: uuid.New().String(), Description: "Started task", Status: "pending", Start: &start, Entry: now, Modified: now, } var buf bytes.Buffer oldStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w outputChan := make(chan string, 1) go func() { buf.ReadFrom(r) outputChan <- buf.String() }() printTaskDetail(taskWithStart, false) w.Close() os.Stdout = oldStdout output := <-outputChan if !strings.Contains(output, "Started:") { t.Error("Output should contain started field") } }) t.Run("printTaskDetail outputs end time", func(t *testing.T) { end := now.Add(-1 * time.Hour) taskWithEnd := &models.Task{ ID: 1, UUID: uuid.New().String(), Description: "Completed task", Status: "completed", End: &end, Entry: now, Modified: now, } var buf bytes.Buffer oldStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w outputChan := make(chan string, 1) go func() { buf.ReadFrom(r) outputChan <- buf.String() }() printTaskDetail(taskWithEnd, false) w.Close() os.Stdout = oldStdout output := <-outputChan if !strings.Contains(output, "Completed:") { t.Error("Output should contain completed field") } }) t.Run("printTaskDetail outputs annotations", func(t *testing.T) { taskWithAnnotations := &models.Task{ ID: 1, UUID: uuid.New().String(), Description: "Task with notes", Status: "pending", Annotations: []string{"Note 1", "Note 2"}, Entry: now, Modified: now, } var buf bytes.Buffer oldStdout := os.Stdout r, w, _ := os.Pipe() os.Stdout = w outputChan := make(chan string, 1) go func() { buf.ReadFrom(r) outputChan <- buf.String() }() printTaskDetail(taskWithAnnotations, false) w.Close() os.Stdout = oldStdout output := <-outputChan if !strings.Contains(output, "Annotations:") { t.Error("Output should contain annotations field") } if !strings.Contains(output, "Note 1") || !strings.Contains(output, "Note 2") { t.Error("Output should contain annotation texts") } }) }) t.Run("ListProjects", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() handler, err := NewTaskHandler() if err != nil { t.Fatalf("Failed to create handler: %v", err) } defer handler.Close() tasks := []*models.Task{ {UUID: uuid.New().String(), Description: "Task 1", Status: "pending", Project: "web-app"}, {UUID: uuid.New().String(), Description: "Task 2", Status: "completed", Project: "web-app"}, {UUID: uuid.New().String(), Description: "Task 3", Status: "pending", Project: "mobile-app"}, {UUID: uuid.New().String(), Description: "Task 4", Status: "pending", Project: ""}, } for _, task := range tasks { _, err := handler.repos.Tasks.Create(ctx, task) if err != nil { t.Fatalf("Failed to create task: %v", err) } } t.Run("lists projects successfully", func(t *testing.T) { err := handler.ListProjects(ctx, true) if err != nil { t.Errorf("ListProjects failed: %v", err) } }) t.Run("returns no projects when none exist", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() err := handler.ListProjects(ctx, true) if err != nil { t.Errorf("ListProjects with no projects failed: %v", err) } }) t.Run("fails when repository List fails", func(t *testing.T) { cancelCtx, cancel := context.WithCancel(context.Background()) cancel() err := handler.ListProjects(cancelCtx, true) if err == nil { t.Error("Expected error when repository List fails") } if !strings.Contains(err.Error(), "failed to list tasks for projects") { t.Errorf("Expected 'failed to list tasks for projects' error, got: %v", err) } }) }) t.Run("ListTags", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() handler, err := NewTaskHandler() if err != nil { t.Fatalf("Failed to create handler: %v", err) } defer handler.Close() tasks := []*models.Task{ {UUID: uuid.New().String(), Description: "Task 1", Status: "pending", Tags: []string{"frontend", "urgent"}}, {UUID: uuid.New().String(), Description: "Task 2", Status: "completed", Tags: []string{"backend", "database"}}, {UUID: uuid.New().String(), Description: "Task 3", Status: "pending", Tags: []string{"frontend", "ios"}}, {UUID: uuid.New().String(), Description: "Task 4", Status: "pending", Tags: []string{}}, } for _, task := range tasks { _, err := handler.repos.Tasks.Create(ctx, task) if err != nil { t.Fatalf("Failed to create task: %v", err) } } t.Run("lists tags successfully", func(t *testing.T) { err := handler.ListTags(ctx, true) if err != nil { t.Errorf("ListTags failed: %v", err) } }) t.Run("returns no tags when none exist", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() err := handler.ListTags(ctx, true) if err != nil { t.Errorf("ListTags with no tags failed: %v", err) } }) t.Run("fails when repository List fails", func(t *testing.T) { cancelCtx, cancel := context.WithCancel(context.Background()) cancel() err := handler.ListTags(cancelCtx, true) if err == nil { t.Error("Expected error when repository List fails") } if !strings.Contains(err.Error(), "failed to list tasks for tags") { t.Errorf("Expected 'failed to list tasks for tags' error, got: %v", err) } }) }) t.Run("Pluralize", func(t *testing.T) { t.Run("returns empty string for singular", func(t *testing.T) { result := pluralize(1) if result != "" { t.Errorf("Expected empty string for 1, got '%s'", result) } }) t.Run("returns 's' for plural", func(t *testing.T) { result := pluralize(0) if result != "s" { t.Errorf("Expected 's' for 0, got '%s'", result) } result = pluralize(2) if result != "s" { t.Errorf("Expected 's' for 2, got '%s'", result) } result = pluralize(10) if result != "s" { t.Errorf("Expected 's' for 10, got '%s'", result) } }) }) t.Run("InteractiveComponentsStatic", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() handler, err := NewTaskHandler() if err != nil { t.Fatalf("Failed to create task handler: %v", err) } defer handler.Close() err = handler.Create(ctx, "Test Task 1", "high", "test-project", "test-context", "", "", "", "", "", "", "", []string{"tag1"}) if err != nil { t.Fatalf("Failed to create test task: %v", err) } err = handler.Create(ctx, "Test Task 2", "medium", "test-project", "test-context", "", "", "", "", "", "", "", []string{"tag2"}) if err != nil { t.Fatalf("Failed to create test task: %v", err) } t.Run("taskListStaticMode", func(t *testing.T) { var output bytes.Buffer t.Run("lists all tasks", func(t *testing.T) { output.Reset() taskTable := ui.NewTaskListFromTable(handler.repos.Tasks, &output, os.Stdin, true, true, "", "", "") err := taskTable.Browse(ctx) if err != nil { t.Errorf("Static task list should succeed: %v", err) } if !strings.Contains(output.String(), "Test Task 1") { t.Error("Output should contain Test Task 1") } if !strings.Contains(output.String(), "Test Task 2") { t.Error("Output should contain Test Task 2") } }) t.Run("filters by status", func(t *testing.T) { output.Reset() taskTable := ui.NewTaskListFromTable(handler.repos.Tasks, &output, os.Stdin, true, false, "pending", "", "") err := taskTable.Browse(ctx) if err != nil { t.Errorf("Static task list with status filter should succeed: %v", err) } }) t.Run("filters by priority", func(t *testing.T) { output.Reset() taskTable := ui.NewTaskListFromTable(handler.repos.Tasks, &output, os.Stdin, true, false, "", "high", "") err := taskTable.Browse(ctx) if err != nil { t.Errorf("Static task list with priority filter should succeed: %v", err) } }) t.Run("filters by project", func(t *testing.T) { output.Reset() taskTable := ui.NewTaskListFromTable(handler.repos.Tasks, &output, os.Stdin, true, false, "", "", "test-project") err := taskTable.Browse(ctx) if err != nil { t.Errorf("Static task list with project filter should succeed: %v", err) } }) }) t.Run("projectListStaticMode", func(t *testing.T) { var output bytes.Buffer t.Run("lists projects", func(t *testing.T) { output.Reset() projectTable := ui.NewProjectListFromTable(handler.repos.Tasks, &output, os.Stdin, true) err := projectTable.Browse(ctx) if err != nil { t.Errorf("Static project list should succeed: %v", err) } if !strings.Contains(output.String(), "test-project") { t.Error("Output should contain test-project") } }) }) t.Run("tagListStaticMode", func(t *testing.T) { var output bytes.Buffer t.Run("lists tags", func(t *testing.T) { output.Reset() tagTable := ui.NewTagListFromTable(handler.repos.Tasks, &output, os.Stdin, true) err := tagTable.Browse(ctx) if err != nil { t.Errorf("Static tag list should succeed: %v", err) } if !strings.Contains(output.String(), "tag1") { t.Error("Output should contain tag1") } }) }) t.Run("contextListStaticMode", func(t *testing.T) { oldStdout := os.Stdout defer func() { os.Stdout = oldStdout }() r, w, _ := os.Pipe() os.Stdout = w outputChan := make(chan string, 1) go func() { var buf bytes.Buffer buf.ReadFrom(r) outputChan <- buf.String() }() t.Run("lists contexts with tasks", func(t *testing.T) { err := handler.listContextsStatic(ctx, false) w.Close() capturedOutput := <-outputChan if err != nil { t.Errorf("listContextsStatic should succeed: %v", err) } if !strings.Contains(capturedOutput, "test-context") { t.Error("Output should contain 'test-context' context") } }) r, w, _ = os.Pipe() os.Stdout = w go func() { var buf bytes.Buffer buf.ReadFrom(r) outputChan <- buf.String() }() t.Run("lists contexts with todo.txt format", func(t *testing.T) { err := handler.listContextsStatic(ctx, true) w.Close() capturedOutput := <-outputChan if err != nil { t.Errorf("listContextsStatic with todoTxt should succeed: %v", err) } if !strings.Contains(capturedOutput, "@test-context") { t.Error("Output should contain '@test-context' in todo.txt format") } }) t.Run("handles no contexts", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() handler2, err := NewTaskHandler() if err != nil { t.Fatalf("Failed to create handler: %v", err) } defer handler2.Close() r, w, _ = os.Pipe() os.Stdout = w go func() { var buf bytes.Buffer buf.ReadFrom(r) outputChan <- buf.String() }() err = handler2.listContextsStatic(ctx, false) w.Close() capturedOutput := <-outputChan if err != nil { t.Errorf("listContextsStatic with no contexts should succeed: %v", err) } if !strings.Contains(capturedOutput, "No contexts found") { t.Error("Output should contain 'No contexts found'") } }) t.Run("handles repository error", func(t *testing.T) { cancelCtx, cancel := context.WithCancel(ctx) cancel() err := handler.listContextsStatic(cancelCtx, false) if err == nil { t.Error("Expected error with cancelled context") } if !strings.Contains(err.Error(), "failed to list tasks for contexts") { t.Errorf("Expected specific error message, got: %v", err) } }) t.Run("counts tasks per context correctly", func(t *testing.T) { r, w, _ = os.Pipe() os.Stdout = w go func() { var buf bytes.Buffer buf.ReadFrom(r) outputChan <- buf.String() }() err := handler.listContextsStatic(ctx, false) w.Close() capturedOutput := <-outputChan if err != nil { t.Errorf("listContextsStatic should succeed: %v", err) } if !strings.Contains(capturedOutput, "test-context (2 tasks)") { t.Error("Output should show correct count for test-context context") } }) }) }) t.Run("ListContexts", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() handler, err := NewTaskHandler() if err != nil { t.Fatalf("Failed to create handler: %v", err) } defer handler.Close() tasks := []*models.Task{ {UUID: uuid.New().String(), Description: "Task with context 1", Status: "pending", Context: "test-context"}, {UUID: uuid.New().String(), Description: "Task with context 2", Status: "pending", Context: "work-context"}, {UUID: uuid.New().String(), Description: "Task without context", Status: "pending"}, } for _, task := range tasks { _, err := handler.repos.Tasks.Create(ctx, task) if err != nil { t.Fatalf("Failed to create task: %v", err) } } t.Run("lists contexts in static mode", func(t *testing.T) { err := handler.ListContexts(ctx, true) if err != nil { t.Errorf("ListContexts static mode failed: %v", err) } }) t.Run("lists contexts in interactive mode (falls back to static)", func(t *testing.T) { err := handler.ListContexts(ctx, false) if err != nil { t.Errorf("ListContexts interactive mode failed: %v", err) } }) t.Run("lists contexts with todoTxt flag true", func(t *testing.T) { err := handler.ListContexts(ctx, true, true) if err != nil { t.Errorf("ListContexts with todoTxt=true failed: %v", err) } }) t.Run("lists contexts with todoTxt flag false", func(t *testing.T) { err := handler.ListContexts(ctx, true, false) if err != nil { t.Errorf("ListContexts with todoTxt=false failed: %v", err) } }) t.Run("handles database error in static mode", func(t *testing.T) { cancelCtx, cancel := context.WithCancel(ctx) cancel() err := handler.ListContexts(cancelCtx, true) if err == nil { t.Error("Expected error with cancelled context in static mode") } if !strings.Contains(err.Error(), "failed to list tasks for contexts") { t.Errorf("Expected specific error message, got: %v", err) } }) t.Run("handles database error in interactive mode", func(t *testing.T) { cancelCtx, cancel := context.WithCancel(ctx) cancel() err := handler.ListContexts(cancelCtx, false) if err == nil { t.Error("Expected error with cancelled context in interactive mode") } if !strings.Contains(err.Error(), "failed to list tasks for contexts") { t.Errorf("Expected specific error message, got: %v", err) } }) t.Run("returns no contexts when none exist", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() handler_, err := NewTaskHandler() if err != nil { t.Fatalf("Failed to create handler: %v", err) } defer handler_.Close() err = handler_.ListContexts(ctx, true) if err != nil { t.Errorf("ListContexts with no contexts failed: %v", err) } }) }) t.Run("SetRecur", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() handler, err := NewTaskHandler() if err != nil { t.Fatalf("Failed to create handler: %v", err) } defer handler.Close() id, err := handler.repos.Tasks.Create(ctx, &models.Task{ UUID: uuid.New().String(), Description: "Test task", Status: "pending", }) if err != nil { t.Fatalf("Failed to create task: %v", err) } t.Run("sets recurrence rule", func(t *testing.T) { err := handler.SetRecur(ctx, strconv.FormatInt(id, 10), "FREQ=DAILY", "2025-12-31") if err != nil { t.Errorf("SetRecur failed: %v", err) } task, err := handler.repos.Tasks.Get(ctx, id) if err != nil { t.Fatalf("Failed to get task: %v", err) } if task.Recur != "FREQ=DAILY" { t.Errorf("Expected recur to be 'FREQ=DAILY', got '%s'", task.Recur) } if task.Until == nil { t.Error("Expected until to be set") } }) t.Run("handles invalid until date", func(t *testing.T) { err := handler.SetRecur(ctx, strconv.FormatInt(id, 10), "FREQ=WEEKLY", "invalid-date") if err == nil { t.Error("Expected error for invalid until date") } }) t.Run("fails when repository Get fails", func(t *testing.T) { cancelCtx, cancel := context.WithCancel(context.Background()) cancel() err := handler.SetRecur(cancelCtx, "1", "FREQ=DAILY", "") if err == nil { t.Error("Expected error when repository Get fails") } if !strings.Contains(err.Error(), "failed to find task") { t.Errorf("Expected 'failed to find task' error, got: %v", err) } }) t.Run("fails with canceled context", func(t *testing.T) { task := &models.Task{ UUID: uuid.New().String(), Description: "Test task", Status: "pending", } id, err := handler.repos.Tasks.Create(ctx, task) if err != nil { t.Fatalf("Failed to create task: %v", err) } cancelCtx, cancel := context.WithCancel(context.Background()) cancel() err = handler.SetRecur(cancelCtx, strconv.FormatInt(id, 10), "FREQ=DAILY", "") if err == nil { t.Error("Expected error with canceled context") } }) }) t.Run("ClearRecur", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() handler, err := NewTaskHandler() if err != nil { t.Fatalf("Failed to create handler: %v", err) } defer handler.Close() until := time.Now() id, err := handler.repos.Tasks.Create(ctx, &models.Task{ UUID: uuid.New().String(), Description: "Test task", Status: "pending", Recur: "FREQ=DAILY", Until: &until, }) if err != nil { t.Fatalf("Failed to create task: %v", err) } if err = handler.ClearRecur(ctx, strconv.FormatInt(id, 10)); err != nil { t.Errorf("ClearRecur failed: %v", err) } task, err := handler.repos.Tasks.Get(ctx, id) if err != nil { t.Fatalf("Failed to get task: %v", err) } if task.Recur != "" { t.Errorf("Expected recur to be cleared, got '%s'", task.Recur) } if task.Until != nil { t.Error("Expected until to be cleared") } t.Run("fails when repository Get fails", func(t *testing.T) { cancelCtx, cancel := context.WithCancel(context.Background()) cancel() err := handler.ClearRecur(cancelCtx, "1") if err == nil { t.Error("Expected error when repository Get fails") } if !strings.Contains(err.Error(), "failed to find task") { t.Errorf("Expected 'failed to find task' error, got: %v", err) } }) t.Run("fails with canceled context", func(t *testing.T) { task := &models.Task{ UUID: uuid.New().String(), Description: "Test task", Status: "pending", Recur: "FREQ=DAILY", } id, err := handler.repos.Tasks.Create(ctx, task) if err != nil { t.Fatalf("Failed to create task: %v", err) } cancelCtx, cancel := context.WithCancel(context.Background()) cancel() if err = handler.ClearRecur(cancelCtx, strconv.FormatInt(id, 10)); err == nil { t.Error("Expected error with canceled context") } }) }) t.Run("ShowRecur", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() handler, err := NewTaskHandler() if err != nil { t.Fatalf("Failed to create handler: %v", err) } defer handler.Close() until := time.Now() id, err := handler.repos.Tasks.Create(ctx, &models.Task{ UUID: uuid.New().String(), Description: "Test task", Status: "pending", Recur: "FREQ=DAILY", Until: &until, }) if err != nil { t.Fatalf("Failed to create task: %v", err) } err = handler.ShowRecur(ctx, strconv.FormatInt(id, 10)) if err != nil { t.Errorf("ShowRecur failed: %v", err) } t.Run("fails when repository Get fails", func(t *testing.T) { cancelCtx, cancel := context.WithCancel(context.Background()) cancel() err := handler.ShowRecur(cancelCtx, "1") if err == nil { t.Error("Expected error when repository Get fails") } if !strings.Contains(err.Error(), "failed to find task") { t.Errorf("Expected 'failed to find task' error, got: %v", err) } }) }) t.Run("AddDep", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() handler, err := NewTaskHandler() if err != nil { t.Fatalf("Failed to create handler: %v", err) } defer handler.Close() task1UUID := uuid.New().String() task2UUID := uuid.New().String() id1, err := handler.repos.Tasks.Create(ctx, &models.Task{ UUID: task1UUID, Description: "Task 1", Status: "pending", }) if err != nil { t.Fatalf("Failed to create task 1: %v", err) } if _, err = handler.repos.Tasks.Create(ctx, &models.Task{ UUID: task2UUID, Description: "Task 2", Status: "pending", }); err != nil { t.Fatalf("Failed to create task 2: %v", err) } err = handler.AddDep(ctx, strconv.FormatInt(id1, 10), task2UUID) if err != nil { t.Errorf("AddDep failed: %v", err) } task, err := handler.repos.Tasks.Get(ctx, id1) if err != nil { t.Fatalf("Failed to get task: %v", err) } if len(task.DependsOn) != 1 { t.Errorf("Expected 1 dependency, got %d", len(task.DependsOn)) } if task.DependsOn[0] != task2UUID { t.Errorf("Expected dependency to be '%s', got '%s'", task2UUID, task.DependsOn[0]) } }) t.Run("RemoveDep", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() handler, err := NewTaskHandler() if err != nil { t.Fatalf("Failed to create handler: %v", err) } defer handler.Close() task1UUID := uuid.New().String() task2UUID := uuid.New().String() _, err = handler.repos.Tasks.Create(ctx, &models.Task{ UUID: task2UUID, Description: "Task 2", Status: "pending", }) if err != nil { t.Fatalf("Failed to create task 2: %v", err) } id1, err := handler.repos.Tasks.Create(ctx, &models.Task{ UUID: task1UUID, Description: "Task 1", Status: "pending", DependsOn: []string{task2UUID}, }) if err != nil { t.Fatalf("Failed to create task 1: %v", err) } err = handler.RemoveDep(ctx, strconv.FormatInt(id1, 10), task2UUID) if err != nil { t.Errorf("RemoveDep failed: %v", err) } task, err := handler.repos.Tasks.Get(ctx, id1) if err != nil { t.Fatalf("Failed to get task: %v", err) } if len(task.DependsOn) != 0 { t.Errorf("Expected 0 dependencies, got %d", len(task.DependsOn)) } }) t.Run("ListDeps", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() handler, err := NewTaskHandler() if err != nil { t.Fatalf("Failed to create handler: %v", err) } defer handler.Close() task1UUID := uuid.New().String() task2UUID := uuid.New().String() _, err = handler.repos.Tasks.Create(ctx, &models.Task{ UUID: task2UUID, Description: "Task 2", Status: "pending", }) if err != nil { t.Fatalf("Failed to create task 2: %v", err) } id1, err := handler.repos.Tasks.Create(ctx, &models.Task{ UUID: task1UUID, Description: "Task 1", Status: "pending", DependsOn: []string{task2UUID}, }) if err != nil { t.Fatalf("Failed to create task 1: %v", err) } err = handler.ListDeps(ctx, strconv.FormatInt(id1, 10)) if err != nil { t.Errorf("ListDeps failed: %v", err) } }) t.Run("BlockedByDep", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() handler, err := NewTaskHandler() if err != nil { t.Fatalf("Failed to create handler: %v", err) } defer handler.Close() task1UUID := uuid.New().String() task2UUID := uuid.New().String() id2, err := handler.repos.Tasks.Create(ctx, &models.Task{UUID: task2UUID, Description: "Task 2", Status: "pending"}) if err != nil { t.Fatalf("Failed to create task 2: %v", err) } if _, err = handler.repos.Tasks.Create(ctx, &models.Task{UUID: task1UUID, Description: "Task 1", Status: "pending", DependsOn: []string{task2UUID}}); err != nil { t.Fatalf("Failed to create task 1: %v", err) } if err = handler.BlockedByDep(ctx, strconv.FormatInt(id2, 10)); err != nil { t.Errorf("BlockedByDep failed: %v", err) } }) t.Run("Annotate", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() handler, err := NewTaskHandler() if err != nil { t.Fatalf("Failed to create handler: %v", err) } defer handler.Close() id, err := handler.repos.Tasks.Create(ctx, &models.Task{ UUID: uuid.New().String(), Description: "Test task", Status: "pending", }) if err != nil { t.Fatalf("Failed to create task: %v", err) } t.Run("adds annotation successfully", func(t *testing.T) { err := handler.Annotate(ctx, strconv.FormatInt(id, 10), "First annotation") shared.AssertNoError(t, err, "Annotate should succeed") task, err := handler.repos.Tasks.Get(ctx, id) shared.AssertNoError(t, err, "Get should succeed") shared.AssertEqual(t, 1, len(task.Annotations), "should have 1 annotation") shared.AssertEqual(t, "First annotation", task.Annotations[0], "annotation text should match") }) t.Run("adds multiple annotations", func(t *testing.T) { err := handler.Annotate(ctx, strconv.FormatInt(id, 10), "Second annotation") shared.AssertNoError(t, err, "Annotate should succeed") task, err := handler.repos.Tasks.Get(ctx, id) shared.AssertNoError(t, err, "Get should succeed") shared.AssertEqual(t, 2, len(task.Annotations), "should have 2 annotations") }) t.Run("fails with empty annotation", func(t *testing.T) { err := handler.Annotate(ctx, strconv.FormatInt(id, 10), "") shared.AssertError(t, err, "should fail with empty annotation") shared.AssertContains(t, err.Error(), "annotation text required", "error message") }) t.Run("fails with invalid task ID", func(t *testing.T) { err := handler.Annotate(ctx, "99999", "Test annotation") shared.AssertError(t, err, "should fail with invalid task ID") shared.AssertContains(t, err.Error(), "failed to find task", "error message") }) t.Run("works with UUID", func(t *testing.T) { task := &models.Task{ UUID: uuid.New().String(), Description: "UUID task", Status: "pending", } _, err := handler.repos.Tasks.Create(ctx, task) shared.AssertNoError(t, err, "Create should succeed") err = handler.Annotate(ctx, task.UUID, "UUID annotation") shared.AssertNoError(t, err, "Annotate with UUID should succeed") retrieved, err := handler.repos.Tasks.GetByUUID(ctx, task.UUID) shared.AssertNoError(t, err, "GetByUUID should succeed") shared.AssertEqual(t, 1, len(retrieved.Annotations), "should have 1 annotation") }) }) t.Run("ListAnnotations", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() handler, err := NewTaskHandler() if err != nil { t.Fatalf("Failed to create handler: %v", err) } defer handler.Close() t.Run("lists annotations successfully", func(t *testing.T) { task := &models.Task{ UUID: uuid.New().String(), Description: "Test task", Status: "pending", Annotations: []string{"Annotation 1", "Annotation 2", "Annotation 3"}, } id, err := handler.repos.Tasks.Create(ctx, task) shared.AssertNoError(t, err, "Create should succeed") err = handler.ListAnnotations(ctx, strconv.FormatInt(id, 10)) shared.AssertNoError(t, err, "ListAnnotations should succeed") }) t.Run("handles task with no annotations", func(t *testing.T) { task := &models.Task{ UUID: uuid.New().String(), Description: "Task without annotations", Status: "pending", } id, err := handler.repos.Tasks.Create(ctx, task) shared.AssertNoError(t, err, "Create should succeed") err = handler.ListAnnotations(ctx, strconv.FormatInt(id, 10)) shared.AssertNoError(t, err, "ListAnnotations should succeed for empty annotations") }) t.Run("fails with invalid task ID", func(t *testing.T) { err := handler.ListAnnotations(ctx, "99999") shared.AssertError(t, err, "should fail with invalid task ID") shared.AssertContains(t, err.Error(), "failed to find task", "error message") }) }) t.Run("RemoveAnnotation", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() handler, err := NewTaskHandler() if err != nil { t.Fatalf("Failed to create handler: %v", err) } defer handler.Close() t.Run("removes annotation successfully", func(t *testing.T) { task := &models.Task{ UUID: uuid.New().String(), Description: "Test task", Status: "pending", Annotations: []string{"First", "Second", "Third"}, } id, err := handler.repos.Tasks.Create(ctx, task) shared.AssertNoError(t, err, "Create should succeed") err = handler.RemoveAnnotation(ctx, strconv.FormatInt(id, 10), 2) shared.AssertNoError(t, err, "RemoveAnnotation should succeed") retrieved, err := handler.repos.Tasks.Get(ctx, id) shared.AssertNoError(t, err, "Get should succeed") shared.AssertEqual(t, 2, len(retrieved.Annotations), "should have 2 annotations") shared.AssertEqual(t, "First", retrieved.Annotations[0], "first annotation should remain") shared.AssertEqual(t, "Third", retrieved.Annotations[1], "third annotation should be second") }) t.Run("fails with invalid index (too low)", func(t *testing.T) { task := &models.Task{ UUID: uuid.New().String(), Description: "Test task", Status: "pending", Annotations: []string{"First"}, } id, err := handler.repos.Tasks.Create(ctx, task) shared.AssertNoError(t, err, "Create should succeed") err = handler.RemoveAnnotation(ctx, strconv.FormatInt(id, 10), 0) shared.AssertError(t, err, "should fail with index 0") shared.AssertContains(t, err.Error(), "index out of range", "error message") }) t.Run("fails with invalid index (too high)", func(t *testing.T) { task := &models.Task{ UUID: uuid.New().String(), Description: "Test task", Status: "pending", Annotations: []string{"First"}, } id, err := handler.repos.Tasks.Create(ctx, task) shared.AssertNoError(t, err, "Create should succeed") err = handler.RemoveAnnotation(ctx, strconv.FormatInt(id, 10), 5) shared.AssertError(t, err, "should fail with index > len") shared.AssertContains(t, err.Error(), "index out of range", "error message") }) t.Run("fails when task has no annotations", func(t *testing.T) { task := &models.Task{ UUID: uuid.New().String(), Description: "Task without annotations", Status: "pending", } id, err := handler.repos.Tasks.Create(ctx, task) shared.AssertNoError(t, err, "Create should succeed") err = handler.RemoveAnnotation(ctx, strconv.FormatInt(id, 10), 1) shared.AssertError(t, err, "should fail when no annotations") shared.AssertContains(t, err.Error(), "has no annotations", "error message") }) t.Run("fails with invalid task ID", func(t *testing.T) { err := handler.RemoveAnnotation(ctx, "99999", 1) shared.AssertError(t, err, "should fail with invalid task ID") shared.AssertContains(t, err.Error(), "failed to find task", "error message") }) }) t.Run("BulkEdit", func(t *testing.T) { suite := NewHandlerTestSuite(t) defer suite.cleanup() handler, err := NewTaskHandler() if err != nil { t.Fatalf("Failed to create handler: %v", err) } defer handler.Close() t.Run("updates multiple tasks successfully", func(t *testing.T) { id1, err := handler.repos.Tasks.Create(ctx, &models.Task{ UUID: uuid.New().String(), Description: "Task 1", Status: "pending", }) shared.AssertNoError(t, err, "Create should succeed") id2, err := handler.repos.Tasks.Create(ctx, &models.Task{ UUID: uuid.New().String(), Description: "Task 2", Status: "pending", }) shared.AssertNoError(t, err, "Create should succeed") taskIDs := []string{strconv.FormatInt(id1, 10), strconv.FormatInt(id2, 10)} err = handler.BulkEdit(ctx, taskIDs, "done", "high", "test-project", "", []string{}, false, false) shared.AssertNoError(t, err, "BulkEdit should succeed") task1, err := handler.repos.Tasks.Get(ctx, id1) shared.AssertNoError(t, err, "Get should succeed") shared.AssertEqual(t, "done", task1.Status, "task 1 status should be updated") shared.AssertEqual(t, "high", task1.Priority, "task 1 priority should be updated") shared.AssertEqual(t, "test-project", task1.Project, "task 1 project should be updated") task2, err := handler.repos.Tasks.Get(ctx, id2) shared.AssertNoError(t, err, "Get should succeed") shared.AssertEqual(t, "done", task2.Status, "task 2 status should be updated") shared.AssertEqual(t, "high", task2.Priority, "task 2 priority should be updated") shared.AssertEqual(t, "test-project", task2.Project, "task 2 project should be updated") }) t.Run("updates with tag replacement", func(t *testing.T) { id1, err := handler.repos.Tasks.Create(ctx, &models.Task{ UUID: uuid.New().String(), Description: "Task 1", Status: "pending", Tags: []string{"old-tag"}, }) shared.AssertNoError(t, err, "Create should succeed") taskIDs := []string{strconv.FormatInt(id1, 10)} err = handler.BulkEdit(ctx, taskIDs, "", "", "", "", []string{"new-tag1", "new-tag2"}, false, false) shared.AssertNoError(t, err, "BulkEdit should succeed") task, err := handler.repos.Tasks.Get(ctx, id1) shared.AssertNoError(t, err, "Get should succeed") shared.AssertEqual(t, 2, len(task.Tags), "should have 2 tags") shared.AssertTrue(t, slices.Contains(task.Tags, "new-tag1"), "should contain new-tag1") shared.AssertTrue(t, slices.Contains(task.Tags, "new-tag2"), "should contain new-tag2") }) t.Run("adds tags with add-tags flag", func(t *testing.T) { id1, err := handler.repos.Tasks.Create(ctx, &models.Task{ UUID: uuid.New().String(), Description: "Task 1", Status: "pending", Tags: []string{"existing-tag"}, }) shared.AssertNoError(t, err, "Create should succeed") taskIDs := []string{strconv.FormatInt(id1, 10)} err = handler.BulkEdit(ctx, taskIDs, "", "", "", "", []string{"new-tag"}, true, false) shared.AssertNoError(t, err, "BulkEdit should succeed") task, err := handler.repos.Tasks.Get(ctx, id1) shared.AssertNoError(t, err, "Get should succeed") shared.AssertEqual(t, 2, len(task.Tags), "should have 2 tags") shared.AssertTrue(t, slices.Contains(task.Tags, "existing-tag"), "should contain existing-tag") shared.AssertTrue(t, slices.Contains(task.Tags, "new-tag"), "should contain new-tag") }) t.Run("removes tags with remove-tags flag", func(t *testing.T) { id1, err := handler.repos.Tasks.Create(ctx, &models.Task{ UUID: uuid.New().String(), Description: "Task 1", Status: "pending", Tags: []string{"tag1", "tag2", "tag3"}, }) shared.AssertNoError(t, err, "Create should succeed") taskIDs := []string{strconv.FormatInt(id1, 10)} err = handler.BulkEdit(ctx, taskIDs, "", "", "", "", []string{"tag2"}, false, true) shared.AssertNoError(t, err, "BulkEdit should succeed") task, err := handler.repos.Tasks.Get(ctx, id1) shared.AssertNoError(t, err, "Get should succeed") shared.AssertEqual(t, 2, len(task.Tags), "should have 2 tags") shared.AssertTrue(t, slices.Contains(task.Tags, "tag1"), "should contain tag1") shared.AssertTrue(t, slices.Contains(task.Tags, "tag3"), "should contain tag3") shared.AssertFalse(t, slices.Contains(task.Tags, "tag2"), "should not contain tag2") }) t.Run("fails with no task IDs", func(t *testing.T) { err := handler.BulkEdit(ctx, []string{}, "done", "", "", "", []string{}, false, false) shared.AssertError(t, err, "should fail with no task IDs") shared.AssertContains(t, err.Error(), "no task IDs provided", "error message") }) t.Run("fails with invalid task ID", func(t *testing.T) { err := handler.BulkEdit(ctx, []string{"99999"}, "done", "", "", "", []string{}, false, false) shared.AssertError(t, err, "should fail with invalid task ID") }) t.Run("works with UUIDs", func(t *testing.T) { task1 := &models.Task{ UUID: uuid.New().String(), Description: "UUID task 1", Status: "pending", } _, err := handler.repos.Tasks.Create(ctx, task1) shared.AssertNoError(t, err, "Create should succeed") task2 := &models.Task{ UUID: uuid.New().String(), Description: "UUID task 2", Status: "pending", } _, err = handler.repos.Tasks.Create(ctx, task2) shared.AssertNoError(t, err, "Create should succeed") taskIDs := []string{task1.UUID, task2.UUID} err = handler.BulkEdit(ctx, taskIDs, "done", "", "", "", []string{}, false, false) shared.AssertNoError(t, err, "BulkEdit with UUIDs should succeed") retrieved1, err := handler.repos.Tasks.GetByUUID(ctx, task1.UUID) shared.AssertNoError(t, err, "GetByUUID should succeed") shared.AssertEqual(t, "done", retrieved1.Status, "task 1 status should be updated") retrieved2, err := handler.repos.Tasks.GetByUUID(ctx, task2.UUID) shared.AssertNoError(t, err, "GetByUUID should succeed") shared.AssertEqual(t, "done", retrieved2.Status, "task 2 status should be updated") }) }) }