summaryrefslogtreecommitdiff
path: root/internal/store/native_tasks_test.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-07-14 20:23:18 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-16 02:44:32 +0000
commit550ffde0d8b71be96a4d21d7c07b718f367c2c48 (patch)
treeef21db975b1504ffe712f992181d616a511be1c5 /internal/store/native_tasks_test.go
parentc99d52ccf5c52e9c75b6fcb661c8e74816b34924 (diff)
feat(tasks): create the next recurring iteration on completion
CompleteNativeTask now creates a new row for the next occurrence when completing the latest iteration of a recurring series (using the one-shot next_occurrence_override if set, else ComputeNextOccurrence). Completing an already-superseded row (the periodic due-check beat it to creating the successor) just marks it completed, no double-create.
Diffstat (limited to 'internal/store/native_tasks_test.go')
-rw-r--r--internal/store/native_tasks_test.go190
1 files changed, 190 insertions, 0 deletions
diff --git a/internal/store/native_tasks_test.go b/internal/store/native_tasks_test.go
index 0a87bfa..fae7e90 100644
--- a/internal/store/native_tasks_test.go
+++ b/internal/store/native_tasks_test.go
@@ -168,3 +168,193 @@ func TestGetNativeTasks_NonRecurringTask_HasEmptyRecurrenceFields(t *testing.T)
t.Errorf("expected nil NextOccurrenceOverride for non-recurring task, got %v", found.NextOccurrenceOverride)
}
}
+
+func TestGetNativeTaskByID_UnknownID_ReturnsErrNotFound(t *testing.T) {
+ s := newNativeTasksTestStore(t)
+
+ _, err := s.GetNativeTaskByID("does-not-exist")
+ if !errors.Is(err, ErrNativeTaskNotFound) {
+ t.Fatalf("expected ErrNativeTaskNotFound, got %v", err)
+ }
+}
+
+func TestGetNativeTaskByID_RealID_ReturnsTask(t *testing.T) {
+ s := newNativeTasksTestStore(t)
+
+ task, err := s.GetNativeTaskByID("real-1")
+ if err != nil {
+ t.Fatalf("GetNativeTaskByID: %v", err)
+ }
+ if task.Content != "Real task" {
+ t.Errorf("Content = %q, want %q", task.Content, "Real task")
+ }
+}
+
+func TestCompleteNativeTask_NonRecurring_JustCompletes(t *testing.T) {
+ s := newNativeTasksTestStore(t)
+
+ if err := s.CompleteNativeTask("real-1"); err != nil {
+ t.Fatalf("CompleteNativeTask: %v", err)
+ }
+
+ tasks, err := s.db.Query(`SELECT id FROM native_tasks`)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer tasks.Close()
+ count := 0
+ for tasks.Next() {
+ count++
+ }
+ if count != 1 {
+ t.Errorf("expected exactly 1 row (no iteration created for a non-recurring task), got %d", count)
+ }
+}
+
+func TestCompleteNativeTask_LatestInSeries_CreatesNextIteration(t *testing.T) {
+ s := newNativeTasksTestStore(t)
+ if _, err := s.db.Exec(`
+ INSERT INTO native_tasks (id, content, due_date, recurrence_freq, recurrence_interval, recurrence_series_id)
+ VALUES ('rec-1', 'Water plants', '2026-07-13', 'weekly', 1, 'series-1')
+ `); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := s.CompleteNativeTask("rec-1"); err != nil {
+ t.Fatalf("CompleteNativeTask: %v", err)
+ }
+
+ var completed bool
+ if err := s.db.QueryRow(`SELECT completed FROM native_tasks WHERE id = 'rec-1'`).Scan(&completed); err != nil {
+ t.Fatal(err)
+ }
+ if !completed {
+ t.Error("expected rec-1 to be marked completed")
+ }
+
+ var nextCount int
+ var nextDue string
+ if err := s.db.QueryRow(`
+ SELECT COUNT(*), COALESCE(MAX(due_date), '') FROM native_tasks
+ WHERE recurrence_series_id = 'series-1' AND id != 'rec-1'
+ `).Scan(&nextCount, &nextDue); err != nil {
+ t.Fatal(err)
+ }
+ if nextCount != 1 {
+ t.Fatalf("expected exactly 1 new iteration, got %d", nextCount)
+ }
+ if nextDue[:10] != "2026-07-20" {
+ t.Errorf("next iteration due_date = %q, want 2026-07-20", nextDue)
+ }
+}
+
+func TestCompleteNativeTask_UsesNextOccurrenceOverride(t *testing.T) {
+ s := newNativeTasksTestStore(t)
+ if _, err := s.db.Exec(`
+ INSERT INTO native_tasks (id, content, due_date, recurrence_freq, recurrence_interval, recurrence_series_id, next_occurrence_override)
+ VALUES ('rec-1', 'Water plants', '2026-07-13', 'weekly', 1, 'series-1', '2026-08-01')
+ `); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := s.CompleteNativeTask("rec-1"); err != nil {
+ t.Fatalf("CompleteNativeTask: %v", err)
+ }
+
+ var nextDue string
+ if err := s.db.QueryRow(`
+ SELECT due_date FROM native_tasks WHERE recurrence_series_id = 'series-1' AND id != 'rec-1'
+ `).Scan(&nextDue); err != nil {
+ t.Fatal(err)
+ }
+ if nextDue[:10] != "2026-08-01" {
+ t.Errorf("next iteration due_date = %q, want 2026-08-01 (the override)", nextDue)
+ }
+}
+
+func TestCompleteNativeTask_AlreadySuperseded_DoesNotDoubleCreate(t *testing.T) {
+ s := newNativeTasksTestStore(t)
+ if _, err := s.db.Exec(`
+ INSERT INTO native_tasks (id, content, due_date, recurrence_freq, recurrence_interval, recurrence_series_id)
+ VALUES ('rec-1', 'Water plants', '2026-07-13', 'weekly', 1, 'series-1')
+ `); err != nil {
+ t.Fatal(err)
+ }
+ // Simulate the periodic due-check having already created the successor
+ // before the user got around to completing rec-1.
+ if _, err := s.db.Exec(`
+ INSERT INTO native_tasks (id, content, due_date, recurrence_freq, recurrence_interval, recurrence_series_id)
+ VALUES ('rec-2', 'Water plants', '2026-07-20', 'weekly', 1, 'series-1')
+ `); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := s.CompleteNativeTask("rec-1"); err != nil {
+ t.Fatalf("CompleteNativeTask: %v", err)
+ }
+
+ var count int
+ if err := s.db.QueryRow(`SELECT COUNT(*) FROM native_tasks WHERE recurrence_series_id = 'series-1'`).Scan(&count); err != nil {
+ t.Fatal(err)
+ }
+ if count != 2 {
+ t.Errorf("expected still exactly 2 rows in the series (no double-create), got %d", count)
+ }
+}
+
+func TestSetTaskRecurrence_FirstTimeGeneratesSeriesID(t *testing.T) {
+ s := newNativeTasksTestStore(t)
+
+ if err := s.SetTaskRecurrence("real-1", "weekly", 1, []int{1, 3}); err != nil {
+ t.Fatalf("SetTaskRecurrence: %v", err)
+ }
+
+ task, err := s.GetNativeTaskByID("real-1")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if task.RecurrenceFreq != "weekly" {
+ t.Errorf("RecurrenceFreq = %q, want weekly", task.RecurrenceFreq)
+ }
+ if task.RecurrenceSeriesID == "" {
+ t.Error("expected a generated RecurrenceSeriesID, got empty string")
+ }
+ if len(task.RecurrenceWeekdays) != 2 || task.RecurrenceWeekdays[0] != 1 || task.RecurrenceWeekdays[1] != 3 {
+ t.Errorf("RecurrenceWeekdays = %v, want [1 3]", task.RecurrenceWeekdays)
+ }
+}
+
+func TestSetTaskRecurrence_ClearingKeepsSeriesID(t *testing.T) {
+ s := newNativeTasksTestStore(t)
+ if err := s.SetTaskRecurrence("real-1", "weekly", 1, nil); err != nil {
+ t.Fatal(err)
+ }
+ task, err := s.GetNativeTaskByID("real-1")
+ if err != nil {
+ t.Fatal(err)
+ }
+ seriesID := task.RecurrenceSeriesID
+
+ if err := s.SetTaskRecurrence("real-1", "", 1, nil); err != nil {
+ t.Fatalf("SetTaskRecurrence (clear): %v", err)
+ }
+
+ task, err = s.GetNativeTaskByID("real-1")
+ if err != nil {
+ t.Fatal(err)
+ }
+ if task.RecurrenceFreq != "" {
+ t.Errorf("RecurrenceFreq = %q, want empty after clearing", task.RecurrenceFreq)
+ }
+ if task.RecurrenceSeriesID != seriesID {
+ t.Errorf("RecurrenceSeriesID = %q, want unchanged %q after clearing", task.RecurrenceSeriesID, seriesID)
+ }
+}
+
+func TestSetNextOccurrenceOverride_UnknownID_ReturnsErrNotFound(t *testing.T) {
+ s := newNativeTasksTestStore(t)
+ err := s.SetNextOccurrenceOverride("does-not-exist", time.Date(2026, 8, 1, 0, 0, 0, 0, time.UTC))
+ if !errors.Is(err, ErrNativeTaskNotFound) {
+ t.Fatalf("expected ErrNativeTaskNotFound, got %v", err)
+ }
+}