summaryrefslogtreecommitdiff
path: root/internal/store/native_tasks_test.go
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-07-15 01:22:08 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-07-16 02:48:28 +0000
commit16649b7b00ed0b3be03aa039922536a853993597 (patch)
tree3f73643be87c58147f9a8b726a3e3d904b99e381 /internal/store/native_tasks_test.go
parent159e6710649c879d0df77eefc6c09f7d088d7229 (diff)
fix(tasks): make CreateNextIteration atomically self-guarding
Folds the "is this still the latest row in its series?" check directly into the INSERT as a single atomic INSERT...SELECT...WHERE NOT EXISTS statement, instead of a separate SELECT-then-INSERT sequence. Closes a narrow race where the completion trigger and the periodic due-date check could both pass their "still latest" check before either had inserted, producing two duplicate successor rows for the same series. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VTUSAEKfsPc6WGDq45yPHD
Diffstat (limited to 'internal/store/native_tasks_test.go')
-rw-r--r--internal/store/native_tasks_test.go31
1 files changed, 31 insertions, 0 deletions
diff --git a/internal/store/native_tasks_test.go b/internal/store/native_tasks_test.go
index a299ac6..c362eb7 100644
--- a/internal/store/native_tasks_test.go
+++ b/internal/store/native_tasks_test.go
@@ -302,6 +302,37 @@ func TestCompleteNativeTask_AlreadySuperseded_DoesNotDoubleCreate(t *testing.T)
}
}
+func TestCreateNextIteration_ConcurrentGuard_SecondCallIsNoOp(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)
+ }
+ old, err := s.GetNativeTaskByID("rec-1")
+ if err != nil {
+ t.Fatal(err)
+ }
+
+ // Simulate two concurrent triggers both computing from the same
+ // snapshot of rec-1 and both attempting to create its successor.
+ if err := s.CreateNextIteration(*old); err != nil {
+ t.Fatalf("first CreateNextIteration: %v", err)
+ }
+ if err := s.CreateNextIteration(*old); err != nil {
+ t.Fatalf("second CreateNextIteration (should be a safe no-op, not an error): %v", err)
+ }
+
+ var count int
+ if err := s.db.QueryRow(`SELECT COUNT(*) FROM native_tasks WHERE recurrence_series_id = 'series-1' AND id != 'rec-1'`).Scan(&count); err != nil {
+ t.Fatal(err)
+ }
+ if count != 1 {
+ t.Errorf("expected exactly 1 successor row after two CreateNextIteration calls from the same snapshot, got %d", count)
+ }
+}
+
func TestSetTaskRecurrence_FirstTimeGeneratesSeriesID(t *testing.T) {
s := newNativeTasksTestStore(t)