package engine import ( "testing" "time" "github.com/google/uuid" ) func TestParseRecurrencePattern(t *testing.T) { tests := []struct { pattern string expected time.Duration }{ {"1d", 24 * time.Hour}, {"7d", 7 * 24 * time.Hour}, {"1w", 7 * 24 * time.Hour}, {"2w", 14 * 24 * time.Hour}, {"1m", 30 * 24 * time.Hour}, {"1y", 365 * 24 * time.Hour}, // Test word forms {"daily", 24 * time.Hour}, {"weekly", 7 * 24 * time.Hour}, {"monthly", 30 * 24 * time.Hour}, {"yearly", 365 * 24 * time.Hour}, } for _, tt := range tests { t.Run(tt.pattern, func(t *testing.T) { result, err := ParseRecurrencePattern(tt.pattern) if err != nil { t.Fatalf("Failed to parse pattern %s: %v", tt.pattern, err) } if result != tt.expected { t.Errorf("Pattern %s: expected %v, got %v", tt.pattern, tt.expected, result) } }) } } func TestFormatRecurrenceDuration(t *testing.T) { tests := []struct { duration time.Duration expected string }{ {24 * time.Hour, "1d"}, {7 * 24 * time.Hour, "1w"}, {14 * 24 * time.Hour, "2w"}, {30 * 24 * time.Hour, "1m"}, {365 * 24 * time.Hour, "1y"}, } for _, tt := range tests { t.Run(tt.expected, func(t *testing.T) { result := FormatRecurrenceDuration(tt.duration) if result != tt.expected { t.Errorf("Duration %v: expected %s, got %s", tt.duration, tt.expected, result) } }) } } func TestCalculateNextDue(t *testing.T) { base := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) // Test weekly recurrence next := CalculateNextDue(base, 7*24*time.Hour) expected := time.Date(2026, 1, 8, 0, 0, 0, 0, time.UTC) if !next.Equal(expected) { t.Errorf("Expected %v, got %v", expected, next) } // Test daily recurrence next2 := CalculateNextDue(base, 24*time.Hour) expected2 := time.Date(2026, 1, 2, 0, 0, 0, 0, time.UTC) if !next2.Equal(expected2) { t.Errorf("Expected %v, got %v", expected2, next2) } } func TestRecurringTaskCreation(t *testing.T) { // Create a recurring task (template + first instance) dueDate := time.Now().Add(24 * time.Hour) duration := 7 * 24 * time.Hour // Create template template := &Task{ UUID: uuid.New(), Status: StatusRecurring, Description: "Weekly review", Priority: PriorityMedium, Created: time.Now(), Modified: time.Now(), Due: &dueDate, RecurrenceDuration: &duration, ParentUUID: nil, // This is the template Tags: []string{}, } if err := template.Save(); err != nil { t.Fatalf("Failed to save template: %v", err) } if err := template.AddTag("recurring"); err != nil { t.Fatalf("Failed to add tag to template: %v", err) } // Create first instance instance := &Task{ UUID: uuid.New(), Status: StatusPending, Description: template.Description, Priority: template.Priority, Created: time.Now(), Modified: time.Now(), Due: template.Due, ParentUUID: &template.UUID, Tags: []string{}, } if err := instance.Save(); err != nil { t.Fatalf("Failed to save instance: %v", err) } if err := instance.AddTag("recurring"); err != nil { t.Fatalf("Failed to add tag to instance: %v", err) } // Verify instance is linked to template if instance.ParentUUID == nil { t.Error("Instance should have parent UUID") } if *instance.ParentUUID != template.UUID { t.Error("Instance parent UUID should match template UUID") } // Verify template is recurring if !template.IsRecurringTemplate() { t.Error("Template should be identified as recurring template") } // Verify instance is a recurring instance if !instance.IsRecurringInstance() { t.Error("Instance should be identified as recurring instance") } } func TestSpawnNextInstance(t *testing.T) { // Create template dueDate := time.Now().Add(24 * time.Hour) duration := 7 * 24 * time.Hour template := &Task{ UUID: uuid.New(), Status: StatusRecurring, Description: "Weekly task", Priority: PriorityHigh, Created: time.Now(), Modified: time.Now(), Due: &dueDate, RecurrenceDuration: &duration, ParentUUID: nil, Tags: []string{}, } if err := template.Save(); err != nil { t.Fatalf("Failed to save template: %v", err) } if err := template.AddTag("work"); err != nil { t.Fatalf("Failed to add tag: %v", err) } // Create and complete first instance instance1 := &Task{ UUID: uuid.New(), Status: StatusPending, Description: template.Description, Priority: template.Priority, Created: time.Now(), Modified: time.Now(), Due: template.Due, ParentUUID: &template.UUID, Tags: []string{}, } if err := instance1.Save(); err != nil { t.Fatalf("Failed to save instance: %v", err) } if err := instance1.AddTag("work"); err != nil { t.Fatalf("Failed to add tag to instance: %v", err) } // Complete the instance (should spawn next) if err := instance1.Complete(); err != nil { t.Fatalf("Failed to complete instance: %v", err) } // Find the newly spawned instance filter := DefaultFilter() tasks, err := GetTasks(filter) if err != nil { t.Fatalf("Failed to get tasks: %v", err) } // Should have at least one new pending instance found := false for _, task := range tasks { if task.ParentUUID != nil && *task.ParentUUID == template.UUID && task.Status == StatusPending { found = true // Verify due date was advanced // New behavior: calculates from End date (completion time), not original due date if task.Due == nil { t.Error("New instance should have due date") } else { // Should be 7 days from when we completed instance1 // Allow reasonable tolerance for test timing diff := task.Due.Sub(time.Now()) expectedDiff := duration tolerance := 5 * time.Second if diff < expectedDiff-tolerance || diff > expectedDiff+tolerance { t.Errorf("Expected due date ~%v from now, got %v from now (diff: %v)", duration, diff, diff-expectedDiff) } } // Verify tags were copied if len(task.Tags) == 0 { t.Error("Tags should be copied to new instance") } foundTag := false for _, tag := range task.Tags { if tag == "work" { foundTag = true } } if !foundTag { t.Error("'work' tag should be copied to new instance") } break } } if !found { t.Error("Should have spawned a new instance after completing recurring task") } } func TestRecurrenceWithUntilDate(t *testing.T) { // Create template with until date very soon dueDate := time.Now().Add(-24 * time.Hour) // Already past until := time.Now().Add(-12 * time.Hour) // Until date also past duration := 7 * 24 * time.Hour template := &Task{ UUID: uuid.New(), Status: StatusRecurring, Description: "Expired recurring task", Priority: PriorityMedium, Created: time.Now(), Modified: time.Now(), Due: &dueDate, Until: &until, RecurrenceDuration: &duration, ParentUUID: nil, Tags: []string{}, } if err := template.Save(); err != nil { t.Fatalf("Failed to save template: %v", err) } // Create instance instance := &Task{ UUID: uuid.New(), Status: StatusPending, Description: template.Description, Priority: template.Priority, Created: time.Now(), Modified: time.Now(), Due: template.Due, Until: template.Until, ParentUUID: &template.UUID, Tags: []string{}, } if err := instance.Save(); err != nil { t.Fatalf("Failed to save instance: %v", err) } // Count pending instances before filter := DefaultFilter() beforeTasks, _ := GetTasks(filter) beforeCount := 0 for _, task := range beforeTasks { if task.ParentUUID != nil && *task.ParentUUID == template.UUID && task.Status == StatusPending { beforeCount++ } } // Complete instance - should NOT spawn new one (past until date) if err := instance.Complete(); err != nil { t.Fatalf("Failed to complete instance: %v", err) } // Count pending instances after afterTasks, _ := GetTasks(filter) afterCount := 0 for _, task := range afterTasks { if task.ParentUUID != nil && *task.ParentUUID == template.UUID && task.Status == StatusPending { afterCount++ } } // Should have one less (the one we completed) and no new one spawned if afterCount != beforeCount-1 { t.Errorf("Expected %d pending instances after, got %d (should not spawn due to until date)", beforeCount-1, afterCount) } }