diff options
| author | Peter Stone <thepeterstone@gmail.com> | 2026-09-03 00:40:47 +0000 |
|---|---|---|
| committer | Peter Stone <thepeterstone@gmail.com> | 2026-09-03 00:40:47 +0000 |
| commit | 10f9fcd271bbac4ff26f32880db212b5bd3b7f00 (patch) | |
| tree | 83b7e8320255dec7c2304988a0660b9420bf7284 /internal/models/recurrence_test.go | |
| parent | 46dfe32326a7e15d1591939d30ffb070eeec406c (diff) | |
Show one row per recurring chore in the flat task list
The recurrence scheduler materializes a series' next instance once the
current one comes due, regardless of whether it was completed -- that's
deliberate, so ignoring an occurrence doesn't stall the series. The side
effect is that the flat task list shows an unfinished chore and its
already-generated successor at the same time: "Bills" appears twice right
now (Sep 1 overdue + Oct 1), as does "Bones's meds".
CollapseRecurringSeries keeps only the earliest-due open instance per
series, which is the one actually owed. Applied to the atoms list only --
the timeline and widget are date-anchored, where each instance correctly
belongs to its own day, so they keep all of them.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018AwnALZL6bD4VBtATDVv3B
Diffstat (limited to 'internal/models/recurrence_test.go')
| -rw-r--r-- | internal/models/recurrence_test.go | 40 |
1 files changed, 40 insertions, 0 deletions
diff --git a/internal/models/recurrence_test.go b/internal/models/recurrence_test.go index 8d18996..affba56 100644 --- a/internal/models/recurrence_test.go +++ b/internal/models/recurrence_test.go @@ -49,3 +49,43 @@ func TestComputeNextOccurrence(t *testing.T) { }) } } + +func TestCollapseRecurringSeries_KeepsOnlyEarliestOpenInstance(t *testing.T) { + sep1 := time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC) + oct1 := time.Date(2026, 10, 1, 0, 0, 0, 0, time.UTC) + + tasks := []Task{ + {ID: "bills-sep", Content: "Bills", DueDate: &sep1, RecurrenceSeriesID: "bills"}, + {ID: "bills-oct", Content: "Bills", DueDate: &oct1, RecurrenceSeriesID: "bills"}, + {ID: "one-off", Content: "Fix curtain holder"}, + } + + got := CollapseRecurringSeries(tasks) + + var ids []string + for _, task := range got { + ids = append(ids, task.ID) + } + if len(got) != 2 { + t.Fatalf("got %d tasks (%v), want 2: one row per recurring series plus the one-off", len(got), ids) + } + if got[0].ID != "bills-sep" { + t.Errorf("kept %q for the Bills series, want bills-sep (the earliest open instance is the one you actually owe)", got[0].ID) + } + if got[1].ID != "one-off" { + t.Errorf("non-recurring task was dropped or reordered: got %q", got[1].ID) + } +} + +func TestCollapseRecurringSeries_UndatedAndDistinctSeriesUntouched(t *testing.T) { + due := time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC) + tasks := []Task{ + {ID: "a", RecurrenceSeriesID: "series-a", DueDate: &due}, + {ID: "b", RecurrenceSeriesID: "series-b", DueDate: &due}, + {ID: "c", RecurrenceSeriesID: "series-c"}, // recurring but undated + } + + if got := CollapseRecurringSeries(tasks); len(got) != 3 { + t.Errorf("len = %d, want 3 -- distinct series must not collapse into each other", len(got)) + } +} |
