summaryrefslogtreecommitdiff
path: root/internal/store/native_tasks_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/store/native_tasks_test.go')
-rw-r--r--internal/store/native_tasks_test.go82
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)
+ }
+}