diff options
Diffstat (limited to 'internal/store/native_tasks_test.go')
| -rw-r--r-- | internal/store/native_tasks_test.go | 82 |
1 files changed, 81 insertions, 1 deletions
diff --git a/internal/store/native_tasks_test.go b/internal/store/native_tasks_test.go index 9a7e55f..9453c86 100644 --- a/internal/store/native_tasks_test.go +++ b/internal/store/native_tasks_test.go @@ -7,6 +7,7 @@ import ( "testing" "time" + "task-dashboard/internal/config" "task-dashboard/internal/models" _ "github.com/mattn/go-sqlite3" @@ -48,7 +49,8 @@ func newNativeTasksTestStore(t *testing.T) *Store { chain_unlocked BOOLEAN DEFAULT 0, bucket_id TEXT DEFAULT '', bucket_state TEXT DEFAULT '', - bucket_last_active_at DATETIME + bucket_last_active_at DATETIME, + abandoned BOOLEAN DEFAULT 0 ) `); err != nil { t.Fatal(err) @@ -718,3 +720,81 @@ func TestDeleteNativeTask_UnknownID_ReturnsErrNotFound(t *testing.T) { t.Errorf("err = %v, want ErrNativeTaskNotFound", err) } } + +// A weekly task not done before its next iteration lands is stale, not still +// owed -- the successor supersedes it. Before this, the skipped instance stayed +// open forever and showed up as a permanent overdue item, so ignoring one +// occurrence cost a list slot indefinitely. +func TestAdvanceDueRecurringTasks_AbandonsSupersededInstance(t *testing.T) { + s := newNativeTasksTestStore(t) + + due := config.Now().Add(-24 * time.Hour) + task := models.Task{ + ID: "weekly-1", Content: "Take out trash", DueDate: &due, + RecurrenceFreq: "weekly", RecurrenceInterval: 1, + } + if err := s.CreateNativeTask(task); err != nil { + t.Fatal(err) + } + if err := s.SetTaskRecurrence("weekly-1", "weekly", 1, nil); err != nil { + t.Fatal(err) + } + + if _, err := s.AdvanceDueRecurringTasks(config.Now()); err != nil { + t.Fatalf("AdvanceDueRecurringTasks: %v", err) + } + + // The superseded original must no longer be an open, actionable task. + overdue, err := s.GetOverdueNativeTasks(config.Now()) + if err != nil { + t.Fatal(err) + } + for _, o := range overdue { + if o.ID == "weekly-1" { + t.Error("superseded instance weekly-1 is still showing as overdue; it should have been abandoned when its successor was created") + } + } + + // ...and the series should be left with exactly one live occurrence: the + // successor. Scoped to this series because the test store seeds an + // unrelated task of its own. + var seriesID string + if err := s.db.QueryRow(`SELECT recurrence_series_id FROM native_tasks WHERE id = 'weekly-1'`).Scan(&seriesID); err != nil { + t.Fatal(err) + } + var open int + if err := s.db.QueryRow(` + SELECT COUNT(*) FROM native_tasks + WHERE recurrence_series_id = ? AND completed = 0 AND abandoned = 0 + `, seriesID).Scan(&open); err != nil { + t.Fatal(err) + } + if open != 1 { + t.Errorf("live occurrences in series = %d, want exactly 1 (the successor); a skipped occurrence must not keep its slot", open) + } +} + +// Completing normally must not mark anything abandoned -- only the scheduler's +// supersede path does that. +func TestCompleteNativeTask_DoesNotAbandonAnything(t *testing.T) { + s := newNativeTasksTestStore(t) + + due := config.Now() + if err := s.CreateNativeTask(models.Task{ID: "w1", Content: "Chore", DueDate: &due}); err != nil { + t.Fatal(err) + } + if err := s.SetTaskRecurrence("w1", "weekly", 1, nil); err != nil { + t.Fatal(err) + } + if err := s.CompleteNativeTask("w1"); err != nil { + t.Fatal(err) + } + + var abandoned int + if err := s.db.QueryRow(`SELECT COUNT(*) FROM native_tasks WHERE abandoned = 1`).Scan(&abandoned); err != nil { + t.Fatal(err) + } + if abandoned != 0 { + t.Errorf("abandoned = %d, want 0 -- a completed occurrence was done, not skipped", abandoned) + } +} |
