cli + tui to publish to leaflet (wip) & manage tasks, notes & watch/read lists ๐Ÿƒ
charm leaflet readability golang

feat: add task recurrence and dependency management commands

+803 -42
+109 -19
cmd/task_commands.go
··· 24 24 addTaskCmd, listTaskCmd, viewTaskCmd, updateTaskCmd, editTaskCmd, 25 25 deleteTaskCmd, taskProjectsCmd, taskTagsCmd, taskContextsCmd, 26 26 taskCompleteCmd, taskStartCmd, taskStopCmd, timesheetViewCmd, 27 + taskRecurCmd, taskDependCmd, 27 28 } { 28 29 cmd := init(c.handler) 29 30 root.AddCommand(cmd) ··· 54 55 return h.Create(c.Context(), description, priority, project, context, due, recur, until, parent, dependsOn, tags) 55 56 }, 56 57 } 57 - cmd.Flags().StringP("priority", "p", "", "Set task priority") 58 - cmd.Flags().String("project", "", "Set task project") 59 - cmd.Flags().StringP("context", "c", "", "Set task context") 60 - cmd.Flags().StringP("due", "d", "", "Set due date (YYYY-MM-DD)") 61 - cmd.Flags().String("recur", "", "Set recurrence rule (e.g., FREQ=DAILY)") 62 - cmd.Flags().String("until", "", "Set recurrence end date (YYYY-MM-DD)") 63 - cmd.Flags().String("parent", "", "Set parent task UUID") 64 - cmd.Flags().String("depends-on", "", "Set task dependencies (comma-separated UUIDs)") 65 - cmd.Flags().StringSliceP("tags", "t", []string{}, "Add tags to task") 58 + addCommonTaskFlags(cmd) 59 + addDueDateFlag(cmd) 60 + addRecurrenceFlags(cmd) 61 + addParentFlag(cmd) 62 + addDependencyFlags(cmd) 66 63 67 64 return cmd 68 65 } ··· 114 111 return handler.View(cmd.Context(), args, format, jsonOutput, noMetadata) 115 112 }, 116 113 } 117 - viewCmd.Flags().String("format", "detailed", "Output format (detailed, brief)") 118 - viewCmd.Flags().Bool("json", false, "Output as JSON") 119 - viewCmd.Flags().Bool("no-metadata", false, "Hide creation/modification timestamps") 114 + addOutputFlags(viewCmd) 120 115 121 116 return viewCmd 122 117 } ··· 148 143 } 149 144 updateCmd.Flags().String("description", "", "Update task description") 150 145 updateCmd.Flags().String("status", "", "Update task status") 151 - updateCmd.Flags().StringP("priority", "p", "", "Update task priority") 152 - updateCmd.Flags().String("project", "", "Update task project") 153 - updateCmd.Flags().StringP("context", "c", "", "Update task context") 154 - updateCmd.Flags().StringP("due", "d", "", "Update due date (YYYY-MM-DD)") 155 - updateCmd.Flags().String("recur", "", "Update recurrence rule") 156 - updateCmd.Flags().String("until", "", "Update recurrence end date (YYYY-MM-DD)") 157 - updateCmd.Flags().String("parent", "", "Update parent task UUID") 146 + addCommonTaskFlags(updateCmd) 147 + addDueDateFlag(updateCmd) 148 + addRecurrenceFlags(updateCmd) 149 + addParentFlag(updateCmd) 158 150 updateCmd.Flags().StringSlice("add-tag", []string{}, "Add tags to task") 159 151 updateCmd.Flags().StringSlice("remove-tag", []string{}, "Remove tags from task") 160 152 updateCmd.Flags().String("add-depends", "", "Add task dependencies (comma-separated UUIDs)") ··· 305 297 }, 306 298 } 307 299 } 300 + 301 + func taskRecurCmd(h *handlers.TaskHandler) *cobra.Command { 302 + root := &cobra.Command{ 303 + Use: "recur", 304 + Short: "Manage task recurrence", 305 + Aliases: []string{"repeat"}, 306 + } 307 + 308 + setCmd := &cobra.Command{ 309 + Use: "set [task-id]", 310 + Short: "Set recurrence rule for a task", 311 + Args: cobra.ExactArgs(1), 312 + RunE: func(c *cobra.Command, args []string) error { 313 + rule, _ := c.Flags().GetString("rule") 314 + until, _ := c.Flags().GetString("until") 315 + defer h.Close() 316 + return h.SetRecur(c.Context(), args[0], rule, until) 317 + }, 318 + } 319 + setCmd.Flags().String("rule", "", "Recurrence rule (e.g., FREQ=DAILY)") 320 + setCmd.Flags().String("until", "", "Recurrence end date (YYYY-MM-DD)") 321 + 322 + clearCmd := &cobra.Command{ 323 + Use: "clear [task-id]", 324 + Short: "Clear recurrence rule from a task", 325 + Args: cobra.ExactArgs(1), 326 + RunE: func(c *cobra.Command, args []string) error { 327 + defer h.Close() 328 + return h.ClearRecur(c.Context(), args[0]) 329 + }, 330 + } 331 + 332 + showCmd := &cobra.Command{ 333 + Use: "show [task-id]", 334 + Short: "Show recurrence details for a task", 335 + Args: cobra.ExactArgs(1), 336 + RunE: func(c *cobra.Command, args []string) error { 337 + defer h.Close() 338 + return h.ShowRecur(c.Context(), args[0]) 339 + }, 340 + } 341 + 342 + root.AddCommand(setCmd, clearCmd, showCmd) 343 + return root 344 + } 345 + 346 + func taskDependCmd(h *handlers.TaskHandler) *cobra.Command { 347 + root := &cobra.Command{ 348 + Use: "depend", 349 + Short: "Manage task dependencies", 350 + Aliases: []string{"dep", "deps"}, 351 + } 352 + 353 + addCmd := &cobra.Command{ 354 + Use: "add [task-id] [depends-on-uuid]", 355 + Short: "Add a dependency to a task", 356 + Args: cobra.ExactArgs(2), 357 + RunE: func(c *cobra.Command, args []string) error { 358 + defer h.Close() 359 + return h.AddDep(c.Context(), args[0], args[1]) 360 + }, 361 + } 362 + 363 + removeCmd := &cobra.Command{ 364 + Use: "remove [task-id] [depends-on-uuid]", 365 + Short: "Remove a dependency from a task", 366 + Aliases: []string{"rm"}, 367 + Args: cobra.ExactArgs(2), 368 + RunE: func(c *cobra.Command, args []string) error { 369 + defer h.Close() 370 + return h.RemoveDep(c.Context(), args[0], args[1]) 371 + }, 372 + } 373 + 374 + listCmd := &cobra.Command{ 375 + Use: "list [task-id]", 376 + Short: "List dependencies for a task", 377 + Aliases: []string{"ls"}, 378 + Args: cobra.ExactArgs(1), 379 + RunE: func(c *cobra.Command, args []string) error { 380 + defer h.Close() 381 + return h.ListDeps(c.Context(), args[0]) 382 + }, 383 + } 384 + 385 + blockedByCmd := &cobra.Command{ 386 + Use: "blocked-by [task-id]", 387 + Short: "Show tasks blocked by this task", 388 + Args: cobra.ExactArgs(1), 389 + RunE: func(c *cobra.Command, args []string) error { 390 + defer h.Close() 391 + return h.BlockedByDep(c.Context(), args[0]) 392 + }, 393 + } 394 + 395 + root.AddCommand(addCmd, removeCmd, listCmd, blockedByCmd) 396 + return root 397 + }
+33
cmd/task_flags.go
··· 1 + package main 2 + 3 + import "github.com/spf13/cobra" 4 + 5 + func addCommonTaskFlags(cmd *cobra.Command) { 6 + cmd.Flags().StringP("priority", "p", "", "Set task priority") 7 + cmd.Flags().String("project", "", "Set task project") 8 + cmd.Flags().StringP("context", "c", "", "Set task context") 9 + cmd.Flags().StringSliceP("tags", "t", []string{}, "Add tags to task") 10 + } 11 + 12 + func addRecurrenceFlags(cmd *cobra.Command) { 13 + cmd.Flags().String("recur", "", "Set recurrence rule (e.g., FREQ=DAILY)") 14 + cmd.Flags().String("until", "", "Set recurrence end date (YYYY-MM-DD)") 15 + } 16 + 17 + func addDependencyFlags(cmd *cobra.Command) { 18 + cmd.Flags().String("depends-on", "", "Set task dependencies (comma-separated UUIDs)") 19 + } 20 + 21 + func addParentFlag(cmd *cobra.Command) { 22 + cmd.Flags().String("parent", "", "Set parent task UUID") 23 + } 24 + 25 + func addOutputFlags(cmd *cobra.Command) { 26 + cmd.Flags().String("format", "detailed", "Output format (detailed, brief)") 27 + cmd.Flags().Bool("json", false, "Output as JSON") 28 + cmd.Flags().Bool("no-metadata", false, "Hide creation/modification timestamps") 29 + } 30 + 31 + func addDueDateFlag(cmd *cobra.Command) { 32 + cmd.Flags().StringP("due", "d", "", "Set due date (YYYY-MM-DD)") 33 + }
+100
cmd/task_flags_test.go
··· 1 + package main 2 + 3 + import ( 4 + "testing" 5 + 6 + "github.com/spf13/cobra" 7 + ) 8 + 9 + func TestTaskFlags(t *testing.T) { 10 + t.Run("AddCommonTaskFlags", func(t *testing.T) { 11 + cmd := &cobra.Command{} 12 + addCommonTaskFlags(cmd) 13 + 14 + if cmd.Flags().Lookup("priority") == nil { 15 + t.Error("Expected priority flag to be defined") 16 + } 17 + if cmd.Flags().Lookup("project") == nil { 18 + t.Error("Expected project flag to be defined") 19 + } 20 + if cmd.Flags().Lookup("context") == nil { 21 + t.Error("Expected context flag to be defined") 22 + } 23 + if cmd.Flags().Lookup("tags") == nil { 24 + t.Error("Expected tags flag to be defined") 25 + } 26 + 27 + if cmd.Flags().ShorthandLookup("p") == nil { 28 + t.Error("Expected 'p' shorthand for priority") 29 + } 30 + if cmd.Flags().ShorthandLookup("c") == nil { 31 + t.Error("Expected 'c' shorthand for context") 32 + } 33 + if cmd.Flags().ShorthandLookup("t") == nil { 34 + t.Error("Expected 't' shorthand for tags") 35 + } 36 + }) 37 + 38 + t.Run("AddRecurrenceFlags", func(t *testing.T) { 39 + cmd := &cobra.Command{} 40 + addRecurrenceFlags(cmd) 41 + 42 + if cmd.Flags().Lookup("recur") == nil { 43 + t.Error("Expected recur flag to be defined") 44 + } 45 + if cmd.Flags().Lookup("until") == nil { 46 + t.Error("Expected until flag to be defined") 47 + } 48 + }) 49 + 50 + t.Run("AddDependencyFlags", func(t *testing.T) { 51 + cmd := &cobra.Command{} 52 + addDependencyFlags(cmd) 53 + 54 + if cmd.Flags().Lookup("depends-on") == nil { 55 + t.Error("Expected depends-on flag to be defined") 56 + } 57 + }) 58 + 59 + t.Run("AddParentFlag", func(t *testing.T) { 60 + cmd := &cobra.Command{} 61 + addParentFlag(cmd) 62 + 63 + if cmd.Flags().Lookup("parent") == nil { 64 + t.Error("Expected parent flag to be defined") 65 + } 66 + }) 67 + 68 + t.Run("AddOutputFlags", func(t *testing.T) { 69 + cmd := &cobra.Command{} 70 + addOutputFlags(cmd) 71 + 72 + if cmd.Flags().Lookup("format") == nil { 73 + t.Error("Expected format flag to be defined") 74 + } 75 + if cmd.Flags().Lookup("json") == nil { 76 + t.Error("Expected json flag to be defined") 77 + } 78 + if cmd.Flags().Lookup("no-metadata") == nil { 79 + t.Error("Expected no-metadata flag to be defined") 80 + } 81 + 82 + format, _ := cmd.Flags().GetString("format") 83 + if format != "detailed" { 84 + t.Errorf("Expected format default to be 'detailed', got '%s'", format) 85 + } 86 + }) 87 + 88 + t.Run("AddDueDateFlag", func(t *testing.T) { 89 + cmd := &cobra.Command{} 90 + addDueDateFlag(cmd) 91 + 92 + if cmd.Flags().Lookup("due") == nil { 93 + t.Error("Expected due flag to be defined") 94 + } 95 + 96 + if cmd.Flags().ShorthandLookup("d") == nil { 97 + t.Error("Expected 'd' shorthand for due") 98 + } 99 + }) 100 + }
+37
docs/manual/noteleaf.1.txt
··· 113 113 -d, --days <n> Number of days (default 7) 114 114 -t, --task <id> Timesheet for specific task 115 115 116 + noteleaf task recur set <id> 117 + Set recurrence rule for a task. 118 + Flags: 119 + --rule <value> Recurrence rule (e.g., FREQ=DAILY) 120 + --until YYYY-MM-DD Recurrence end date 121 + 122 + noteleaf task recur clear <id> 123 + Clear recurrence rule from a task. 124 + 125 + noteleaf task recur show <id> 126 + Show recurrence details for a task. 127 + 128 + noteleaf task depend add <id> <depends-on-uuid> 129 + Add a dependency to a task. 130 + 131 + noteleaf task depend remove <id> <depends-on-uuid> 132 + Remove a dependency from a task. 133 + Alias: rm 134 + 135 + noteleaf task depend list <id> 136 + List dependencies for a task. 137 + Alias: ls 138 + 139 + noteleaf task depend blocked-by <id> 140 + Show tasks blocked by this task. 141 + 116 142 MOVIE COMMANDS 117 143 noteleaf movie add [query...] 118 144 Search and add a movie to the watch queue. ··· 247 273 Save an article: 248 274 noteleaf article add https://example.com/post 249 275 noteleaf article list --author "Ada Lovelace" 276 + 277 + Manage task recurrence: 278 + noteleaf task recur set 42 --rule FREQ=DAILY --until 2025-12-31 279 + noteleaf task recur show 42 280 + noteleaf task recur clear 42 281 + 282 + Manage task dependencies: 283 + noteleaf task depend add 42 abc123-uuid 284 + noteleaf task depend list 42 285 + noteleaf task depend blocked-by abc123-uuid 286 + noteleaf task depend remove 42 abc123-uuid 250 287 251 288 FILES 252 289 (TODO: configuration and data file paths once implemented)
+230 -6
internal/handlers/tasks.go
··· 261 261 task.Tags = removeString(task.Tags, tag) 262 262 } 263 263 264 - // Handle dependency additions 265 264 if addDeps != "" { 266 - deps := strings.Split(addDeps, ",") 267 - for _, dep := range deps { 265 + deps := strings.SplitSeq(addDeps, ",") 266 + for dep := range deps { 268 267 dep = strings.TrimSpace(dep) 269 268 if dep != "" && !slices.Contains(task.DependsOn, dep) { 270 269 task.DependsOn = append(task.DependsOn, dep) ··· 272 271 } 273 272 } 274 273 275 - // Handle dependency removals 276 274 if removeDeps != "" { 277 - deps := strings.Split(removeDeps, ",") 278 - for _, dep := range deps { 275 + deps := strings.SplitSeq(removeDeps, ",") 276 + for dep := range deps { 279 277 dep = strings.TrimSpace(dep) 280 278 task.DependsOn = removeString(task.DependsOn, dep) 281 279 } ··· 848 846 return fmt.Errorf("failed to marshal task to JSON: %w", err) 849 847 } 850 848 fmt.Println(string(jsonData)) 849 + return nil 850 + } 851 + 852 + // SetRecur sets the recurrence rule for a task 853 + func (h *TaskHandler) SetRecur(ctx context.Context, taskID, rule, until string) error { 854 + var task *models.Task 855 + var err error 856 + 857 + if id, err_ := strconv.ParseInt(taskID, 10, 64); err_ == nil { 858 + task, err = h.repos.Tasks.Get(ctx, id) 859 + } else { 860 + task, err = h.repos.Tasks.GetByUUID(ctx, taskID) 861 + } 862 + 863 + if err != nil { 864 + return fmt.Errorf("failed to find task: %w", err) 865 + } 866 + 867 + if rule != "" { 868 + task.Recur = models.RRule(rule) 869 + } 870 + 871 + if until != "" { 872 + if untilTime, err := time.Parse("2006-01-02", until); err == nil { 873 + task.Until = &untilTime 874 + } else { 875 + return fmt.Errorf("invalid until date format, use YYYY-MM-DD: %w", err) 876 + } 877 + } 878 + 879 + err = h.repos.Tasks.Update(ctx, task) 880 + if err != nil { 881 + return fmt.Errorf("failed to update task recurrence: %w", err) 882 + } 883 + 884 + fmt.Printf("Recurrence set for task (ID: %d): %s\n", task.ID, task.Description) 885 + if task.Recur != "" { 886 + fmt.Printf("Rule: %s\n", task.Recur) 887 + } 888 + if task.Until != nil { 889 + fmt.Printf("Until: %s\n", task.Until.Format("2006-01-02")) 890 + } 891 + 892 + return nil 893 + } 894 + 895 + // ClearRecur clears the recurrence rule from a task 896 + func (h *TaskHandler) ClearRecur(ctx context.Context, taskID string) error { 897 + var task *models.Task 898 + var err error 899 + 900 + if id, err_ := strconv.ParseInt(taskID, 10, 64); err_ == nil { 901 + task, err = h.repos.Tasks.Get(ctx, id) 902 + } else { 903 + task, err = h.repos.Tasks.GetByUUID(ctx, taskID) 904 + } 905 + 906 + if err != nil { 907 + return fmt.Errorf("failed to find task: %w", err) 908 + } 909 + 910 + task.Recur = "" 911 + task.Until = nil 912 + 913 + err = h.repos.Tasks.Update(ctx, task) 914 + if err != nil { 915 + return fmt.Errorf("failed to clear task recurrence: %w", err) 916 + } 917 + 918 + fmt.Printf("Recurrence cleared for task (ID: %d): %s\n", task.ID, task.Description) 919 + return nil 920 + } 921 + 922 + // ShowRecur displays the recurrence details for a task 923 + func (h *TaskHandler) ShowRecur(ctx context.Context, taskID string) error { 924 + var task *models.Task 925 + var err error 926 + 927 + if id, err_ := strconv.ParseInt(taskID, 10, 64); err_ == nil { 928 + task, err = h.repos.Tasks.Get(ctx, id) 929 + } else { 930 + task, err = h.repos.Tasks.GetByUUID(ctx, taskID) 931 + } 932 + 933 + if err != nil { 934 + return fmt.Errorf("failed to find task: %w", err) 935 + } 936 + 937 + fmt.Printf("Task (ID: %d): %s\n", task.ID, task.Description) 938 + if task.Recur != "" { 939 + fmt.Printf("Recurrence rule: %s\n", task.Recur) 940 + if task.Until != nil { 941 + fmt.Printf("Recurrence until: %s\n", task.Until.Format("2006-01-02")) 942 + } else { 943 + fmt.Printf("Recurrence until: (no end date)\n") 944 + } 945 + } else { 946 + fmt.Printf("No recurrence set\n") 947 + } 948 + 949 + return nil 950 + } 951 + 952 + // AddDep adds a dependency to a task 953 + func (h *TaskHandler) AddDep(ctx context.Context, taskID, dependsOnUUID string) error { 954 + var task *models.Task 955 + var err error 956 + 957 + if id, err_ := strconv.ParseInt(taskID, 10, 64); err_ == nil { 958 + task, err = h.repos.Tasks.Get(ctx, id) 959 + } else { 960 + task, err = h.repos.Tasks.GetByUUID(ctx, taskID) 961 + } 962 + 963 + if err != nil { 964 + return fmt.Errorf("failed to find task: %w", err) 965 + } 966 + 967 + if _, err := h.repos.Tasks.GetByUUID(ctx, dependsOnUUID); err != nil { 968 + return fmt.Errorf("dependency task not found: %w", err) 969 + } 970 + 971 + err = h.repos.Tasks.AddDependency(ctx, task.UUID, dependsOnUUID) 972 + if err != nil { 973 + return fmt.Errorf("failed to add dependency: %w", err) 974 + } 975 + 976 + fmt.Printf("Dependency added to task (ID: %d): %s\n", task.ID, task.Description) 977 + fmt.Printf("Now depends on: %s\n", dependsOnUUID) 978 + 979 + return nil 980 + } 981 + 982 + // RemoveDep removes a dependency from a task 983 + func (h *TaskHandler) RemoveDep(ctx context.Context, taskID, dependsOnUUID string) error { 984 + var task *models.Task 985 + var err error 986 + 987 + if id, err_ := strconv.ParseInt(taskID, 10, 64); err_ == nil { 988 + task, err = h.repos.Tasks.Get(ctx, id) 989 + } else { 990 + task, err = h.repos.Tasks.GetByUUID(ctx, taskID) 991 + } 992 + 993 + if err != nil { 994 + return fmt.Errorf("failed to find task: %w", err) 995 + } 996 + 997 + err = h.repos.Tasks.RemoveDependency(ctx, task.UUID, dependsOnUUID) 998 + if err != nil { 999 + return fmt.Errorf("failed to remove dependency: %w", err) 1000 + } 1001 + 1002 + fmt.Printf("Dependency removed from task (ID: %d): %s\n", task.ID, task.Description) 1003 + fmt.Printf("No longer depends on: %s\n", dependsOnUUID) 1004 + 1005 + return nil 1006 + } 1007 + 1008 + // ListDeps lists all dependencies for a task 1009 + func (h *TaskHandler) ListDeps(ctx context.Context, taskID string) error { 1010 + var task *models.Task 1011 + var err error 1012 + 1013 + if id, err_ := strconv.ParseInt(taskID, 10, 64); err_ == nil { 1014 + task, err = h.repos.Tasks.Get(ctx, id) 1015 + } else { 1016 + task, err = h.repos.Tasks.GetByUUID(ctx, taskID) 1017 + } 1018 + 1019 + if err != nil { 1020 + return fmt.Errorf("failed to find task: %w", err) 1021 + } 1022 + 1023 + fmt.Printf("Task (ID: %d): %s\n", task.ID, task.Description) 1024 + 1025 + if len(task.DependsOn) == 0 { 1026 + fmt.Printf("No dependencies\n") 1027 + return nil 1028 + } 1029 + 1030 + fmt.Printf("Depends on %d task(s):\n", len(task.DependsOn)) 1031 + for _, depUUID := range task.DependsOn { 1032 + depTask, err := h.repos.Tasks.GetByUUID(ctx, depUUID) 1033 + if err != nil { 1034 + fmt.Printf(" - %s (not found)\n", depUUID) 1035 + continue 1036 + } 1037 + fmt.Printf(" - [%d] %s (UUID: %s)\n", depTask.ID, depTask.Description, depTask.UUID) 1038 + } 1039 + 1040 + return nil 1041 + } 1042 + 1043 + // BlockedByDep shows tasks that are blocked by the given task 1044 + func (h *TaskHandler) BlockedByDep(ctx context.Context, taskID string) error { 1045 + var task *models.Task 1046 + var err error 1047 + 1048 + if id, err_ := strconv.ParseInt(taskID, 10, 64); err_ == nil { 1049 + task, err = h.repos.Tasks.Get(ctx, id) 1050 + } else { 1051 + task, err = h.repos.Tasks.GetByUUID(ctx, taskID) 1052 + } 1053 + 1054 + if err != nil { 1055 + return fmt.Errorf("failed to find task: %w", err) 1056 + } 1057 + 1058 + fmt.Printf("Task (ID: %d): %s\n", task.ID, task.Description) 1059 + 1060 + dependents, err := h.repos.Tasks.GetDependents(ctx, task.UUID) 1061 + if err != nil { 1062 + return fmt.Errorf("failed to get dependent tasks: %w", err) 1063 + } 1064 + 1065 + if len(dependents) == 0 { 1066 + fmt.Printf("No tasks are blocked by this task\n") 1067 + return nil 1068 + } 1069 + 1070 + fmt.Printf("Blocks %d task(s):\n", len(dependents)) 1071 + for _, dep := range dependents { 1072 + fmt.Printf(" - [%d] %s\n", dep.ID, dep.Description) 1073 + } 1074 + 851 1075 return nil 852 1076 } 853 1077
+294 -17
internal/handlers/tasks_test.go
··· 1167 1167 defer handler.Close() 1168 1168 1169 1169 tasks := []*models.Task{ 1170 - { 1171 - UUID: uuid.New().String(), 1172 - Description: "Task with context 1", 1173 - Status: "pending", 1174 - Context: "test-context", 1175 - }, 1176 - { 1177 - UUID: uuid.New().String(), 1178 - Description: "Task with context 2", 1179 - Status: "pending", 1180 - Context: "work-context", 1181 - }, 1182 - { 1183 - UUID: uuid.New().String(), 1184 - Description: "Task without context", 1185 - Status: "pending", 1186 - }, 1170 + {UUID: uuid.New().String(), Description: "Task with context 1", Status: "pending", Context: "test-context"}, 1171 + {UUID: uuid.New().String(), Description: "Task with context 2", Status: "pending", Context: "work-context"}, 1172 + {UUID: uuid.New().String(), Description: "Task without context", Status: "pending"}, 1187 1173 } 1188 1174 1189 1175 for _, task := range tasks { ··· 1262 1248 t.Errorf("ListContexts with no contexts failed: %v", err) 1263 1249 } 1264 1250 }) 1251 + }) 1252 + 1253 + t.Run("RecurSet", func(t *testing.T) { 1254 + _, cleanup := setupTaskTest(t) 1255 + defer cleanup() 1256 + 1257 + ctx := context.Background() 1258 + 1259 + handler, err := NewTaskHandler() 1260 + if err != nil { 1261 + t.Fatalf("Failed to create handler: %v", err) 1262 + } 1263 + defer handler.Close() 1264 + 1265 + id, err := handler.repos.Tasks.Create(ctx, &models.Task{ 1266 + UUID: uuid.New().String(), Description: "Test task", Status: "pending", 1267 + }) 1268 + if err != nil { 1269 + t.Fatalf("Failed to create task: %v", err) 1270 + } 1271 + 1272 + t.Run("sets recurrence rule", func(t *testing.T) { 1273 + err := handler.SetRecur(ctx, strconv.FormatInt(id, 10), "FREQ=DAILY", "2025-12-31") 1274 + if err != nil { 1275 + t.Errorf("RecurSet failed: %v", err) 1276 + } 1277 + 1278 + task, err := handler.repos.Tasks.Get(ctx, id) 1279 + if err != nil { 1280 + t.Fatalf("Failed to get task: %v", err) 1281 + } 1282 + 1283 + if task.Recur != "FREQ=DAILY" { 1284 + t.Errorf("Expected recur to be 'FREQ=DAILY', got '%s'", task.Recur) 1285 + } 1286 + 1287 + if task.Until == nil { 1288 + t.Error("Expected until to be set") 1289 + } 1290 + }) 1291 + 1292 + t.Run("handles invalid until date", func(t *testing.T) { 1293 + err := handler.SetRecur(ctx, strconv.FormatInt(id, 10), "FREQ=WEEKLY", "invalid-date") 1294 + if err == nil { 1295 + t.Error("Expected error for invalid until date") 1296 + } 1297 + }) 1298 + }) 1299 + 1300 + t.Run("RecurClear", func(t *testing.T) { 1301 + _, cleanup := setupTaskTest(t) 1302 + defer cleanup() 1303 + 1304 + ctx := context.Background() 1305 + 1306 + handler, err := NewTaskHandler() 1307 + if err != nil { 1308 + t.Fatalf("Failed to create handler: %v", err) 1309 + } 1310 + defer handler.Close() 1311 + 1312 + until := time.Now() 1313 + id, err := handler.repos.Tasks.Create(ctx, &models.Task{ 1314 + UUID: uuid.New().String(), 1315 + Description: "Test task", 1316 + Status: "pending", 1317 + Recur: "FREQ=DAILY", 1318 + Until: &until, 1319 + }) 1320 + if err != nil { 1321 + t.Fatalf("Failed to create task: %v", err) 1322 + } 1323 + 1324 + err = handler.ClearRecur(ctx, strconv.FormatInt(id, 10)) 1325 + if err != nil { 1326 + t.Errorf("RecurClear failed: %v", err) 1327 + } 1328 + 1329 + task, err := handler.repos.Tasks.Get(ctx, id) 1330 + if err != nil { 1331 + t.Fatalf("Failed to get task: %v", err) 1332 + } 1333 + 1334 + if task.Recur != "" { 1335 + t.Errorf("Expected recur to be cleared, got '%s'", task.Recur) 1336 + } 1337 + 1338 + if task.Until != nil { 1339 + t.Error("Expected until to be cleared") 1340 + } 1341 + }) 1342 + 1343 + t.Run("RecurShow", func(t *testing.T) { 1344 + _, cleanup := setupTaskTest(t) 1345 + defer cleanup() 1346 + 1347 + ctx := context.Background() 1348 + 1349 + handler, err := NewTaskHandler() 1350 + if err != nil { 1351 + t.Fatalf("Failed to create handler: %v", err) 1352 + } 1353 + defer handler.Close() 1354 + 1355 + until := time.Now() 1356 + id, err := handler.repos.Tasks.Create(ctx, &models.Task{ 1357 + UUID: uuid.New().String(), 1358 + Description: "Test task", 1359 + Status: "pending", 1360 + Recur: "FREQ=DAILY", 1361 + Until: &until, 1362 + }) 1363 + if err != nil { 1364 + t.Fatalf("Failed to create task: %v", err) 1365 + } 1366 + 1367 + err = handler.ShowRecur(ctx, strconv.FormatInt(id, 10)) 1368 + if err != nil { 1369 + t.Errorf("RecurShow failed: %v", err) 1370 + } 1371 + }) 1372 + 1373 + t.Run("DependAdd", func(t *testing.T) { 1374 + _, cleanup := setupTaskTest(t) 1375 + defer cleanup() 1376 + 1377 + ctx := context.Background() 1378 + 1379 + handler, err := NewTaskHandler() 1380 + if err != nil { 1381 + t.Fatalf("Failed to create handler: %v", err) 1382 + } 1383 + defer handler.Close() 1384 + 1385 + task1UUID := uuid.New().String() 1386 + task2UUID := uuid.New().String() 1387 + 1388 + id1, err := handler.repos.Tasks.Create(ctx, &models.Task{ 1389 + UUID: task1UUID, Description: "Task 1", Status: "pending", 1390 + }) 1391 + if err != nil { 1392 + t.Fatalf("Failed to create task 1: %v", err) 1393 + } 1394 + 1395 + _, err = handler.repos.Tasks.Create(ctx, &models.Task{ 1396 + UUID: task2UUID, Description: "Task 2", Status: "pending", 1397 + }) 1398 + if err != nil { 1399 + t.Fatalf("Failed to create task 2: %v", err) 1400 + } 1401 + 1402 + err = handler.AddDep(ctx, strconv.FormatInt(id1, 10), task2UUID) 1403 + if err != nil { 1404 + t.Errorf("DependAdd failed: %v", err) 1405 + } 1406 + 1407 + task, err := handler.repos.Tasks.Get(ctx, id1) 1408 + if err != nil { 1409 + t.Fatalf("Failed to get task: %v", err) 1410 + } 1411 + 1412 + if len(task.DependsOn) != 1 { 1413 + t.Errorf("Expected 1 dependency, got %d", len(task.DependsOn)) 1414 + } 1415 + 1416 + if task.DependsOn[0] != task2UUID { 1417 + t.Errorf("Expected dependency to be '%s', got '%s'", task2UUID, task.DependsOn[0]) 1418 + } 1419 + }) 1420 + 1421 + t.Run("DependRemove", func(t *testing.T) { 1422 + _, cleanup := setupTaskTest(t) 1423 + defer cleanup() 1424 + 1425 + ctx := context.Background() 1426 + 1427 + handler, err := NewTaskHandler() 1428 + if err != nil { 1429 + t.Fatalf("Failed to create handler: %v", err) 1430 + } 1431 + defer handler.Close() 1432 + 1433 + task1UUID := uuid.New().String() 1434 + task2UUID := uuid.New().String() 1435 + 1436 + _, err = handler.repos.Tasks.Create(ctx, &models.Task{ 1437 + UUID: task2UUID, Description: "Task 2", Status: "pending", 1438 + }) 1439 + if err != nil { 1440 + t.Fatalf("Failed to create task 2: %v", err) 1441 + } 1442 + 1443 + id1, err := handler.repos.Tasks.Create(ctx, &models.Task{ 1444 + UUID: task1UUID, 1445 + Description: "Task 1", 1446 + Status: "pending", 1447 + DependsOn: []string{task2UUID}, 1448 + }) 1449 + if err != nil { 1450 + t.Fatalf("Failed to create task 1: %v", err) 1451 + } 1452 + 1453 + err = handler.RemoveDep(ctx, strconv.FormatInt(id1, 10), task2UUID) 1454 + if err != nil { 1455 + t.Errorf("DependRemove failed: %v", err) 1456 + } 1457 + 1458 + task, err := handler.repos.Tasks.Get(ctx, id1) 1459 + if err != nil { 1460 + t.Fatalf("Failed to get task: %v", err) 1461 + } 1462 + 1463 + if len(task.DependsOn) != 0 { 1464 + t.Errorf("Expected 0 dependencies, got %d", len(task.DependsOn)) 1465 + } 1466 + }) 1467 + 1468 + t.Run("DependList", func(t *testing.T) { 1469 + _, cleanup := setupTaskTest(t) 1470 + defer cleanup() 1471 + 1472 + ctx := context.Background() 1473 + 1474 + handler, err := NewTaskHandler() 1475 + if err != nil { 1476 + t.Fatalf("Failed to create handler: %v", err) 1477 + } 1478 + defer handler.Close() 1479 + 1480 + task1UUID := uuid.New().String() 1481 + task2UUID := uuid.New().String() 1482 + 1483 + _, err = handler.repos.Tasks.Create(ctx, &models.Task{ 1484 + UUID: task2UUID, Description: "Task 2", Status: "pending", 1485 + }) 1486 + if err != nil { 1487 + t.Fatalf("Failed to create task 2: %v", err) 1488 + } 1489 + 1490 + id1, err := handler.repos.Tasks.Create(ctx, &models.Task{ 1491 + UUID: task1UUID, 1492 + Description: "Task 1", 1493 + Status: "pending", 1494 + DependsOn: []string{task2UUID}, 1495 + }) 1496 + if err != nil { 1497 + t.Fatalf("Failed to create task 1: %v", err) 1498 + } 1499 + 1500 + err = handler.ListDeps(ctx, strconv.FormatInt(id1, 10)) 1501 + if err != nil { 1502 + t.Errorf("DependList failed: %v", err) 1503 + } 1504 + }) 1505 + 1506 + t.Run("DependBlockedBy", func(t *testing.T) { 1507 + _, cleanup := setupTaskTest(t) 1508 + defer cleanup() 1509 + 1510 + ctx := context.Background() 1511 + 1512 + handler, err := NewTaskHandler() 1513 + if err != nil { 1514 + t.Fatalf("Failed to create handler: %v", err) 1515 + } 1516 + defer handler.Close() 1517 + 1518 + task1UUID := uuid.New().String() 1519 + task2UUID := uuid.New().String() 1520 + 1521 + id2, err := handler.repos.Tasks.Create(ctx, &models.Task{ 1522 + UUID: task2UUID, Description: "Task 2", Status: "pending", 1523 + }) 1524 + if err != nil { 1525 + t.Fatalf("Failed to create task 2: %v", err) 1526 + } 1527 + 1528 + _, err = handler.repos.Tasks.Create(ctx, &models.Task{ 1529 + UUID: task1UUID, 1530 + Description: "Task 1", 1531 + Status: "pending", 1532 + DependsOn: []string{task2UUID}, 1533 + }) 1534 + if err != nil { 1535 + t.Fatalf("Failed to create task 1: %v", err) 1536 + } 1537 + 1538 + err = handler.BlockedByDep(ctx, strconv.FormatInt(id2, 10)) 1539 + if err != nil { 1540 + t.Errorf("DependBlockedBy failed: %v", err) 1541 + } 1265 1542 }) 1266 1543 }