cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists 馃崈
charm
leaflet
readability
golang
1package handlers
2
3import (
4 "bytes"
5 "context"
6 "os"
7 "runtime"
8 "slices"
9 "strconv"
10 "strings"
11 "testing"
12 "time"
13
14 "github.com/google/uuid"
15 "github.com/stormlightlabs/noteleaf/internal/models"
16 "github.com/stormlightlabs/noteleaf/internal/shared"
17 "github.com/stormlightlabs/noteleaf/internal/ui"
18)
19
20func TestTaskHandler(t *testing.T) {
21 ctx := context.Background()
22 t.Run("New", func(t *testing.T) {
23 t.Run("creates handler successfully", func(t *testing.T) {
24 suite := NewHandlerTestSuite(t)
25 defer suite.cleanup()
26
27 handler, err := NewTaskHandler()
28 if err != nil {
29 t.Fatalf("NewTaskHandler failed: %v", err)
30 }
31 if handler == nil {
32 t.Fatal("Handler should not be nil")
33 }
34 defer handler.Close()
35
36 if handler.db == nil {
37 t.Error("Handler database should not be nil")
38 }
39 if handler.config == nil {
40 t.Error("Handler config should not be nil")
41 }
42 if handler.repos == nil {
43 t.Error("Handler repos should not be nil")
44 }
45 })
46
47 t.Run("handles database initialization error", func(t *testing.T) {
48 originalXDG := os.Getenv("XDG_CONFIG_HOME")
49 originalHome := os.Getenv("HOME")
50
51 if runtime.GOOS == "windows" {
52 originalAppData := os.Getenv("APPDATA")
53 os.Unsetenv("APPDATA")
54 defer os.Setenv("APPDATA", originalAppData)
55 } else {
56 os.Unsetenv("XDG_CONFIG_HOME")
57 os.Unsetenv("HOME")
58 defer os.Setenv("XDG_CONFIG_HOME", originalXDG)
59 defer os.Setenv("HOME", originalHome)
60 }
61
62 handler, err := NewTaskHandler()
63 if err == nil {
64 if handler != nil {
65 handler.Close()
66 }
67 t.Error("Expected error when database initialization fails")
68 }
69 })
70 })
71
72 t.Run("Create", func(t *testing.T) {
73 suite := NewHandlerTestSuite(t)
74 defer suite.cleanup()
75
76 handler := CreateHandler(t, NewTaskHandler)
77
78 t.Run("creates task successfully", func(t *testing.T) {
79 desc := "Buy groceries and cook dinner"
80 err := handler.Create(ctx, desc, "", "", "", "", "", "", "", "", "", "", []string{})
81 shared.AssertNoError(t, err, "CreateTask should succeed")
82
83 tasks, err := handler.repos.Tasks.GetPending(ctx)
84 shared.AssertNoError(t, err, "Failed to get pending tasks")
85
86 if len(tasks) != 1 {
87 t.Errorf("Expected 1 task, got %d", len(tasks))
88 }
89
90 task := tasks[0]
91 expectedDesc := "Buy groceries and cook dinner"
92 if task.Description != expectedDesc {
93 t.Errorf("Expected description '%s', got '%s'", expectedDesc, task.Description)
94 }
95
96 if task.Status != "pending" {
97 t.Errorf("Expected status 'pending', got '%s'", task.Status)
98 }
99
100 if task.UUID == "" {
101 t.Error("Task should have a UUID")
102 }
103 })
104
105 t.Run("fails with empty description", func(t *testing.T) {
106 desc := ""
107 err := handler.Create(ctx, desc, "", "", "", "", "", "", "", "", "", "", []string{})
108 shared.AssertError(t, err, "Expected error for empty description")
109 shared.AssertContains(t, err.Error(), "task description required", "Error message should mention required description")
110 })
111
112 t.Run("creates task with flags", func(t *testing.T) {
113 description := "Task with flags"
114 priority := "A"
115 project := "test-project"
116 due := "2024-12-31"
117 tags := []string{"urgent", "work"}
118
119 err := handler.Create(ctx, description, priority, project, "test-context", due, "", "", "", "", "", "", tags)
120 if err != nil {
121 t.Errorf("CreateTask with flags failed: %v", err)
122 }
123
124 tasks, err := handler.repos.Tasks.GetPending(ctx)
125 if err != nil {
126 t.Fatalf("Failed to get pending tasks: %v", err)
127 }
128
129 if len(tasks) < 1 {
130 t.Errorf("Expected at least 1 task, got %d", len(tasks))
131 }
132
133 var task *models.Task
134 for _, t := range tasks {
135 if t.Description == "Task with flags" {
136 task = t
137 break
138 }
139 }
140
141 if task == nil {
142 t.Fatal("Could not find created task")
143 }
144
145 if task.Priority != priority {
146 t.Errorf("Expected priority '%s', got '%s'", priority, task.Priority)
147 }
148
149 if task.Project != project {
150 t.Errorf("Expected project '%s', got '%s'", project, task.Project)
151 }
152
153 if task.Due == nil {
154 t.Error("Expected due date to be set")
155 } else if task.Due.Format("2006-01-02") != due {
156 t.Errorf("Expected due date '%s', got '%s'", due, task.Due.Format("2006-01-02"))
157 }
158
159 if len(task.Tags) != len(tags) {
160 t.Errorf("Expected %d tags, got %d", len(tags), len(task.Tags))
161 } else {
162 for i, tag := range tags {
163 if task.Tags[i] != tag {
164 t.Errorf("Expected tag '%s' at index %d, got '%s'", tag, i, task.Tags[i])
165 }
166 }
167 }
168 })
169
170 t.Run("fails with invalid due date format", func(t *testing.T) {
171 desc := "Task with invalid date"
172 invalidDue := "invalid-date"
173
174 err := handler.Create(ctx, desc, "", "", "", invalidDue, "", "", "", "", "", "", []string{})
175 if err == nil {
176 t.Error("Expected error for invalid due date format")
177 }
178
179 if !strings.Contains(err.Error(), "invalid due date format") {
180 t.Errorf("Expected error about invalid date format, got: %v", err)
181 }
182 })
183
184 t.Run("fails when repository Create returns error", func(t *testing.T) {
185 ctx, cancel := context.WithCancel(ctx)
186 cancel()
187
188 err := handler.Create(ctx, "Test task", "", "", "", "", "", "", "", "", "", "", []string{})
189 if err == nil {
190 t.Error("Expected error when repository Create fails")
191 }
192
193 if !strings.Contains(err.Error(), "failed to create task") {
194 t.Errorf("Expected 'failed to create task' error, got: %v", err)
195 }
196 })
197 })
198
199 t.Run("List", func(t *testing.T) {
200 suite := NewHandlerTestSuite(t)
201 defer suite.cleanup()
202
203 handler, err := NewTaskHandler()
204 if err != nil {
205 t.Fatalf("Failed to create handler: %v", err)
206 }
207 defer handler.Close()
208
209 task1 := &models.Task{
210 UUID: uuid.New().String(),
211 Description: "Task 1",
212 Status: "pending",
213 Priority: "A",
214 Project: "work",
215 }
216 _, err = handler.repos.Tasks.Create(ctx, task1)
217 if err != nil {
218 t.Fatalf("Failed to create task1: %v", err)
219 }
220
221 task2 := &models.Task{
222 UUID: uuid.New().String(),
223 Description: "Task 2",
224 Status: "completed",
225 }
226 _, err = handler.repos.Tasks.Create(ctx, task2)
227 if err != nil {
228 t.Fatalf("Failed to create task2: %v", err)
229 }
230
231 t.Run("lists pending tasks by default (static mode)", func(t *testing.T) {
232 err := handler.List(ctx, true, false, "", "", "", "", "")
233 if err != nil {
234 t.Errorf("ListTasks failed: %v", err)
235 }
236 })
237
238 t.Run("filters by status (static mode)", func(t *testing.T) {
239 err := handler.List(ctx, true, false, "completed", "", "", "", "")
240 if err != nil {
241 t.Errorf("ListTasks with status filter failed: %v", err)
242 }
243 })
244
245 t.Run("filters by priority (static mode)", func(t *testing.T) {
246 err := handler.List(ctx, true, false, "", "A", "", "", "")
247 if err != nil {
248 t.Errorf("ListTasks with priority filter failed: %v", err)
249 }
250 })
251
252 t.Run("filters by project (static mode)", func(t *testing.T) {
253 err := handler.List(ctx, true, false, "", "", "work", "", "")
254 if err != nil {
255 t.Errorf("ListTasks with project filter failed: %v", err)
256 }
257 })
258
259 t.Run("show all tasks (static mode)", func(t *testing.T) {
260 err := handler.List(ctx, true, true, "", "", "", "", "")
261 if err != nil {
262 t.Errorf("ListTasks with show all failed: %v", err)
263 }
264 })
265
266 t.Run("interactive mode path", func(t *testing.T) {
267 if err := TestTaskInteractiveList(t, handler, false, "", "", ""); err != nil {
268 t.Errorf("Interactive task list test failed: %v", err)
269 }
270 })
271
272 t.Run("interactive mode path with filters", func(t *testing.T) {
273 if err := TestTaskInteractiveList(t, handler, false, "pending", "A", "work"); err != nil {
274 t.Errorf("Interactive task list test with filters failed: %v", err)
275 }
276 })
277
278 t.Run("interactive mode path show all", func(t *testing.T) {
279 if err := TestTaskInteractiveList(t, handler, true, "", "", ""); err != nil {
280 t.Errorf("Interactive task list test with show all failed: %v", err)
281 }
282 })
283 })
284
285 t.Run("Update", func(t *testing.T) {
286 suite := NewHandlerTestSuite(t)
287 defer suite.cleanup()
288 handler, err := NewTaskHandler()
289 if err != nil {
290 t.Fatalf("Failed to create handler: %v", err)
291 }
292 defer handler.Close()
293
294 task := &models.Task{
295 UUID: uuid.New().String(),
296 Description: "Original description",
297 Status: "pending",
298 }
299 id, err := handler.repos.Tasks.Create(ctx, task)
300 if err != nil {
301 t.Fatalf("Failed to create task: %v", err)
302 }
303
304 t.Run("updates task by ID", func(t *testing.T) {
305 taskID := strconv.FormatInt(id, 10)
306
307 err := handler.Update(ctx, taskID, "Updated description", "", "", "", "", "", "", "", "", []string{}, []string{}, "", "")
308 if err != nil {
309 t.Errorf("UpdateTask failed: %v", err)
310 }
311
312 updatedTask, err := handler.repos.Tasks.Get(ctx, id)
313 if err != nil {
314 t.Fatalf("Failed to get updated task: %v", err)
315 }
316
317 if updatedTask.Description != "Updated description" {
318 t.Errorf("Expected description 'Updated description', got '%s'", updatedTask.Description)
319 }
320 })
321
322 t.Run("updates task by UUID", func(t *testing.T) {
323 taskID := task.UUID
324 err := handler.Update(ctx, taskID, "", "completed", "", "", "", "", "", "", "", []string{}, []string{}, "", "")
325 if err != nil {
326 t.Errorf("UpdateTask by UUID failed: %v", err)
327 }
328
329 updatedTask, err := handler.repos.Tasks.GetByUUID(ctx, task.UUID)
330 if err != nil {
331 t.Fatalf("Failed to get updated task by UUID: %v", err)
332 }
333
334 if updatedTask.Status != "completed" {
335 t.Errorf("Expected status 'completed', got '%s'", updatedTask.Status)
336 }
337 })
338
339 t.Run("updates multiple fields", func(t *testing.T) {
340 taskID := strconv.FormatInt(id, 10)
341 err := handler.Update(ctx, taskID, "Multiple updates", "", "B", "test", "office", "2024-12-31", "", "", "", []string{}, []string{}, "", "")
342 if err != nil {
343 t.Errorf("UpdateTask with multiple fields failed: %v", err)
344 }
345
346 updatedTask, err := handler.repos.Tasks.Get(ctx, id)
347 if err != nil {
348 t.Fatalf("Failed to get updated task: %v", err)
349 }
350
351 if updatedTask.Description != "Multiple updates" {
352 t.Errorf("Expected description 'Multiple updates', got '%s'", updatedTask.Description)
353 }
354 if updatedTask.Priority != "B" {
355 t.Errorf("Expected priority 'B', got '%s'", updatedTask.Priority)
356 }
357 if updatedTask.Project != "test" {
358 t.Errorf("Expected project 'test', got '%s'", updatedTask.Project)
359 }
360 if updatedTask.Due == nil {
361 t.Error("Expected due date to be set")
362 }
363 })
364
365 t.Run("adds and removes tags", func(t *testing.T) {
366 taskID := strconv.FormatInt(id, 10)
367 err := handler.Update(ctx, taskID, "", "", "", "", "", "", "", "", "", []string{"work", "urgent"}, []string{}, "", "")
368 if err != nil {
369 t.Errorf("UpdateTask with add tags failed: %v", err)
370 }
371
372 updatedTask, err := handler.repos.Tasks.Get(ctx, id)
373 if err != nil {
374 t.Fatalf("Failed to get updated task: %v", err)
375 }
376
377 if len(updatedTask.Tags) != 2 {
378 t.Errorf("Expected 2 tags, got %d", len(updatedTask.Tags))
379 }
380
381 taskID = strconv.FormatInt(id, 10)
382
383 err = handler.Update(ctx, taskID, "", "", "", "", "", "", "", "", "", []string{}, []string{"urgent"}, "", "")
384 if err != nil {
385 t.Errorf("UpdateTask with remove tag failed: %v", err)
386 }
387
388 updatedTask, err = handler.repos.Tasks.Get(ctx, id)
389 if err != nil {
390 t.Fatalf("Failed to get updated task: %v", err)
391 }
392
393 if len(updatedTask.Tags) != 1 {
394 t.Errorf("Expected 1 tag after removal, got %d", len(updatedTask.Tags))
395 }
396
397 if updatedTask.Tags[0] != "work" {
398 t.Errorf("Expected remaining tag 'work', got '%s'", updatedTask.Tags[0])
399 }
400 })
401
402 t.Run("fails with missing task ID", func(t *testing.T) {
403 err := handler.Update(ctx, "", "", "", "", "", "", "", "", "", "", []string{}, []string{}, "", "")
404 if err == nil {
405 t.Error("Expected error for missing task ID")
406 }
407
408 if !strings.Contains(err.Error(), "failed to find task") {
409 t.Errorf("Expected error about task not found, got: %v", err)
410 }
411 })
412
413 t.Run("fails with invalid task ID", func(t *testing.T) {
414 taskID := "99999"
415
416 err := handler.Update(ctx, taskID, "test", "", "", "", "", "", "", "", "", []string{}, []string{}, "", "")
417 if err == nil {
418 t.Error("Expected error for invalid task ID")
419 }
420
421 if !strings.Contains(err.Error(), "failed to find task") {
422 t.Errorf("Expected error about task not found, got: %v", err)
423 }
424 })
425
426 t.Run("fails when repository Get fails", func(t *testing.T) {
427 cancelCtx, cancel := context.WithCancel(context.Background())
428 cancel()
429
430 err := handler.Update(cancelCtx, "1", "test", "", "", "", "", "", "", "", "", []string{}, []string{}, "", "")
431 if err == nil {
432 t.Error("Expected error when repository Get fails")
433 }
434
435 if !strings.Contains(err.Error(), "failed to find task") {
436 t.Errorf("Expected 'failed to find task' error, got: %v", err)
437 }
438 })
439
440 t.Run("fails when repository operations fail with canceled context", func(t *testing.T) {
441 task := &models.Task{
442 UUID: uuid.New().String(),
443 Description: "Test task",
444 Status: "pending",
445 }
446 id, err := handler.repos.Tasks.Create(ctx, task)
447 if err != nil {
448 t.Fatalf("Failed to create task: %v", err)
449 }
450
451 cancelCtx, cancel := context.WithCancel(context.Background())
452 cancel()
453
454 taskID := strconv.FormatInt(id, 10)
455 err = handler.Update(cancelCtx, taskID, "Updated", "", "", "", "", "", "", "", "", []string{}, []string{}, "", "")
456 if err == nil {
457 t.Error("Expected error with canceled context")
458 }
459 })
460 })
461
462 t.Run("Delete", func(t *testing.T) {
463 suite := NewHandlerTestSuite(t)
464 defer suite.cleanup()
465
466 handler := CreateHandler(t, NewTaskHandler)
467
468 task := &models.Task{
469 UUID: uuid.New().String(),
470 Description: "Task to delete",
471 Status: "pending",
472 }
473 id, err := handler.repos.Tasks.Create(ctx, task)
474 if err != nil {
475 t.Fatalf("Failed to create task: %v", err)
476 }
477
478 t.Run("deletes task by ID", func(t *testing.T) {
479 args := []string{strconv.FormatInt(id, 10)}
480
481 err := handler.Delete(ctx, args)
482 if err != nil {
483 t.Errorf("DeleteTask failed: %v", err)
484 }
485
486 _, err = handler.repos.Tasks.Get(ctx, id)
487 if err == nil {
488 t.Error("Expected error when getting deleted task")
489 }
490 })
491
492 t.Run("deletes task by UUID", func(t *testing.T) {
493 task2 := &models.Task{
494 UUID: uuid.New().String(),
495 Description: "Task to delete by UUID",
496 Status: "pending",
497 }
498 _, err := handler.repos.Tasks.Create(ctx, task2)
499 if err != nil {
500 t.Fatalf("Failed to create task2: %v", err)
501 }
502
503 args := []string{task2.UUID}
504
505 err = handler.Delete(ctx, args)
506 if err != nil {
507 t.Errorf("DeleteTask by UUID failed: %v", err)
508 }
509
510 _, err = handler.repos.Tasks.GetByUUID(ctx, task2.UUID)
511 if err == nil {
512 t.Error("Expected error when getting deleted task by UUID")
513 }
514 })
515
516 t.Run("fails with missing task ID", func(t *testing.T) {
517 args := []string{}
518
519 err := handler.Delete(ctx, args)
520 if err == nil {
521 t.Error("Expected error for missing task ID")
522 }
523
524 if !strings.Contains(err.Error(), "task ID required") {
525 t.Errorf("Expected error about required task ID, got: %v", err)
526 }
527 })
528
529 t.Run("fails with invalid task ID", func(t *testing.T) {
530 args := []string{"99999"}
531
532 err := handler.Delete(ctx, args)
533 if err == nil {
534 t.Error("Expected error for invalid task ID")
535 }
536
537 if !strings.Contains(err.Error(), "failed to find task") {
538 t.Errorf("Expected error about task not found, got: %v", err)
539 }
540 })
541 })
542
543 t.Run("View", func(t *testing.T) {
544 suite := NewHandlerTestSuite(t)
545 defer suite.cleanup()
546
547 handler, err := NewTaskHandler()
548 if err != nil {
549 t.Fatalf("Failed to create handler: %v", err)
550 }
551 defer handler.Close()
552
553 now := time.Now()
554 task := &models.Task{
555 UUID: uuid.New().String(),
556 Description: "Task to view",
557 Status: "pending",
558 Priority: "A",
559 Project: "test",
560 Tags: []string{"work", "important"},
561 Entry: now,
562 Modified: now,
563 }
564 id, err := handler.repos.Tasks.Create(ctx, task)
565 if err != nil {
566 t.Fatalf("Failed to create task: %v", err)
567 }
568
569 t.Run("views task by ID", func(t *testing.T) {
570 args := []string{strconv.FormatInt(id, 10)}
571
572 err := handler.View(ctx, args, "detailed", false, false)
573 if err != nil {
574 t.Errorf("ViewTask failed: %v", err)
575 }
576 })
577
578 t.Run("views task by UUID", func(t *testing.T) {
579 args := []string{task.UUID}
580
581 err := handler.View(ctx, args, "detailed", false, false)
582 if err != nil {
583 t.Errorf("ViewTask by UUID failed: %v", err)
584 }
585 })
586
587 t.Run("fails with missing task ID", func(t *testing.T) {
588 args := []string{}
589
590 err := handler.View(ctx, args, "detailed", false, false)
591 if err == nil {
592 t.Error("Expected error for missing task ID")
593 }
594
595 if !strings.Contains(err.Error(), "task ID required") {
596 t.Errorf("Expected error about required task ID, got: %v", err)
597 }
598 })
599
600 t.Run("fails with invalid task ID", func(t *testing.T) {
601 args := []string{"99999"}
602
603 err := handler.View(ctx, args, "detailed", false, false)
604 if err == nil {
605 t.Error("Expected error for invalid task ID")
606 }
607
608 if !strings.Contains(err.Error(), "failed to find task") {
609 t.Errorf("Expected error about task not found, got: %v", err)
610 }
611 })
612
613 t.Run("uses brief format", func(t *testing.T) {
614 args := []string{strconv.FormatInt(id, 10)}
615 err := handler.View(ctx, args, "brief", false, false)
616 if err != nil {
617 t.Errorf("ViewTask with brief format failed: %v", err)
618 }
619 })
620
621 t.Run("hides metadata", func(t *testing.T) {
622 args := []string{strconv.FormatInt(id, 10)}
623 err := handler.View(ctx, args, "detailed", false, true)
624 if err != nil {
625 t.Errorf("ViewTask with no-metadata failed: %v", err)
626 }
627 })
628
629 t.Run("outputs JSON", func(t *testing.T) {
630 args := []string{strconv.FormatInt(id, 10)}
631 err := handler.View(ctx, args, "detailed", true, false)
632 if err != nil {
633 t.Errorf("ViewTask with JSON output failed: %v", err)
634 }
635 })
636 })
637
638 t.Run("Done", func(t *testing.T) {
639 suite := NewHandlerTestSuite(t)
640 defer suite.cleanup()
641
642 handler, err := NewTaskHandler()
643 if err != nil {
644 t.Fatalf("Failed to create handler: %v", err)
645 }
646 defer handler.Close()
647
648 task := &models.Task{
649 UUID: uuid.New().String(),
650 Description: "Task to complete",
651 Status: "pending",
652 }
653 id, err := handler.repos.Tasks.Create(ctx, task)
654 if err != nil {
655 t.Fatalf("Failed to create task: %v", err)
656 }
657
658 t.Run("marks task as done by ID", func(t *testing.T) {
659 args := []string{strconv.FormatInt(id, 10)}
660
661 err := handler.Done(ctx, args)
662 if err != nil {
663 t.Errorf("DoneTask failed: %v", err)
664 }
665
666 completedTask, err := handler.repos.Tasks.Get(ctx, id)
667 if err != nil {
668 t.Fatalf("Failed to get completed task: %v", err)
669 }
670
671 if completedTask.Status != "completed" {
672 t.Errorf("Expected status 'completed', got '%s'", completedTask.Status)
673 }
674
675 if completedTask.End == nil {
676 t.Error("Expected end time to be set")
677 }
678 })
679
680 t.Run("handles already completed task", func(t *testing.T) {
681 task2 := &models.Task{
682 UUID: uuid.New().String(),
683 Description: "Already completed task",
684 Status: "completed",
685 }
686 id2, err := handler.repos.Tasks.Create(ctx, task2)
687 if err != nil {
688 t.Fatalf("Failed to create task2: %v", err)
689 }
690
691 args := []string{strconv.FormatInt(id2, 10)}
692
693 err = handler.Done(ctx, args)
694 if err != nil {
695 t.Errorf("DoneTask on completed task failed: %v", err)
696 }
697 })
698
699 t.Run("marks task as done by UUID", func(t *testing.T) {
700 task3 := &models.Task{
701 UUID: uuid.New().String(),
702 Description: "Task to complete by UUID",
703 Status: "pending",
704 }
705 _, err := handler.repos.Tasks.Create(ctx, task3)
706 if err != nil {
707 t.Fatalf("Failed to create task3: %v", err)
708 }
709
710 args := []string{task3.UUID}
711
712 err = handler.Done(ctx, args)
713 if err != nil {
714 t.Errorf("DoneTask by UUID failed: %v", err)
715 }
716
717 completedTask, err := handler.repos.Tasks.GetByUUID(ctx, task3.UUID)
718 if err != nil {
719 t.Fatalf("Failed to get completed task by UUID: %v", err)
720 }
721
722 if completedTask.Status != "completed" {
723 t.Errorf("Expected status 'completed', got '%s'", completedTask.Status)
724 }
725
726 if completedTask.End == nil {
727 t.Error("Expected end time to be set")
728 }
729 })
730
731 t.Run("fails with missing task ID", func(t *testing.T) {
732 args := []string{}
733
734 err := handler.Done(ctx, args)
735 if err == nil {
736 t.Error("Expected error for missing task ID")
737 }
738
739 if !strings.Contains(err.Error(), "task ID required") {
740 t.Errorf("Expected error about required task ID, got: %v", err)
741 }
742 })
743
744 t.Run("fails with invalid task ID", func(t *testing.T) {
745 args := []string{"99999"}
746
747 err := handler.Done(ctx, args)
748 if err == nil {
749 t.Error("Expected error for invalid task ID")
750 }
751
752 if !strings.Contains(err.Error(), "failed to find task") {
753 t.Errorf("Expected error about task not found, got: %v", err)
754 }
755 })
756 })
757
758 t.Run("Helper", func(t *testing.T) {
759 t.Run("removeString function", func(t *testing.T) {
760 slice := []string{"a", "b", "c", "b"}
761 result := removeString(slice, "b")
762
763 if len(result) != 2 {
764 t.Errorf("Expected 2 items after removing 'b', got %d", len(result))
765 }
766
767 if slices.Contains(result, "b") {
768 t.Error("Expected 'b' to be removed from slice")
769 }
770
771 if !slices.Contains(result, "a") || !slices.Contains(result, "c") {
772 t.Error("Expected 'a' and 'c' to remain in slice")
773 }
774 })
775
776 t.Run("parseDescription extracts project", func(t *testing.T) {
777 parsed := parseDescription("Buy groceries +shopping")
778
779 if parsed.Description != "Buy groceries" {
780 t.Errorf("Expected description 'Buy groceries', got '%s'", parsed.Description)
781 }
782 if parsed.Project != "shopping" {
783 t.Errorf("Expected project 'shopping', got '%s'", parsed.Project)
784 }
785 })
786
787 t.Run("parseDescription extracts context", func(t *testing.T) {
788 parsed := parseDescription("Call boss @work")
789
790 if parsed.Description != "Call boss" {
791 t.Errorf("Expected description 'Call boss', got '%s'", parsed.Description)
792 }
793 if parsed.Context != "work" {
794 t.Errorf("Expected context 'work', got '%s'", parsed.Context)
795 }
796 })
797
798 t.Run("parseDescription extracts tags", func(t *testing.T) {
799 parsed := parseDescription("Fix bug #urgent #backend")
800
801 if parsed.Description != "Fix bug" {
802 t.Errorf("Expected description 'Fix bug', got '%s'", parsed.Description)
803 }
804 if len(parsed.Tags) != 2 {
805 t.Errorf("Expected 2 tags, got %d", len(parsed.Tags))
806 }
807 if parsed.Tags[0] != "urgent" || parsed.Tags[1] != "backend" {
808 t.Errorf("Expected tags 'urgent' and 'backend', got %v", parsed.Tags)
809 }
810 })
811
812 t.Run("parseDescription extracts due date", func(t *testing.T) {
813 parsed := parseDescription("Submit report due:2024-12-31")
814
815 if parsed.Description != "Submit report" {
816 t.Errorf("Expected description 'Submit report', got '%s'", parsed.Description)
817 }
818 if parsed.Due != "2024-12-31" {
819 t.Errorf("Expected due '2024-12-31', got '%s'", parsed.Due)
820 }
821 })
822
823 t.Run("parseDescription extracts recurrence", func(t *testing.T) {
824 parsed := parseDescription("Weekly meeting recur:FREQ=WEEKLY")
825
826 if parsed.Description != "Weekly meeting" {
827 t.Errorf("Expected description 'Weekly meeting', got '%s'", parsed.Description)
828 }
829 if parsed.Recur != "FREQ=WEEKLY" {
830 t.Errorf("Expected recur 'FREQ=WEEKLY', got '%s'", parsed.Recur)
831 }
832 })
833
834 t.Run("parseDescription extracts until date", func(t *testing.T) {
835 parsed := parseDescription("Daily standup until:2024-12-31")
836
837 if parsed.Description != "Daily standup" {
838 t.Errorf("Expected description 'Daily standup', got '%s'", parsed.Description)
839 }
840 if parsed.Until != "2024-12-31" {
841 t.Errorf("Expected until '2024-12-31', got '%s'", parsed.Until)
842 }
843 })
844
845 t.Run("parseDescription extracts parent UUID", func(t *testing.T) {
846 parentUUID := "550e8400-e29b-41d4-a716-446655440000"
847 text := "Subtask parent:" + parentUUID
848 parsed := parseDescription(text)
849
850 if parsed.Description != "Subtask" {
851 t.Errorf("Expected description 'Subtask', got '%s'", parsed.Description)
852 }
853 if parsed.ParentUUID != parentUUID {
854 t.Errorf("Expected parent UUID '%s', got '%s'", parentUUID, parsed.ParentUUID)
855 }
856 })
857
858 t.Run("parseDescription extracts dependencies", func(t *testing.T) {
859 uuid1 := "550e8400-e29b-41d4-a716-446655440000"
860 uuid2 := "660e8400-e29b-41d4-a716-446655440001"
861 text := "Task with deps depends:" + uuid1 + "," + uuid2
862 parsed := parseDescription(text)
863
864 if parsed.Description != "Task with deps" {
865 t.Errorf("Expected description 'Task with deps', got '%s'", parsed.Description)
866 }
867 if len(parsed.DependsOn) != 2 {
868 t.Errorf("Expected 2 dependencies, got %d", len(parsed.DependsOn))
869 }
870 if parsed.DependsOn[0] != uuid1 || parsed.DependsOn[1] != uuid2 {
871 t.Errorf("Expected dependencies [%s, %s], got %v", uuid1, uuid2, parsed.DependsOn)
872 }
873 })
874
875 t.Run("parseDescription extracts all metadata", func(t *testing.T) {
876 text := "Complex task +project @context #tag1 #tag2 due:2024-12-31 recur:FREQ=DAILY until:2025-01-31"
877 parsed := parseDescription(text)
878
879 if parsed.Description != "Complex task" {
880 t.Errorf("Expected description 'Complex task', got '%s'", parsed.Description)
881 }
882 if parsed.Project != "project" {
883 t.Errorf("Expected project 'project', got '%s'", parsed.Project)
884 }
885 if parsed.Context != "context" {
886 t.Errorf("Expected context 'context', got '%s'", parsed.Context)
887 }
888 if len(parsed.Tags) != 2 {
889 t.Errorf("Expected 2 tags, got %d", len(parsed.Tags))
890 }
891 if parsed.Due != "2024-12-31" {
892 t.Errorf("Expected due '2024-12-31', got '%s'", parsed.Due)
893 }
894 if parsed.Recur != "FREQ=DAILY" {
895 t.Errorf("Expected recur 'FREQ=DAILY', got '%s'", parsed.Recur)
896 }
897 if parsed.Until != "2025-01-31" {
898 t.Errorf("Expected until '2025-01-31', got '%s'", parsed.Until)
899 }
900 })
901
902 t.Run("parseDescription handles plain text without metadata", func(t *testing.T) {
903 parsed := parseDescription("Just a simple task")
904
905 if parsed.Description != "Just a simple task" {
906 t.Errorf("Expected description 'Just a simple task', got '%s'", parsed.Description)
907 }
908 if parsed.Project != "" {
909 t.Errorf("Expected empty project, got '%s'", parsed.Project)
910 }
911 if parsed.Context != "" {
912 t.Errorf("Expected empty context, got '%s'", parsed.Context)
913 }
914 if len(parsed.Tags) != 0 {
915 t.Errorf("Expected no tags, got %d", len(parsed.Tags))
916 }
917 })
918 })
919
920 t.Run("Print", func(t *testing.T) {
921 suite := NewHandlerTestSuite(t)
922 defer suite.cleanup()
923
924 handler, err := NewTaskHandler()
925 if err != nil {
926 t.Fatalf("Failed to create handler: %v", err)
927 }
928 defer handler.Close()
929
930 now := time.Now()
931 due := now.Add(24 * time.Hour)
932
933 task := &models.Task{
934 ID: 1,
935 UUID: uuid.New().String(),
936 Description: "Test task",
937 Status: "pending",
938 Priority: "A",
939 Project: "test",
940 Tags: []string{"work", "urgent"},
941 Due: &due,
942 Entry: now,
943 Modified: now,
944 }
945
946 t.Run("printTask outputs basic fields", func(t *testing.T) {
947 var buf bytes.Buffer
948 oldStdout := os.Stdout
949 r, w, _ := os.Pipe()
950 os.Stdout = w
951
952 outputChan := make(chan string, 1)
953 go func() {
954 buf.ReadFrom(r)
955 outputChan <- buf.String()
956 }()
957
958 printTask(task)
959 w.Close()
960 os.Stdout = oldStdout
961 output := <-outputChan
962
963 if !strings.Contains(output, "Test task") {
964 t.Error("Output should contain task description")
965 }
966 if !strings.Contains(output, "[A]") {
967 t.Error("Output should contain priority")
968 }
969 if !strings.Contains(output, "+test") {
970 t.Error("Output should contain project")
971 }
972 })
973
974 t.Run("printTask outputs context", func(t *testing.T) {
975 taskWithContext := &models.Task{
976 ID: 1,
977 UUID: uuid.New().String(),
978 Description: "Test task",
979 Status: "pending",
980 Context: "work",
981 }
982
983 var buf bytes.Buffer
984 oldStdout := os.Stdout
985 r, w, _ := os.Pipe()
986 os.Stdout = w
987
988 outputChan := make(chan string, 1)
989 go func() {
990 buf.ReadFrom(r)
991 outputChan <- buf.String()
992 }()
993
994 printTask(taskWithContext)
995 w.Close()
996 os.Stdout = oldStdout
997 output := <-outputChan
998
999 if !strings.Contains(output, "@work") {
1000 t.Error("Output should contain context")
1001 }
1002 })
1003
1004 t.Run("printTask outputs recur indicator", func(t *testing.T) {
1005 taskWithRecur := &models.Task{
1006 ID: 1,
1007 UUID: uuid.New().String(),
1008 Description: "Recurring task",
1009 Status: "pending",
1010 Recur: "FREQ=DAILY",
1011 }
1012
1013 var buf bytes.Buffer
1014 oldStdout := os.Stdout
1015 r, w, _ := os.Pipe()
1016 os.Stdout = w
1017
1018 outputChan := make(chan string, 1)
1019 go func() {
1020 buf.ReadFrom(r)
1021 outputChan <- buf.String()
1022 }()
1023
1024 printTask(taskWithRecur)
1025 w.Close()
1026 os.Stdout = oldStdout
1027 output := <-outputChan
1028
1029 if !strings.Contains(output, "\u21bb") {
1030 t.Error("Output should contain recurrence indicator")
1031 }
1032 })
1033
1034 t.Run("printTask outputs dependency count", func(t *testing.T) {
1035 taskWithDeps := &models.Task{
1036 ID: 1,
1037 UUID: uuid.New().String(),
1038 Description: "Task with dependencies",
1039 Status: "pending",
1040 DependsOn: []string{"uuid1", "uuid2"},
1041 }
1042
1043 var buf bytes.Buffer
1044 oldStdout := os.Stdout
1045 r, w, _ := os.Pipe()
1046 os.Stdout = w
1047
1048 outputChan := make(chan string, 1)
1049 go func() {
1050 buf.ReadFrom(r)
1051 outputChan <- buf.String()
1052 }()
1053
1054 printTask(taskWithDeps)
1055 w.Close()
1056 os.Stdout = oldStdout
1057 output := <-outputChan
1058
1059 if !strings.Contains(output, "\u29372") {
1060 t.Error("Output should contain dependency count")
1061 }
1062 })
1063
1064 t.Run("printTaskDetail outputs context", func(t *testing.T) {
1065 taskWithContext := &models.Task{
1066 ID: 1,
1067 UUID: uuid.New().String(),
1068 Description: "Test task",
1069 Status: "pending",
1070 Context: "office",
1071 Entry: now,
1072 Modified: now,
1073 }
1074
1075 var buf bytes.Buffer
1076 oldStdout := os.Stdout
1077 r, w, _ := os.Pipe()
1078 os.Stdout = w
1079
1080 outputChan := make(chan string, 1)
1081 go func() {
1082 buf.ReadFrom(r)
1083 outputChan <- buf.String()
1084 }()
1085
1086 printTaskDetail(taskWithContext, false)
1087 w.Close()
1088 os.Stdout = oldStdout
1089 output := <-outputChan
1090
1091 if !strings.Contains(output, "Context: office") {
1092 t.Error("Output should contain context field")
1093 }
1094 })
1095
1096 t.Run("printTaskDetail outputs recurrence", func(t *testing.T) {
1097 taskWithRecur := &models.Task{
1098 ID: 1,
1099 UUID: uuid.New().String(),
1100 Description: "Recurring task",
1101 Status: "pending",
1102 Recur: "FREQ=WEEKLY",
1103 Entry: now,
1104 Modified: now,
1105 }
1106
1107 var buf bytes.Buffer
1108 oldStdout := os.Stdout
1109 r, w, _ := os.Pipe()
1110 os.Stdout = w
1111
1112 outputChan := make(chan string, 1)
1113 go func() {
1114 buf.ReadFrom(r)
1115 outputChan <- buf.String()
1116 }()
1117
1118 printTaskDetail(taskWithRecur, false)
1119 w.Close()
1120 os.Stdout = oldStdout
1121 output := <-outputChan
1122
1123 if !strings.Contains(output, "Recurrence: FREQ=WEEKLY") {
1124 t.Error("Output should contain recurrence field")
1125 }
1126 })
1127
1128 t.Run("printTaskDetail outputs until date", func(t *testing.T) {
1129 until := now.Add(30 * 24 * time.Hour)
1130 taskWithUntil := &models.Task{
1131 ID: 1,
1132 UUID: uuid.New().String(),
1133 Description: "Task with until",
1134 Status: "pending",
1135 Until: &until,
1136 Entry: now,
1137 Modified: now,
1138 }
1139
1140 var buf bytes.Buffer
1141 oldStdout := os.Stdout
1142 r, w, _ := os.Pipe()
1143 os.Stdout = w
1144
1145 outputChan := make(chan string, 1)
1146 go func() {
1147 buf.ReadFrom(r)
1148 outputChan <- buf.String()
1149 }()
1150
1151 printTaskDetail(taskWithUntil, false)
1152 w.Close()
1153 os.Stdout = oldStdout
1154 output := <-outputChan
1155
1156 if !strings.Contains(output, "Recur Until:") {
1157 t.Error("Output should contain recur until field")
1158 }
1159 })
1160
1161 t.Run("printTaskDetail outputs parent UUID", func(t *testing.T) {
1162 parentUUID := "550e8400-e29b-41d4-a716-446655440000"
1163 taskWithParent := &models.Task{
1164 ID: 1,
1165 UUID: uuid.New().String(),
1166 Description: "Subtask",
1167 Status: "pending",
1168 ParentUUID: &parentUUID,
1169 Entry: now,
1170 Modified: now,
1171 }
1172
1173 var buf bytes.Buffer
1174 oldStdout := os.Stdout
1175 r, w, _ := os.Pipe()
1176 os.Stdout = w
1177
1178 outputChan := make(chan string, 1)
1179 go func() {
1180 buf.ReadFrom(r)
1181 outputChan <- buf.String()
1182 }()
1183
1184 printTaskDetail(taskWithParent, false)
1185 w.Close()
1186 os.Stdout = oldStdout
1187 output := <-outputChan
1188
1189 if !strings.Contains(output, "Parent Task:") {
1190 t.Error("Output should contain parent task field")
1191 }
1192 if !strings.Contains(output, parentUUID) {
1193 t.Error("Output should contain parent UUID")
1194 }
1195 })
1196
1197 t.Run("printTaskDetail outputs dependencies", func(t *testing.T) {
1198 taskWithDeps := &models.Task{
1199 ID: 1,
1200 UUID: uuid.New().String(),
1201 Description: "Task with deps",
1202 Status: "pending",
1203 DependsOn: []string{"uuid1", "uuid2"},
1204 Entry: now,
1205 Modified: now,
1206 }
1207
1208 var buf bytes.Buffer
1209 oldStdout := os.Stdout
1210 r, w, _ := os.Pipe()
1211 os.Stdout = w
1212
1213 outputChan := make(chan string, 1)
1214 go func() {
1215 buf.ReadFrom(r)
1216 outputChan <- buf.String()
1217 }()
1218
1219 printTaskDetail(taskWithDeps, false)
1220 w.Close()
1221 os.Stdout = oldStdout
1222 output := <-outputChan
1223
1224 if !strings.Contains(output, "Depends On:") {
1225 t.Error("Output should contain depends on field")
1226 }
1227 if !strings.Contains(output, "uuid1") || !strings.Contains(output, "uuid2") {
1228 t.Error("Output should contain dependency UUIDs")
1229 }
1230 })
1231
1232 t.Run("printTaskDetail outputs start time", func(t *testing.T) {
1233 start := now.Add(-1 * time.Hour)
1234 taskWithStart := &models.Task{
1235 ID: 1,
1236 UUID: uuid.New().String(),
1237 Description: "Started task",
1238 Status: "pending",
1239 Start: &start,
1240 Entry: now,
1241 Modified: now,
1242 }
1243
1244 var buf bytes.Buffer
1245 oldStdout := os.Stdout
1246 r, w, _ := os.Pipe()
1247 os.Stdout = w
1248
1249 outputChan := make(chan string, 1)
1250 go func() {
1251 buf.ReadFrom(r)
1252 outputChan <- buf.String()
1253 }()
1254
1255 printTaskDetail(taskWithStart, false)
1256 w.Close()
1257 os.Stdout = oldStdout
1258 output := <-outputChan
1259
1260 if !strings.Contains(output, "Started:") {
1261 t.Error("Output should contain started field")
1262 }
1263 })
1264
1265 t.Run("printTaskDetail outputs end time", func(t *testing.T) {
1266 end := now.Add(-1 * time.Hour)
1267 taskWithEnd := &models.Task{
1268 ID: 1,
1269 UUID: uuid.New().String(),
1270 Description: "Completed task",
1271 Status: "completed",
1272 End: &end,
1273 Entry: now,
1274 Modified: now,
1275 }
1276
1277 var buf bytes.Buffer
1278 oldStdout := os.Stdout
1279 r, w, _ := os.Pipe()
1280 os.Stdout = w
1281
1282 outputChan := make(chan string, 1)
1283 go func() {
1284 buf.ReadFrom(r)
1285 outputChan <- buf.String()
1286 }()
1287
1288 printTaskDetail(taskWithEnd, false)
1289 w.Close()
1290 os.Stdout = oldStdout
1291 output := <-outputChan
1292
1293 if !strings.Contains(output, "Completed:") {
1294 t.Error("Output should contain completed field")
1295 }
1296 })
1297
1298 t.Run("printTaskDetail outputs annotations", func(t *testing.T) {
1299 taskWithAnnotations := &models.Task{
1300 ID: 1,
1301 UUID: uuid.New().String(),
1302 Description: "Task with notes",
1303 Status: "pending",
1304 Annotations: []string{"Note 1", "Note 2"},
1305 Entry: now,
1306 Modified: now,
1307 }
1308
1309 var buf bytes.Buffer
1310 oldStdout := os.Stdout
1311 r, w, _ := os.Pipe()
1312 os.Stdout = w
1313
1314 outputChan := make(chan string, 1)
1315 go func() {
1316 buf.ReadFrom(r)
1317 outputChan <- buf.String()
1318 }()
1319
1320 printTaskDetail(taskWithAnnotations, false)
1321 w.Close()
1322 os.Stdout = oldStdout
1323 output := <-outputChan
1324
1325 if !strings.Contains(output, "Annotations:") {
1326 t.Error("Output should contain annotations field")
1327 }
1328 if !strings.Contains(output, "Note 1") || !strings.Contains(output, "Note 2") {
1329 t.Error("Output should contain annotation texts")
1330 }
1331 })
1332 })
1333
1334 t.Run("ListProjects", func(t *testing.T) {
1335 suite := NewHandlerTestSuite(t)
1336 defer suite.cleanup()
1337
1338 handler, err := NewTaskHandler()
1339 if err != nil {
1340 t.Fatalf("Failed to create handler: %v", err)
1341 }
1342 defer handler.Close()
1343
1344 tasks := []*models.Task{
1345 {UUID: uuid.New().String(), Description: "Task 1", Status: "pending", Project: "web-app"},
1346 {UUID: uuid.New().String(), Description: "Task 2", Status: "completed", Project: "web-app"},
1347 {UUID: uuid.New().String(), Description: "Task 3", Status: "pending", Project: "mobile-app"},
1348 {UUID: uuid.New().String(), Description: "Task 4", Status: "pending", Project: ""},
1349 }
1350
1351 for _, task := range tasks {
1352 _, err := handler.repos.Tasks.Create(ctx, task)
1353 if err != nil {
1354 t.Fatalf("Failed to create task: %v", err)
1355 }
1356 }
1357
1358 t.Run("lists projects successfully", func(t *testing.T) {
1359 err := handler.ListProjects(ctx, true)
1360 if err != nil {
1361 t.Errorf("ListProjects failed: %v", err)
1362 }
1363 })
1364
1365 t.Run("returns no projects when none exist", func(t *testing.T) {
1366 suite := NewHandlerTestSuite(t)
1367 defer suite.cleanup()
1368
1369 err := handler.ListProjects(ctx, true)
1370 if err != nil {
1371 t.Errorf("ListProjects with no projects failed: %v", err)
1372 }
1373 })
1374
1375 t.Run("fails when repository List fails", func(t *testing.T) {
1376 cancelCtx, cancel := context.WithCancel(context.Background())
1377 cancel()
1378
1379 err := handler.ListProjects(cancelCtx, true)
1380 if err == nil {
1381 t.Error("Expected error when repository List fails")
1382 }
1383
1384 if !strings.Contains(err.Error(), "failed to list tasks for projects") {
1385 t.Errorf("Expected 'failed to list tasks for projects' error, got: %v", err)
1386 }
1387 })
1388 })
1389
1390 t.Run("ListTags", func(t *testing.T) {
1391 suite := NewHandlerTestSuite(t)
1392 defer suite.cleanup()
1393
1394 handler, err := NewTaskHandler()
1395 if err != nil {
1396 t.Fatalf("Failed to create handler: %v", err)
1397 }
1398 defer handler.Close()
1399
1400 tasks := []*models.Task{
1401 {UUID: uuid.New().String(), Description: "Task 1", Status: "pending", Tags: []string{"frontend", "urgent"}},
1402 {UUID: uuid.New().String(), Description: "Task 2", Status: "completed", Tags: []string{"backend", "database"}},
1403 {UUID: uuid.New().String(), Description: "Task 3", Status: "pending", Tags: []string{"frontend", "ios"}},
1404 {UUID: uuid.New().String(), Description: "Task 4", Status: "pending", Tags: []string{}},
1405 }
1406
1407 for _, task := range tasks {
1408 _, err := handler.repos.Tasks.Create(ctx, task)
1409 if err != nil {
1410 t.Fatalf("Failed to create task: %v", err)
1411 }
1412 }
1413
1414 t.Run("lists tags successfully", func(t *testing.T) {
1415 err := handler.ListTags(ctx, true)
1416 if err != nil {
1417 t.Errorf("ListTags failed: %v", err)
1418 }
1419 })
1420
1421 t.Run("returns no tags when none exist", func(t *testing.T) {
1422 suite := NewHandlerTestSuite(t)
1423 defer suite.cleanup()
1424
1425 err := handler.ListTags(ctx, true)
1426 if err != nil {
1427 t.Errorf("ListTags with no tags failed: %v", err)
1428 }
1429 })
1430
1431 t.Run("fails when repository List fails", func(t *testing.T) {
1432 cancelCtx, cancel := context.WithCancel(context.Background())
1433 cancel()
1434
1435 err := handler.ListTags(cancelCtx, true)
1436 if err == nil {
1437 t.Error("Expected error when repository List fails")
1438 }
1439
1440 if !strings.Contains(err.Error(), "failed to list tasks for tags") {
1441 t.Errorf("Expected 'failed to list tasks for tags' error, got: %v", err)
1442 }
1443 })
1444 })
1445
1446 t.Run("Pluralize", func(t *testing.T) {
1447 t.Run("returns empty string for singular", func(t *testing.T) {
1448 result := pluralize(1)
1449 if result != "" {
1450 t.Errorf("Expected empty string for 1, got '%s'", result)
1451 }
1452 })
1453
1454 t.Run("returns 's' for plural", func(t *testing.T) {
1455 result := pluralize(0)
1456 if result != "s" {
1457 t.Errorf("Expected 's' for 0, got '%s'", result)
1458 }
1459
1460 result = pluralize(2)
1461 if result != "s" {
1462 t.Errorf("Expected 's' for 2, got '%s'", result)
1463 }
1464
1465 result = pluralize(10)
1466 if result != "s" {
1467 t.Errorf("Expected 's' for 10, got '%s'", result)
1468 }
1469 })
1470 })
1471
1472 t.Run("InteractiveComponentsStatic", func(t *testing.T) {
1473 suite := NewHandlerTestSuite(t)
1474 defer suite.cleanup()
1475
1476 handler, err := NewTaskHandler()
1477 if err != nil {
1478 t.Fatalf("Failed to create task handler: %v", err)
1479 }
1480 defer handler.Close()
1481
1482 err = handler.Create(ctx, "Test Task 1", "high", "test-project", "test-context", "", "", "", "", "", "", "", []string{"tag1"})
1483 if err != nil {
1484 t.Fatalf("Failed to create test task: %v", err)
1485 }
1486
1487 err = handler.Create(ctx, "Test Task 2", "medium", "test-project", "test-context", "", "", "", "", "", "", "", []string{"tag2"})
1488 if err != nil {
1489 t.Fatalf("Failed to create test task: %v", err)
1490 }
1491
1492 t.Run("taskListStaticMode", func(t *testing.T) {
1493 var output bytes.Buffer
1494
1495 t.Run("lists all tasks", func(t *testing.T) {
1496 output.Reset()
1497 taskTable := ui.NewTaskListFromTable(handler.repos.Tasks, &output, os.Stdin, true, true, "", "", "")
1498 err := taskTable.Browse(ctx)
1499 if err != nil {
1500 t.Errorf("Static task list should succeed: %v", err)
1501 }
1502 if !strings.Contains(output.String(), "Test Task 1") {
1503 t.Error("Output should contain Test Task 1")
1504 }
1505 if !strings.Contains(output.String(), "Test Task 2") {
1506 t.Error("Output should contain Test Task 2")
1507 }
1508 })
1509
1510 t.Run("filters by status", func(t *testing.T) {
1511 output.Reset()
1512 taskTable := ui.NewTaskListFromTable(handler.repos.Tasks, &output, os.Stdin, true, false, "pending", "", "")
1513 err := taskTable.Browse(ctx)
1514 if err != nil {
1515 t.Errorf("Static task list with status filter should succeed: %v", err)
1516 }
1517 })
1518
1519 t.Run("filters by priority", func(t *testing.T) {
1520 output.Reset()
1521 taskTable := ui.NewTaskListFromTable(handler.repos.Tasks, &output, os.Stdin, true, false, "", "high", "")
1522 err := taskTable.Browse(ctx)
1523 if err != nil {
1524 t.Errorf("Static task list with priority filter should succeed: %v", err)
1525 }
1526 })
1527
1528 t.Run("filters by project", func(t *testing.T) {
1529 output.Reset()
1530 taskTable := ui.NewTaskListFromTable(handler.repos.Tasks, &output, os.Stdin, true, false, "", "", "test-project")
1531 err := taskTable.Browse(ctx)
1532 if err != nil {
1533 t.Errorf("Static task list with project filter should succeed: %v", err)
1534 }
1535 })
1536 })
1537
1538 t.Run("projectListStaticMode", func(t *testing.T) {
1539 var output bytes.Buffer
1540
1541 t.Run("lists projects", func(t *testing.T) {
1542 output.Reset()
1543 projectTable := ui.NewProjectListFromTable(handler.repos.Tasks, &output, os.Stdin, true)
1544 err := projectTable.Browse(ctx)
1545 if err != nil {
1546 t.Errorf("Static project list should succeed: %v", err)
1547 }
1548 if !strings.Contains(output.String(), "test-project") {
1549 t.Error("Output should contain test-project")
1550 }
1551 })
1552 })
1553
1554 t.Run("tagListStaticMode", func(t *testing.T) {
1555 var output bytes.Buffer
1556
1557 t.Run("lists tags", func(t *testing.T) {
1558 output.Reset()
1559 tagTable := ui.NewTagListFromTable(handler.repos.Tasks, &output, os.Stdin, true)
1560 err := tagTable.Browse(ctx)
1561 if err != nil {
1562 t.Errorf("Static tag list should succeed: %v", err)
1563 }
1564 if !strings.Contains(output.String(), "tag1") {
1565 t.Error("Output should contain tag1")
1566 }
1567 })
1568 })
1569
1570 t.Run("contextListStaticMode", func(t *testing.T) {
1571 oldStdout := os.Stdout
1572 defer func() { os.Stdout = oldStdout }()
1573
1574 r, w, _ := os.Pipe()
1575 os.Stdout = w
1576
1577 outputChan := make(chan string, 1)
1578 go func() {
1579 var buf bytes.Buffer
1580 buf.ReadFrom(r)
1581 outputChan <- buf.String()
1582 }()
1583
1584 t.Run("lists contexts with tasks", func(t *testing.T) {
1585 err := handler.listContextsStatic(ctx, false)
1586 w.Close()
1587 capturedOutput := <-outputChan
1588
1589 if err != nil {
1590 t.Errorf("listContextsStatic should succeed: %v", err)
1591 }
1592 if !strings.Contains(capturedOutput, "test-context") {
1593 t.Error("Output should contain 'test-context' context")
1594 }
1595 })
1596
1597 r, w, _ = os.Pipe()
1598 os.Stdout = w
1599 go func() {
1600 var buf bytes.Buffer
1601 buf.ReadFrom(r)
1602 outputChan <- buf.String()
1603 }()
1604
1605 t.Run("lists contexts with todo.txt format", func(t *testing.T) {
1606 err := handler.listContextsStatic(ctx, true)
1607 w.Close()
1608 capturedOutput := <-outputChan
1609
1610 if err != nil {
1611 t.Errorf("listContextsStatic with todoTxt should succeed: %v", err)
1612 }
1613 if !strings.Contains(capturedOutput, "@test-context") {
1614 t.Error("Output should contain '@test-context' in todo.txt format")
1615 }
1616 })
1617
1618 t.Run("handles no contexts", func(t *testing.T) {
1619 suite := NewHandlerTestSuite(t)
1620 defer suite.cleanup()
1621
1622 handler2, err := NewTaskHandler()
1623 if err != nil {
1624 t.Fatalf("Failed to create handler: %v", err)
1625 }
1626 defer handler2.Close()
1627
1628 r, w, _ = os.Pipe()
1629 os.Stdout = w
1630 go func() {
1631 var buf bytes.Buffer
1632 buf.ReadFrom(r)
1633 outputChan <- buf.String()
1634 }()
1635
1636 err = handler2.listContextsStatic(ctx, false)
1637 w.Close()
1638 capturedOutput := <-outputChan
1639
1640 if err != nil {
1641 t.Errorf("listContextsStatic with no contexts should succeed: %v", err)
1642 }
1643 if !strings.Contains(capturedOutput, "No contexts found") {
1644 t.Error("Output should contain 'No contexts found'")
1645 }
1646 })
1647
1648 t.Run("handles repository error", func(t *testing.T) {
1649 cancelCtx, cancel := context.WithCancel(ctx)
1650 cancel()
1651
1652 err := handler.listContextsStatic(cancelCtx, false)
1653 if err == nil {
1654 t.Error("Expected error with cancelled context")
1655 }
1656 if !strings.Contains(err.Error(), "failed to list tasks for contexts") {
1657 t.Errorf("Expected specific error message, got: %v", err)
1658 }
1659 })
1660
1661 t.Run("counts tasks per context correctly", func(t *testing.T) {
1662 r, w, _ = os.Pipe()
1663 os.Stdout = w
1664 go func() {
1665 var buf bytes.Buffer
1666 buf.ReadFrom(r)
1667 outputChan <- buf.String()
1668 }()
1669
1670 err := handler.listContextsStatic(ctx, false)
1671 w.Close()
1672 capturedOutput := <-outputChan
1673
1674 if err != nil {
1675 t.Errorf("listContextsStatic should succeed: %v", err)
1676 }
1677
1678 if !strings.Contains(capturedOutput, "test-context (2 tasks)") {
1679 t.Error("Output should show correct count for test-context context")
1680 }
1681 })
1682 })
1683 })
1684
1685 t.Run("ListContexts", func(t *testing.T) {
1686 suite := NewHandlerTestSuite(t)
1687 defer suite.cleanup()
1688
1689 handler, err := NewTaskHandler()
1690 if err != nil {
1691 t.Fatalf("Failed to create handler: %v", err)
1692 }
1693 defer handler.Close()
1694
1695 tasks := []*models.Task{
1696 {UUID: uuid.New().String(), Description: "Task with context 1", Status: "pending", Context: "test-context"},
1697 {UUID: uuid.New().String(), Description: "Task with context 2", Status: "pending", Context: "work-context"},
1698 {UUID: uuid.New().String(), Description: "Task without context", Status: "pending"},
1699 }
1700
1701 for _, task := range tasks {
1702 _, err := handler.repos.Tasks.Create(ctx, task)
1703 if err != nil {
1704 t.Fatalf("Failed to create task: %v", err)
1705 }
1706 }
1707
1708 t.Run("lists contexts in static mode", func(t *testing.T) {
1709 err := handler.ListContexts(ctx, true)
1710 if err != nil {
1711 t.Errorf("ListContexts static mode failed: %v", err)
1712 }
1713 })
1714
1715 t.Run("lists contexts in interactive mode (falls back to static)", func(t *testing.T) {
1716 err := handler.ListContexts(ctx, false)
1717 if err != nil {
1718 t.Errorf("ListContexts interactive mode failed: %v", err)
1719 }
1720 })
1721
1722 t.Run("lists contexts with todoTxt flag true", func(t *testing.T) {
1723 err := handler.ListContexts(ctx, true, true)
1724 if err != nil {
1725 t.Errorf("ListContexts with todoTxt=true failed: %v", err)
1726 }
1727 })
1728
1729 t.Run("lists contexts with todoTxt flag false", func(t *testing.T) {
1730 err := handler.ListContexts(ctx, true, false)
1731 if err != nil {
1732 t.Errorf("ListContexts with todoTxt=false failed: %v", err)
1733 }
1734 })
1735
1736 t.Run("handles database error in static mode", func(t *testing.T) {
1737 cancelCtx, cancel := context.WithCancel(ctx)
1738 cancel()
1739
1740 err := handler.ListContexts(cancelCtx, true)
1741 if err == nil {
1742 t.Error("Expected error with cancelled context in static mode")
1743 }
1744 if !strings.Contains(err.Error(), "failed to list tasks for contexts") {
1745 t.Errorf("Expected specific error message, got: %v", err)
1746 }
1747 })
1748
1749 t.Run("handles database error in interactive mode", func(t *testing.T) {
1750 cancelCtx, cancel := context.WithCancel(ctx)
1751 cancel()
1752
1753 err := handler.ListContexts(cancelCtx, false)
1754 if err == nil {
1755 t.Error("Expected error with cancelled context in interactive mode")
1756 }
1757 if !strings.Contains(err.Error(), "failed to list tasks for contexts") {
1758 t.Errorf("Expected specific error message, got: %v", err)
1759 }
1760 })
1761
1762 t.Run("returns no contexts when none exist", func(t *testing.T) {
1763 suite := NewHandlerTestSuite(t)
1764 defer suite.cleanup()
1765
1766 handler_, err := NewTaskHandler()
1767 if err != nil {
1768 t.Fatalf("Failed to create handler: %v", err)
1769 }
1770 defer handler_.Close()
1771
1772 err = handler_.ListContexts(ctx, true)
1773 if err != nil {
1774 t.Errorf("ListContexts with no contexts failed: %v", err)
1775 }
1776 })
1777 })
1778
1779 t.Run("SetRecur", func(t *testing.T) {
1780 suite := NewHandlerTestSuite(t)
1781 defer suite.cleanup()
1782
1783 handler, err := NewTaskHandler()
1784 if err != nil {
1785 t.Fatalf("Failed to create handler: %v", err)
1786 }
1787 defer handler.Close()
1788
1789 id, err := handler.repos.Tasks.Create(ctx, &models.Task{
1790 UUID: uuid.New().String(), Description: "Test task", Status: "pending",
1791 })
1792 if err != nil {
1793 t.Fatalf("Failed to create task: %v", err)
1794 }
1795
1796 t.Run("sets recurrence rule", func(t *testing.T) {
1797 err := handler.SetRecur(ctx, strconv.FormatInt(id, 10), "FREQ=DAILY", "2025-12-31")
1798 if err != nil {
1799 t.Errorf("SetRecur failed: %v", err)
1800 }
1801
1802 task, err := handler.repos.Tasks.Get(ctx, id)
1803 if err != nil {
1804 t.Fatalf("Failed to get task: %v", err)
1805 }
1806
1807 if task.Recur != "FREQ=DAILY" {
1808 t.Errorf("Expected recur to be 'FREQ=DAILY', got '%s'", task.Recur)
1809 }
1810
1811 if task.Until == nil {
1812 t.Error("Expected until to be set")
1813 }
1814 })
1815
1816 t.Run("handles invalid until date", func(t *testing.T) {
1817 err := handler.SetRecur(ctx, strconv.FormatInt(id, 10), "FREQ=WEEKLY", "invalid-date")
1818 if err == nil {
1819 t.Error("Expected error for invalid until date")
1820 }
1821 })
1822
1823 t.Run("fails when repository Get fails", func(t *testing.T) {
1824 cancelCtx, cancel := context.WithCancel(context.Background())
1825 cancel()
1826
1827 err := handler.SetRecur(cancelCtx, "1", "FREQ=DAILY", "")
1828 if err == nil {
1829 t.Error("Expected error when repository Get fails")
1830 }
1831
1832 if !strings.Contains(err.Error(), "failed to find task") {
1833 t.Errorf("Expected 'failed to find task' error, got: %v", err)
1834 }
1835 })
1836
1837 t.Run("fails with canceled context", func(t *testing.T) {
1838 task := &models.Task{
1839 UUID: uuid.New().String(),
1840 Description: "Test task",
1841 Status: "pending",
1842 }
1843 id, err := handler.repos.Tasks.Create(ctx, task)
1844 if err != nil {
1845 t.Fatalf("Failed to create task: %v", err)
1846 }
1847
1848 cancelCtx, cancel := context.WithCancel(context.Background())
1849 cancel()
1850
1851 err = handler.SetRecur(cancelCtx, strconv.FormatInt(id, 10), "FREQ=DAILY", "")
1852 if err == nil {
1853 t.Error("Expected error with canceled context")
1854 }
1855 })
1856 })
1857
1858 t.Run("ClearRecur", func(t *testing.T) {
1859 suite := NewHandlerTestSuite(t)
1860 defer suite.cleanup()
1861
1862 handler, err := NewTaskHandler()
1863 if err != nil {
1864 t.Fatalf("Failed to create handler: %v", err)
1865 }
1866 defer handler.Close()
1867
1868 until := time.Now()
1869 id, err := handler.repos.Tasks.Create(ctx, &models.Task{
1870 UUID: uuid.New().String(),
1871 Description: "Test task",
1872 Status: "pending",
1873 Recur: "FREQ=DAILY",
1874 Until: &until,
1875 })
1876 if err != nil {
1877 t.Fatalf("Failed to create task: %v", err)
1878 }
1879
1880 if err = handler.ClearRecur(ctx, strconv.FormatInt(id, 10)); err != nil {
1881 t.Errorf("ClearRecur failed: %v", err)
1882 }
1883
1884 task, err := handler.repos.Tasks.Get(ctx, id)
1885 if err != nil {
1886 t.Fatalf("Failed to get task: %v", err)
1887 }
1888
1889 if task.Recur != "" {
1890 t.Errorf("Expected recur to be cleared, got '%s'", task.Recur)
1891 }
1892
1893 if task.Until != nil {
1894 t.Error("Expected until to be cleared")
1895 }
1896
1897 t.Run("fails when repository Get fails", func(t *testing.T) {
1898 cancelCtx, cancel := context.WithCancel(context.Background())
1899 cancel()
1900
1901 err := handler.ClearRecur(cancelCtx, "1")
1902 if err == nil {
1903 t.Error("Expected error when repository Get fails")
1904 }
1905
1906 if !strings.Contains(err.Error(), "failed to find task") {
1907 t.Errorf("Expected 'failed to find task' error, got: %v", err)
1908 }
1909 })
1910
1911 t.Run("fails with canceled context", func(t *testing.T) {
1912 task := &models.Task{
1913 UUID: uuid.New().String(),
1914 Description: "Test task",
1915 Status: "pending",
1916 Recur: "FREQ=DAILY",
1917 }
1918 id, err := handler.repos.Tasks.Create(ctx, task)
1919 if err != nil {
1920 t.Fatalf("Failed to create task: %v", err)
1921 }
1922
1923 cancelCtx, cancel := context.WithCancel(context.Background())
1924 cancel()
1925
1926 if err = handler.ClearRecur(cancelCtx, strconv.FormatInt(id, 10)); err == nil {
1927 t.Error("Expected error with canceled context")
1928 }
1929 })
1930 })
1931
1932 t.Run("ShowRecur", func(t *testing.T) {
1933 suite := NewHandlerTestSuite(t)
1934 defer suite.cleanup()
1935
1936 handler, err := NewTaskHandler()
1937 if err != nil {
1938 t.Fatalf("Failed to create handler: %v", err)
1939 }
1940 defer handler.Close()
1941
1942 until := time.Now()
1943 id, err := handler.repos.Tasks.Create(ctx, &models.Task{
1944 UUID: uuid.New().String(),
1945 Description: "Test task",
1946 Status: "pending",
1947 Recur: "FREQ=DAILY",
1948 Until: &until,
1949 })
1950 if err != nil {
1951 t.Fatalf("Failed to create task: %v", err)
1952 }
1953
1954 err = handler.ShowRecur(ctx, strconv.FormatInt(id, 10))
1955 if err != nil {
1956 t.Errorf("ShowRecur failed: %v", err)
1957 }
1958
1959 t.Run("fails when repository Get fails", func(t *testing.T) {
1960 cancelCtx, cancel := context.WithCancel(context.Background())
1961 cancel()
1962
1963 err := handler.ShowRecur(cancelCtx, "1")
1964 if err == nil {
1965 t.Error("Expected error when repository Get fails")
1966 }
1967
1968 if !strings.Contains(err.Error(), "failed to find task") {
1969 t.Errorf("Expected 'failed to find task' error, got: %v", err)
1970 }
1971 })
1972 })
1973
1974 t.Run("AddDep", func(t *testing.T) {
1975 suite := NewHandlerTestSuite(t)
1976 defer suite.cleanup()
1977
1978 handler, err := NewTaskHandler()
1979 if err != nil {
1980 t.Fatalf("Failed to create handler: %v", err)
1981 }
1982 defer handler.Close()
1983
1984 task1UUID := uuid.New().String()
1985 task2UUID := uuid.New().String()
1986
1987 id1, err := handler.repos.Tasks.Create(ctx, &models.Task{
1988 UUID: task1UUID, Description: "Task 1", Status: "pending",
1989 })
1990 if err != nil {
1991 t.Fatalf("Failed to create task 1: %v", err)
1992 }
1993
1994 if _, err = handler.repos.Tasks.Create(ctx, &models.Task{
1995 UUID: task2UUID, Description: "Task 2", Status: "pending",
1996 }); err != nil {
1997 t.Fatalf("Failed to create task 2: %v", err)
1998 }
1999
2000 err = handler.AddDep(ctx, strconv.FormatInt(id1, 10), task2UUID)
2001 if err != nil {
2002 t.Errorf("AddDep failed: %v", err)
2003 }
2004
2005 task, err := handler.repos.Tasks.Get(ctx, id1)
2006 if err != nil {
2007 t.Fatalf("Failed to get task: %v", err)
2008 }
2009
2010 if len(task.DependsOn) != 1 {
2011 t.Errorf("Expected 1 dependency, got %d", len(task.DependsOn))
2012 }
2013
2014 if task.DependsOn[0] != task2UUID {
2015 t.Errorf("Expected dependency to be '%s', got '%s'", task2UUID, task.DependsOn[0])
2016 }
2017 })
2018
2019 t.Run("RemoveDep", func(t *testing.T) {
2020 suite := NewHandlerTestSuite(t)
2021 defer suite.cleanup()
2022
2023 handler, err := NewTaskHandler()
2024 if err != nil {
2025 t.Fatalf("Failed to create handler: %v", err)
2026 }
2027 defer handler.Close()
2028
2029 task1UUID := uuid.New().String()
2030 task2UUID := uuid.New().String()
2031
2032 _, err = handler.repos.Tasks.Create(ctx, &models.Task{
2033 UUID: task2UUID, Description: "Task 2", Status: "pending",
2034 })
2035 if err != nil {
2036 t.Fatalf("Failed to create task 2: %v", err)
2037 }
2038
2039 id1, err := handler.repos.Tasks.Create(ctx, &models.Task{
2040 UUID: task1UUID,
2041 Description: "Task 1",
2042 Status: "pending",
2043 DependsOn: []string{task2UUID},
2044 })
2045 if err != nil {
2046 t.Fatalf("Failed to create task 1: %v", err)
2047 }
2048
2049 err = handler.RemoveDep(ctx, strconv.FormatInt(id1, 10), task2UUID)
2050 if err != nil {
2051 t.Errorf("RemoveDep failed: %v", err)
2052 }
2053
2054 task, err := handler.repos.Tasks.Get(ctx, id1)
2055 if err != nil {
2056 t.Fatalf("Failed to get task: %v", err)
2057 }
2058
2059 if len(task.DependsOn) != 0 {
2060 t.Errorf("Expected 0 dependencies, got %d", len(task.DependsOn))
2061 }
2062 })
2063
2064 t.Run("ListDeps", func(t *testing.T) {
2065 suite := NewHandlerTestSuite(t)
2066 defer suite.cleanup()
2067
2068 handler, err := NewTaskHandler()
2069 if err != nil {
2070 t.Fatalf("Failed to create handler: %v", err)
2071 }
2072 defer handler.Close()
2073
2074 task1UUID := uuid.New().String()
2075 task2UUID := uuid.New().String()
2076
2077 _, err = handler.repos.Tasks.Create(ctx, &models.Task{
2078 UUID: task2UUID, Description: "Task 2", Status: "pending",
2079 })
2080 if err != nil {
2081 t.Fatalf("Failed to create task 2: %v", err)
2082 }
2083
2084 id1, err := handler.repos.Tasks.Create(ctx, &models.Task{
2085 UUID: task1UUID,
2086 Description: "Task 1",
2087 Status: "pending",
2088 DependsOn: []string{task2UUID},
2089 })
2090 if err != nil {
2091 t.Fatalf("Failed to create task 1: %v", err)
2092 }
2093
2094 err = handler.ListDeps(ctx, strconv.FormatInt(id1, 10))
2095 if err != nil {
2096 t.Errorf("ListDeps failed: %v", err)
2097 }
2098 })
2099
2100 t.Run("BlockedByDep", func(t *testing.T) {
2101 suite := NewHandlerTestSuite(t)
2102 defer suite.cleanup()
2103
2104 handler, err := NewTaskHandler()
2105 if err != nil {
2106 t.Fatalf("Failed to create handler: %v", err)
2107 }
2108 defer handler.Close()
2109
2110 task1UUID := uuid.New().String()
2111 task2UUID := uuid.New().String()
2112
2113 id2, err := handler.repos.Tasks.Create(ctx, &models.Task{UUID: task2UUID, Description: "Task 2", Status: "pending"})
2114 if err != nil {
2115 t.Fatalf("Failed to create task 2: %v", err)
2116 }
2117
2118 if _, err = handler.repos.Tasks.Create(ctx, &models.Task{UUID: task1UUID, Description: "Task 1", Status: "pending", DependsOn: []string{task2UUID}}); err != nil {
2119 t.Fatalf("Failed to create task 1: %v", err)
2120 }
2121
2122 if err = handler.BlockedByDep(ctx, strconv.FormatInt(id2, 10)); err != nil {
2123 t.Errorf("BlockedByDep failed: %v", err)
2124 }
2125 })
2126
2127 t.Run("Annotate", func(t *testing.T) {
2128 suite := NewHandlerTestSuite(t)
2129 defer suite.cleanup()
2130
2131 handler, err := NewTaskHandler()
2132 if err != nil {
2133 t.Fatalf("Failed to create handler: %v", err)
2134 }
2135 defer handler.Close()
2136
2137 id, err := handler.repos.Tasks.Create(ctx, &models.Task{
2138 UUID: uuid.New().String(),
2139 Description: "Test task",
2140 Status: "pending",
2141 })
2142 if err != nil {
2143 t.Fatalf("Failed to create task: %v", err)
2144 }
2145
2146 t.Run("adds annotation successfully", func(t *testing.T) {
2147 err := handler.Annotate(ctx, strconv.FormatInt(id, 10), "First annotation")
2148 shared.AssertNoError(t, err, "Annotate should succeed")
2149
2150 task, err := handler.repos.Tasks.Get(ctx, id)
2151 shared.AssertNoError(t, err, "Get should succeed")
2152 shared.AssertEqual(t, 1, len(task.Annotations), "should have 1 annotation")
2153 shared.AssertEqual(t, "First annotation", task.Annotations[0], "annotation text should match")
2154 })
2155
2156 t.Run("adds multiple annotations", func(t *testing.T) {
2157 err := handler.Annotate(ctx, strconv.FormatInt(id, 10), "Second annotation")
2158 shared.AssertNoError(t, err, "Annotate should succeed")
2159
2160 task, err := handler.repos.Tasks.Get(ctx, id)
2161 shared.AssertNoError(t, err, "Get should succeed")
2162 shared.AssertEqual(t, 2, len(task.Annotations), "should have 2 annotations")
2163 })
2164
2165 t.Run("fails with empty annotation", func(t *testing.T) {
2166 err := handler.Annotate(ctx, strconv.FormatInt(id, 10), "")
2167 shared.AssertError(t, err, "should fail with empty annotation")
2168 shared.AssertContains(t, err.Error(), "annotation text required", "error message")
2169 })
2170
2171 t.Run("fails with invalid task ID", func(t *testing.T) {
2172 err := handler.Annotate(ctx, "99999", "Test annotation")
2173 shared.AssertError(t, err, "should fail with invalid task ID")
2174 shared.AssertContains(t, err.Error(), "failed to find task", "error message")
2175 })
2176
2177 t.Run("works with UUID", func(t *testing.T) {
2178 task := &models.Task{
2179 UUID: uuid.New().String(),
2180 Description: "UUID task",
2181 Status: "pending",
2182 }
2183 _, err := handler.repos.Tasks.Create(ctx, task)
2184 shared.AssertNoError(t, err, "Create should succeed")
2185
2186 err = handler.Annotate(ctx, task.UUID, "UUID annotation")
2187 shared.AssertNoError(t, err, "Annotate with UUID should succeed")
2188
2189 retrieved, err := handler.repos.Tasks.GetByUUID(ctx, task.UUID)
2190 shared.AssertNoError(t, err, "GetByUUID should succeed")
2191 shared.AssertEqual(t, 1, len(retrieved.Annotations), "should have 1 annotation")
2192 })
2193 })
2194
2195 t.Run("ListAnnotations", func(t *testing.T) {
2196 suite := NewHandlerTestSuite(t)
2197 defer suite.cleanup()
2198
2199 handler, err := NewTaskHandler()
2200 if err != nil {
2201 t.Fatalf("Failed to create handler: %v", err)
2202 }
2203 defer handler.Close()
2204
2205 t.Run("lists annotations successfully", func(t *testing.T) {
2206 task := &models.Task{
2207 UUID: uuid.New().String(),
2208 Description: "Test task",
2209 Status: "pending",
2210 Annotations: []string{"Annotation 1", "Annotation 2", "Annotation 3"},
2211 }
2212 id, err := handler.repos.Tasks.Create(ctx, task)
2213 shared.AssertNoError(t, err, "Create should succeed")
2214
2215 err = handler.ListAnnotations(ctx, strconv.FormatInt(id, 10))
2216 shared.AssertNoError(t, err, "ListAnnotations should succeed")
2217 })
2218
2219 t.Run("handles task with no annotations", func(t *testing.T) {
2220 task := &models.Task{
2221 UUID: uuid.New().String(),
2222 Description: "Task without annotations",
2223 Status: "pending",
2224 }
2225 id, err := handler.repos.Tasks.Create(ctx, task)
2226 shared.AssertNoError(t, err, "Create should succeed")
2227
2228 err = handler.ListAnnotations(ctx, strconv.FormatInt(id, 10))
2229 shared.AssertNoError(t, err, "ListAnnotations should succeed for empty annotations")
2230 })
2231
2232 t.Run("fails with invalid task ID", func(t *testing.T) {
2233 err := handler.ListAnnotations(ctx, "99999")
2234 shared.AssertError(t, err, "should fail with invalid task ID")
2235 shared.AssertContains(t, err.Error(), "failed to find task", "error message")
2236 })
2237 })
2238
2239 t.Run("RemoveAnnotation", func(t *testing.T) {
2240 suite := NewHandlerTestSuite(t)
2241 defer suite.cleanup()
2242
2243 handler, err := NewTaskHandler()
2244 if err != nil {
2245 t.Fatalf("Failed to create handler: %v", err)
2246 }
2247 defer handler.Close()
2248
2249 t.Run("removes annotation successfully", func(t *testing.T) {
2250 task := &models.Task{
2251 UUID: uuid.New().String(),
2252 Description: "Test task",
2253 Status: "pending",
2254 Annotations: []string{"First", "Second", "Third"},
2255 }
2256 id, err := handler.repos.Tasks.Create(ctx, task)
2257 shared.AssertNoError(t, err, "Create should succeed")
2258
2259 err = handler.RemoveAnnotation(ctx, strconv.FormatInt(id, 10), 2)
2260 shared.AssertNoError(t, err, "RemoveAnnotation should succeed")
2261
2262 retrieved, err := handler.repos.Tasks.Get(ctx, id)
2263 shared.AssertNoError(t, err, "Get should succeed")
2264 shared.AssertEqual(t, 2, len(retrieved.Annotations), "should have 2 annotations")
2265 shared.AssertEqual(t, "First", retrieved.Annotations[0], "first annotation should remain")
2266 shared.AssertEqual(t, "Third", retrieved.Annotations[1], "third annotation should be second")
2267 })
2268
2269 t.Run("fails with invalid index (too low)", func(t *testing.T) {
2270 task := &models.Task{
2271 UUID: uuid.New().String(),
2272 Description: "Test task",
2273 Status: "pending",
2274 Annotations: []string{"First"},
2275 }
2276 id, err := handler.repos.Tasks.Create(ctx, task)
2277 shared.AssertNoError(t, err, "Create should succeed")
2278
2279 err = handler.RemoveAnnotation(ctx, strconv.FormatInt(id, 10), 0)
2280 shared.AssertError(t, err, "should fail with index 0")
2281 shared.AssertContains(t, err.Error(), "index out of range", "error message")
2282 })
2283
2284 t.Run("fails with invalid index (too high)", func(t *testing.T) {
2285 task := &models.Task{
2286 UUID: uuid.New().String(),
2287 Description: "Test task",
2288 Status: "pending",
2289 Annotations: []string{"First"},
2290 }
2291 id, err := handler.repos.Tasks.Create(ctx, task)
2292 shared.AssertNoError(t, err, "Create should succeed")
2293
2294 err = handler.RemoveAnnotation(ctx, strconv.FormatInt(id, 10), 5)
2295 shared.AssertError(t, err, "should fail with index > len")
2296 shared.AssertContains(t, err.Error(), "index out of range", "error message")
2297 })
2298
2299 t.Run("fails when task has no annotations", func(t *testing.T) {
2300 task := &models.Task{
2301 UUID: uuid.New().String(),
2302 Description: "Task without annotations",
2303 Status: "pending",
2304 }
2305 id, err := handler.repos.Tasks.Create(ctx, task)
2306 shared.AssertNoError(t, err, "Create should succeed")
2307
2308 err = handler.RemoveAnnotation(ctx, strconv.FormatInt(id, 10), 1)
2309 shared.AssertError(t, err, "should fail when no annotations")
2310 shared.AssertContains(t, err.Error(), "has no annotations", "error message")
2311 })
2312
2313 t.Run("fails with invalid task ID", func(t *testing.T) {
2314 err := handler.RemoveAnnotation(ctx, "99999", 1)
2315 shared.AssertError(t, err, "should fail with invalid task ID")
2316 shared.AssertContains(t, err.Error(), "failed to find task", "error message")
2317 })
2318 })
2319
2320 t.Run("BulkEdit", func(t *testing.T) {
2321 suite := NewHandlerTestSuite(t)
2322 defer suite.cleanup()
2323
2324 handler, err := NewTaskHandler()
2325 if err != nil {
2326 t.Fatalf("Failed to create handler: %v", err)
2327 }
2328 defer handler.Close()
2329
2330 t.Run("updates multiple tasks successfully", func(t *testing.T) {
2331 id1, err := handler.repos.Tasks.Create(ctx, &models.Task{
2332 UUID: uuid.New().String(),
2333 Description: "Task 1",
2334 Status: "pending",
2335 })
2336 shared.AssertNoError(t, err, "Create should succeed")
2337
2338 id2, err := handler.repos.Tasks.Create(ctx, &models.Task{
2339 UUID: uuid.New().String(),
2340 Description: "Task 2",
2341 Status: "pending",
2342 })
2343 shared.AssertNoError(t, err, "Create should succeed")
2344
2345 taskIDs := []string{strconv.FormatInt(id1, 10), strconv.FormatInt(id2, 10)}
2346 err = handler.BulkEdit(ctx, taskIDs, "done", "high", "test-project", "", []string{}, false, false)
2347 shared.AssertNoError(t, err, "BulkEdit should succeed")
2348
2349 task1, err := handler.repos.Tasks.Get(ctx, id1)
2350 shared.AssertNoError(t, err, "Get should succeed")
2351 shared.AssertEqual(t, "done", task1.Status, "task 1 status should be updated")
2352 shared.AssertEqual(t, "high", task1.Priority, "task 1 priority should be updated")
2353 shared.AssertEqual(t, "test-project", task1.Project, "task 1 project should be updated")
2354
2355 task2, err := handler.repos.Tasks.Get(ctx, id2)
2356 shared.AssertNoError(t, err, "Get should succeed")
2357 shared.AssertEqual(t, "done", task2.Status, "task 2 status should be updated")
2358 shared.AssertEqual(t, "high", task2.Priority, "task 2 priority should be updated")
2359 shared.AssertEqual(t, "test-project", task2.Project, "task 2 project should be updated")
2360 })
2361
2362 t.Run("updates with tag replacement", func(t *testing.T) {
2363 id1, err := handler.repos.Tasks.Create(ctx, &models.Task{
2364 UUID: uuid.New().String(),
2365 Description: "Task 1",
2366 Status: "pending",
2367 Tags: []string{"old-tag"},
2368 })
2369 shared.AssertNoError(t, err, "Create should succeed")
2370
2371 taskIDs := []string{strconv.FormatInt(id1, 10)}
2372 err = handler.BulkEdit(ctx, taskIDs, "", "", "", "", []string{"new-tag1", "new-tag2"}, false, false)
2373 shared.AssertNoError(t, err, "BulkEdit should succeed")
2374
2375 task, err := handler.repos.Tasks.Get(ctx, id1)
2376 shared.AssertNoError(t, err, "Get should succeed")
2377 shared.AssertEqual(t, 2, len(task.Tags), "should have 2 tags")
2378 shared.AssertTrue(t, slices.Contains(task.Tags, "new-tag1"), "should contain new-tag1")
2379 shared.AssertTrue(t, slices.Contains(task.Tags, "new-tag2"), "should contain new-tag2")
2380 })
2381
2382 t.Run("adds tags with add-tags flag", func(t *testing.T) {
2383 id1, err := handler.repos.Tasks.Create(ctx, &models.Task{
2384 UUID: uuid.New().String(),
2385 Description: "Task 1",
2386 Status: "pending",
2387 Tags: []string{"existing-tag"},
2388 })
2389 shared.AssertNoError(t, err, "Create should succeed")
2390
2391 taskIDs := []string{strconv.FormatInt(id1, 10)}
2392 err = handler.BulkEdit(ctx, taskIDs, "", "", "", "", []string{"new-tag"}, true, false)
2393 shared.AssertNoError(t, err, "BulkEdit should succeed")
2394
2395 task, err := handler.repos.Tasks.Get(ctx, id1)
2396 shared.AssertNoError(t, err, "Get should succeed")
2397 shared.AssertEqual(t, 2, len(task.Tags), "should have 2 tags")
2398 shared.AssertTrue(t, slices.Contains(task.Tags, "existing-tag"), "should contain existing-tag")
2399 shared.AssertTrue(t, slices.Contains(task.Tags, "new-tag"), "should contain new-tag")
2400 })
2401
2402 t.Run("removes tags with remove-tags flag", func(t *testing.T) {
2403 id1, err := handler.repos.Tasks.Create(ctx, &models.Task{
2404 UUID: uuid.New().String(),
2405 Description: "Task 1",
2406 Status: "pending",
2407 Tags: []string{"tag1", "tag2", "tag3"},
2408 })
2409 shared.AssertNoError(t, err, "Create should succeed")
2410
2411 taskIDs := []string{strconv.FormatInt(id1, 10)}
2412 err = handler.BulkEdit(ctx, taskIDs, "", "", "", "", []string{"tag2"}, false, true)
2413 shared.AssertNoError(t, err, "BulkEdit should succeed")
2414
2415 task, err := handler.repos.Tasks.Get(ctx, id1)
2416 shared.AssertNoError(t, err, "Get should succeed")
2417 shared.AssertEqual(t, 2, len(task.Tags), "should have 2 tags")
2418 shared.AssertTrue(t, slices.Contains(task.Tags, "tag1"), "should contain tag1")
2419 shared.AssertTrue(t, slices.Contains(task.Tags, "tag3"), "should contain tag3")
2420 shared.AssertFalse(t, slices.Contains(task.Tags, "tag2"), "should not contain tag2")
2421 })
2422
2423 t.Run("fails with no task IDs", func(t *testing.T) {
2424 err := handler.BulkEdit(ctx, []string{}, "done", "", "", "", []string{}, false, false)
2425 shared.AssertError(t, err, "should fail with no task IDs")
2426 shared.AssertContains(t, err.Error(), "no task IDs provided", "error message")
2427 })
2428
2429 t.Run("fails with invalid task ID", func(t *testing.T) {
2430 err := handler.BulkEdit(ctx, []string{"99999"}, "done", "", "", "", []string{}, false, false)
2431 shared.AssertError(t, err, "should fail with invalid task ID")
2432 })
2433
2434 t.Run("works with UUIDs", func(t *testing.T) {
2435 task1 := &models.Task{
2436 UUID: uuid.New().String(),
2437 Description: "UUID task 1",
2438 Status: "pending",
2439 }
2440 _, err := handler.repos.Tasks.Create(ctx, task1)
2441 shared.AssertNoError(t, err, "Create should succeed")
2442
2443 task2 := &models.Task{
2444 UUID: uuid.New().String(),
2445 Description: "UUID task 2",
2446 Status: "pending",
2447 }
2448 _, err = handler.repos.Tasks.Create(ctx, task2)
2449 shared.AssertNoError(t, err, "Create should succeed")
2450
2451 taskIDs := []string{task1.UUID, task2.UUID}
2452 err = handler.BulkEdit(ctx, taskIDs, "done", "", "", "", []string{}, false, false)
2453 shared.AssertNoError(t, err, "BulkEdit with UUIDs should succeed")
2454
2455 retrieved1, err := handler.repos.Tasks.GetByUUID(ctx, task1.UUID)
2456 shared.AssertNoError(t, err, "GetByUUID should succeed")
2457 shared.AssertEqual(t, "done", retrieved1.Status, "task 1 status should be updated")
2458
2459 retrieved2, err := handler.repos.Tasks.GetByUUID(ctx, task2.UUID)
2460 shared.AssertNoError(t, err, "GetByUUID should succeed")
2461 shared.AssertEqual(t, "done", retrieved2.Status, "task 2 status should be updated")
2462 })
2463 })
2464}