Add smart notification reminder system with frequency-aware scheduling
Replaces one-size-fits-all "2 days before" reminders with intelligent
scheduling based on task frequency. Infrequent tasks (annual) get 30-day
advance notice while frequent tasks (weekly) only get day-of reminders.
Key features:
- Frequency-aware pre-reminders: annual (30d, 14d, 7d), quarterly (7d, 3d),
monthly (3d), bi-weekly (1d), daily/weekly/once (day-of only)
- Overdue tapering: daily for 3 days, then every 3 days, stops after 14 days
- Reminder log table prevents duplicate notifications per due date/stage
- Admin endpoint displays notification schedules for all frequencies
- Comprehensive test suite (100 random tasks, 61 days each, 10 test functions)
New files:
- internal/notifications/reminder_config.go - Editable schedule configuration
- internal/notifications/reminder_schedule.go - Schedule lookup logic
- internal/notifications/reminder_schedule_test.go - Dynamic test suite
- internal/models/reminder_log.go - TaskReminderLog model
- internal/repositories/reminder_repo.go - Reminder log repository
- migrations/010_add_task_reminder_log.{up,down}.sql
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
524
internal/notifications/reminder_schedule_test.go
Normal file
524
internal/notifications/reminder_schedule_test.go
Normal file
@@ -0,0 +1,524 @@
|
||||
package notifications
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestReminderScheduleWith100RandomTasks creates 100 tasks with random frequencies
|
||||
// and validates that notifications are sent on exactly the right days according to
|
||||
// the current FrequencySchedules and OverdueConfig.
|
||||
//
|
||||
// This test is dynamic - it reads from the config, so if the config changes,
|
||||
// the test will still validate the correct behavior.
|
||||
func TestReminderScheduleWith100RandomTasks(t *testing.T) {
|
||||
// Seed random for reproducibility in tests (use current time for variety)
|
||||
rng := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
|
||||
// Available frequency intervals (from OrderedFrequencies)
|
||||
// We'll also add some custom intervals to test the tier mapping
|
||||
frequencyIntervals := []int{0, 1, 7, 14, 30, 90, 180, 365}
|
||||
customIntervals := []int{5, 10, 20, 45, 120, 200, 400} // Custom intervals to test tier mapping
|
||||
|
||||
allIntervals := append(frequencyIntervals, customIntervals...)
|
||||
|
||||
// Base date for tests - use a fixed date for deterministic behavior
|
||||
baseDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
// Track test statistics
|
||||
tasksCreated := 0
|
||||
daysSimulated := 0
|
||||
notificationsSent := 0
|
||||
notificationsExpected := 0
|
||||
|
||||
for i := 0; i < 100; i++ {
|
||||
// Random frequency
|
||||
intervalIdx := rng.Intn(len(allIntervals))
|
||||
intervalDays := allIntervals[intervalIdx]
|
||||
|
||||
// Random due date offset (-30 to +30 days from base date)
|
||||
dueDateOffset := rng.Intn(61) - 30
|
||||
dueDate := baseDate.AddDate(0, 0, dueDateOffset)
|
||||
|
||||
// Create frequency pointer (nil for "Once" which is interval 0)
|
||||
var frequencyDays *int
|
||||
if intervalDays > 0 {
|
||||
frequencyDays = &intervalDays
|
||||
}
|
||||
|
||||
tasksCreated++
|
||||
|
||||
// Get the expected schedule for this frequency
|
||||
expectedSchedule := GetScheduleForFrequency(frequencyDays)
|
||||
expectedOverdueDays := GetOverdueReminderDays()
|
||||
|
||||
// Simulate each day from 45 days before to 15 days after due date
|
||||
for dayOffset := -45; dayOffset <= 15; dayOffset++ {
|
||||
today := dueDate.AddDate(0, 0, -dayOffset) // dayOffset is days until due, so we subtract
|
||||
daysSimulated++
|
||||
|
||||
// Get the reminder stage for today
|
||||
stage := GetReminderStageForToday(dueDate, frequencyDays, today)
|
||||
|
||||
// Calculate what we expect
|
||||
expectedStage := calculateExpectedStage(dayOffset, expectedSchedule, expectedOverdueDays)
|
||||
|
||||
if stage != expectedStage {
|
||||
t.Errorf("Task %d (interval=%d, dueDate=%s): on %s (dayOffset=%d), got stage=%q, expected=%q",
|
||||
i+1, intervalDays, dueDate.Format("2006-01-02"),
|
||||
today.Format("2006-01-02"), dayOffset, stage, expectedStage)
|
||||
}
|
||||
|
||||
if stage != "" {
|
||||
notificationsSent++
|
||||
}
|
||||
if expectedStage != "" {
|
||||
notificationsExpected++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Test Summary:")
|
||||
t.Logf(" Tasks created: %d", tasksCreated)
|
||||
t.Logf(" Days simulated: %d", daysSimulated)
|
||||
t.Logf(" Notifications sent: %d", notificationsSent)
|
||||
t.Logf(" Notifications expected: %d", notificationsExpected)
|
||||
|
||||
if notificationsSent != notificationsExpected {
|
||||
t.Errorf("Notification count mismatch: sent=%d, expected=%d", notificationsSent, notificationsExpected)
|
||||
}
|
||||
}
|
||||
|
||||
// calculateExpectedStage determines what stage should be returned based on the config
|
||||
func calculateExpectedStage(dayOffset int, preReminderDays []int, overdueDays []int) string {
|
||||
if dayOffset > 0 {
|
||||
// Task is in the future (dayOffset days until due)
|
||||
if slices.Contains(preReminderDays, dayOffset) {
|
||||
return formatDaysBeforeStage(dayOffset)
|
||||
}
|
||||
return ""
|
||||
} else if dayOffset == 0 {
|
||||
// Task is due today
|
||||
return "day_of"
|
||||
} else {
|
||||
// Task is overdue (-dayOffset days overdue)
|
||||
daysOverdue := -dayOffset
|
||||
if slices.Contains(overdueDays, daysOverdue) {
|
||||
return formatOverdueStage(daysOverdue)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// TestEachFrequencySchedule validates each frequency's schedule explicitly
|
||||
func TestEachFrequencySchedule(t *testing.T) {
|
||||
dueDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
// Test each configured frequency
|
||||
for intervalDays, expectedSchedule := range FrequencySchedules {
|
||||
t.Run(fmt.Sprintf("Interval_%d_days", intervalDays), func(t *testing.T) {
|
||||
var frequencyDays *int
|
||||
if intervalDays > 0 {
|
||||
frequencyDays = &intervalDays
|
||||
}
|
||||
|
||||
// Verify all expected reminder days trigger notifications
|
||||
for _, reminderDay := range expectedSchedule {
|
||||
today := dueDate.AddDate(0, 0, -reminderDay)
|
||||
stage := GetReminderStageForToday(dueDate, frequencyDays, today)
|
||||
|
||||
expectedStage := formatDaysBeforeStage(reminderDay)
|
||||
if stage != expectedStage {
|
||||
t.Errorf("Reminder %d days before: got %q, expected %q", reminderDay, stage, expectedStage)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify days NOT in the schedule don't trigger notifications (before due date)
|
||||
for day := 1; day <= 45; day++ {
|
||||
if !slices.Contains(expectedSchedule, day) {
|
||||
today := dueDate.AddDate(0, 0, -day)
|
||||
stage := GetReminderStageForToday(dueDate, frequencyDays, today)
|
||||
|
||||
if stage != "" {
|
||||
t.Errorf("Day %d before due should NOT send notification for interval %d, but got %q",
|
||||
day, intervalDays, stage)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestOverdueReminderSchedule validates the overdue tapering logic
|
||||
func TestOverdueReminderSchedule(t *testing.T) {
|
||||
dueDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
// Use nil frequency (Once) - overdue logic is the same for all frequencies
|
||||
var frequencyDays *int
|
||||
|
||||
// Get expected overdue days from config
|
||||
expectedOverdueDays := GetOverdueReminderDays()
|
||||
|
||||
t.Logf("OverdueConfig: DailyReminderDays=%d, TaperIntervalDays=%d, MaxOverdueDays=%d",
|
||||
OverdueConfig.DailyReminderDays, OverdueConfig.TaperIntervalDays, OverdueConfig.MaxOverdueDays)
|
||||
t.Logf("Expected overdue reminder days: %v", expectedOverdueDays)
|
||||
|
||||
// Test each day from 1 to MaxOverdueDays + 5
|
||||
for daysOverdue := 1; daysOverdue <= OverdueConfig.MaxOverdueDays+5; daysOverdue++ {
|
||||
today := dueDate.AddDate(0, 0, daysOverdue)
|
||||
stage := GetReminderStageForToday(dueDate, frequencyDays, today)
|
||||
|
||||
shouldSend := slices.Contains(expectedOverdueDays, daysOverdue)
|
||||
expectedStage := ""
|
||||
if shouldSend {
|
||||
expectedStage = formatOverdueStage(daysOverdue)
|
||||
}
|
||||
|
||||
if stage != expectedStage {
|
||||
t.Errorf("Day %d overdue: got %q, expected %q (shouldSend=%v)",
|
||||
daysOverdue, stage, expectedStage, shouldSend)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestCustomIntervalTierMapping validates that custom intervals map to correct tiers
|
||||
func TestCustomIntervalTierMapping(t *testing.T) {
|
||||
testCases := []struct {
|
||||
customInterval int
|
||||
expectedTier int
|
||||
expectedMessage string
|
||||
}{
|
||||
{2, 7, "2 days should map to Weekly (7) tier"},
|
||||
{5, 7, "5 days should map to Weekly (7) tier"},
|
||||
{8, 14, "8 days should map to Bi-Weekly (14) tier"},
|
||||
{10, 14, "10 days should map to Bi-Weekly (14) tier"},
|
||||
{20, 30, "20 days should map to Monthly (30) tier"},
|
||||
{45, 90, "45 days should map to Quarterly (90) tier"},
|
||||
{100, 180, "100 days should map to Semi-Annually (180) tier"},
|
||||
{200, 365, "200 days should map to Annually (365) tier"},
|
||||
{500, 365, "500 days should map to Annually (365) tier - fallback"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("CustomInterval_%d", tc.customInterval), func(t *testing.T) {
|
||||
schedule := GetScheduleForCustomInterval(tc.customInterval)
|
||||
expectedSchedule := FrequencySchedules[tc.expectedTier]
|
||||
|
||||
if !slices.Equal(schedule, expectedSchedule) {
|
||||
t.Errorf("%s: got schedule %v, expected %v (tier %d)",
|
||||
tc.expectedMessage, schedule, expectedSchedule, tc.expectedTier)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestDayOfNotification ensures day-of notifications always fire
|
||||
func TestDayOfNotification(t *testing.T) {
|
||||
dueDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC)
|
||||
today := dueDate // Same day
|
||||
|
||||
// Test all frequency types - day-of should always fire
|
||||
for intervalDays := range FrequencySchedules {
|
||||
t.Run(fmt.Sprintf("DayOf_Interval_%d", intervalDays), func(t *testing.T) {
|
||||
var frequencyDays *int
|
||||
if intervalDays > 0 {
|
||||
frequencyDays = &intervalDays
|
||||
}
|
||||
|
||||
stage := GetReminderStageForToday(dueDate, frequencyDays, today)
|
||||
if stage != "day_of" {
|
||||
t.Errorf("Day-of notification for interval %d: got %q, expected \"day_of\"",
|
||||
intervalDays, stage)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestNoNotificationAfterMaxOverdue ensures no notifications after MaxOverdueDays
|
||||
func TestNoNotificationAfterMaxOverdue(t *testing.T) {
|
||||
dueDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC)
|
||||
var frequencyDays *int // Once frequency
|
||||
|
||||
// Test days beyond MaxOverdueDays
|
||||
for daysOverdue := OverdueConfig.MaxOverdueDays + 1; daysOverdue <= OverdueConfig.MaxOverdueDays+30; daysOverdue++ {
|
||||
today := dueDate.AddDate(0, 0, daysOverdue)
|
||||
stage := GetReminderStageForToday(dueDate, frequencyDays, today)
|
||||
|
||||
if stage != "" {
|
||||
t.Errorf("Day %d overdue (beyond MaxOverdueDays=%d): should not send notification, but got %q",
|
||||
daysOverdue, OverdueConfig.MaxOverdueDays, stage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestNoNotificationTooFarInFuture ensures no notifications for tasks far in the future
|
||||
func TestNoNotificationTooFarInFuture(t *testing.T) {
|
||||
dueDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
// Get the maximum pre-reminder days from all schedules
|
||||
maxPreReminderDays := 0
|
||||
for _, schedule := range FrequencySchedules {
|
||||
for _, days := range schedule {
|
||||
if days > maxPreReminderDays {
|
||||
maxPreReminderDays = days
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test for each frequency type - days beyond their schedule should not trigger
|
||||
for intervalDays, schedule := range FrequencySchedules {
|
||||
t.Run(fmt.Sprintf("FarFuture_Interval_%d", intervalDays), func(t *testing.T) {
|
||||
var frequencyDays *int
|
||||
if intervalDays > 0 {
|
||||
frequencyDays = &intervalDays
|
||||
}
|
||||
|
||||
// Find the max reminder day for this frequency
|
||||
maxForFrequency := 0
|
||||
for _, d := range schedule {
|
||||
if d > maxForFrequency {
|
||||
maxForFrequency = d
|
||||
}
|
||||
}
|
||||
|
||||
// Test days beyond the max reminder day for this frequency
|
||||
for daysBefore := maxForFrequency + 1; daysBefore <= maxPreReminderDays+10; daysBefore++ {
|
||||
// Skip if this day is actually in the schedule
|
||||
if slices.Contains(schedule, daysBefore) {
|
||||
continue
|
||||
}
|
||||
|
||||
today := dueDate.AddDate(0, 0, -daysBefore)
|
||||
stage := GetReminderStageForToday(dueDate, frequencyDays, today)
|
||||
|
||||
if stage != "" {
|
||||
t.Errorf("Day %d before due (beyond schedule max=%d for interval %d): should not send, but got %q",
|
||||
daysBefore, maxForFrequency, intervalDays, stage)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestConfigConsistency validates that the configuration is internally consistent
|
||||
func TestConfigConsistency(t *testing.T) {
|
||||
// Verify GetOverdueReminderDays matches ShouldSendOverdueReminder
|
||||
overdueDays := GetOverdueReminderDays()
|
||||
|
||||
for day := 1; day <= OverdueConfig.MaxOverdueDays+5; day++ {
|
||||
shouldSend := ShouldSendOverdueReminder(day)
|
||||
isInList := slices.Contains(overdueDays, day)
|
||||
|
||||
if shouldSend != isInList {
|
||||
t.Errorf("Inconsistency at day %d: ShouldSendOverdueReminder=%v, but day in GetOverdueReminderDays=%v",
|
||||
day, shouldSend, isInList)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify all frequencies have 0 (day-of) in their schedule
|
||||
for intervalDays, schedule := range FrequencySchedules {
|
||||
if !slices.Contains(schedule, 0) {
|
||||
t.Errorf("Frequency %d days is missing day-of (0) in schedule: %v", intervalDays, schedule)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify schedules are sorted in descending order (most days first)
|
||||
for intervalDays, schedule := range FrequencySchedules {
|
||||
for i := 0; i < len(schedule)-1; i++ {
|
||||
if schedule[i] < schedule[i+1] {
|
||||
t.Errorf("Frequency %d schedule is not in descending order: %v", intervalDays, schedule)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestStageFormatting validates the stage string formatting
|
||||
func TestStageFormatting(t *testing.T) {
|
||||
testCases := []struct {
|
||||
daysBefore int
|
||||
expectedStage string
|
||||
}{
|
||||
{0, "day_of"},
|
||||
{1, "reminder_1d"},
|
||||
{3, "reminder_3d"},
|
||||
{7, "reminder_7d"},
|
||||
{14, "reminder_14d"},
|
||||
{30, "reminder_30d"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(fmt.Sprintf("Format_%d_days_before", tc.daysBefore), func(t *testing.T) {
|
||||
stage := formatDaysBeforeStage(tc.daysBefore)
|
||||
if stage != tc.expectedStage {
|
||||
t.Errorf("formatDaysBeforeStage(%d) = %q, expected %q", tc.daysBefore, stage, tc.expectedStage)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test overdue formatting
|
||||
overdueTestCases := []struct {
|
||||
daysOverdue int
|
||||
expectedStage string
|
||||
}{
|
||||
{1, "overdue_1"},
|
||||
{3, "overdue_3"},
|
||||
{7, "overdue_7"},
|
||||
{14, "overdue_14"},
|
||||
}
|
||||
|
||||
for _, tc := range overdueTestCases {
|
||||
t.Run(fmt.Sprintf("Format_%d_days_overdue", tc.daysOverdue), func(t *testing.T) {
|
||||
stage := formatOverdueStage(tc.daysOverdue)
|
||||
if stage != tc.expectedStage {
|
||||
t.Errorf("formatOverdueStage(%d) = %q, expected %q", tc.daysOverdue, stage, tc.expectedStage)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// BenchmarkGetReminderStageForToday benchmarks the main function
|
||||
func BenchmarkGetReminderStageForToday(b *testing.B) {
|
||||
dueDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC)
|
||||
today := time.Date(2025, 6, 12, 0, 0, 0, 0, time.UTC) // 3 days before
|
||||
intervalDays := 30
|
||||
frequencyDays := &intervalDays
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
GetReminderStageForToday(dueDate, frequencyDays, today)
|
||||
}
|
||||
}
|
||||
|
||||
// TestReminderScheduleTableDriven uses the config to generate a comprehensive
|
||||
// table-driven test that validates all expected notification days
|
||||
func TestReminderScheduleTableDriven(t *testing.T) {
|
||||
dueDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
// Build test cases from config
|
||||
for intervalDays, preSchedule := range FrequencySchedules {
|
||||
var frequencyDays *int
|
||||
if intervalDays > 0 {
|
||||
frequencyDays = &intervalDays
|
||||
}
|
||||
|
||||
freqName := FrequencyNames[intervalDays]
|
||||
if freqName == "" {
|
||||
freqName = fmt.Sprintf("Custom_%d", intervalDays)
|
||||
}
|
||||
|
||||
// Test pre-due reminders
|
||||
for _, daysBefore := range preSchedule {
|
||||
t.Run(fmt.Sprintf("%s_PreReminder_%dd", freqName, daysBefore), func(t *testing.T) {
|
||||
today := dueDate.AddDate(0, 0, -daysBefore)
|
||||
stage := GetReminderStageForToday(dueDate, frequencyDays, today)
|
||||
expected := formatDaysBeforeStage(daysBefore)
|
||||
|
||||
if stage != expected {
|
||||
t.Errorf("got %q, expected %q", stage, expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Test overdue reminders (same for all frequencies)
|
||||
overdueDays := GetOverdueReminderDays()
|
||||
for _, daysOverdue := range overdueDays {
|
||||
t.Run(fmt.Sprintf("%s_Overdue_%dd", freqName, daysOverdue), func(t *testing.T) {
|
||||
today := dueDate.AddDate(0, 0, daysOverdue)
|
||||
stage := GetReminderStageForToday(dueDate, frequencyDays, today)
|
||||
expected := formatOverdueStage(daysOverdue)
|
||||
|
||||
if stage != expected {
|
||||
t.Errorf("got %q, expected %q", stage, expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestFullSimulation100Tasks is the main comprehensive test that simulates
|
||||
// 100 random tasks over the full date range
|
||||
func TestFullSimulation100Tasks(t *testing.T) {
|
||||
// Use a fixed seed for reproducible tests
|
||||
rng := rand.New(rand.NewSource(42))
|
||||
|
||||
// All available frequencies including custom
|
||||
frequencies := []int{0, 1, 7, 14, 30, 90, 180, 365, 5, 10, 20, 45, 100, 200, 500}
|
||||
|
||||
baseDate := time.Date(2025, 6, 15, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
// Statistics
|
||||
totalDays := 0
|
||||
totalNotifications := 0
|
||||
notificationsByStage := make(map[string]int)
|
||||
|
||||
for taskIdx := 0; taskIdx < 100; taskIdx++ {
|
||||
// Random frequency
|
||||
intervalDays := frequencies[rng.Intn(len(frequencies))]
|
||||
|
||||
// Random due date (within 60-day range around base)
|
||||
dueDateOffset := rng.Intn(61) - 30
|
||||
dueDate := baseDate.AddDate(0, 0, dueDateOffset)
|
||||
|
||||
var frequencyDays *int
|
||||
if intervalDays > 0 {
|
||||
frequencyDays = &intervalDays
|
||||
}
|
||||
|
||||
// Get expected schedules from config
|
||||
preSchedule := GetScheduleForFrequency(frequencyDays)
|
||||
overdueDays := GetOverdueReminderDays()
|
||||
|
||||
// Simulate 45 days before to 15 days after due date
|
||||
for dayOffset := -45; dayOffset <= 15; dayOffset++ {
|
||||
// dayOffset: negative = future (before due), positive = past (overdue)
|
||||
// So if dayOffset = -10, we're 10 days BEFORE due date
|
||||
// If dayOffset = 5, we're 5 days AFTER due date (overdue)
|
||||
today := dueDate.AddDate(0, 0, dayOffset)
|
||||
totalDays++
|
||||
|
||||
stage := GetReminderStageForToday(dueDate, frequencyDays, today)
|
||||
|
||||
// Calculate expected stage
|
||||
var expectedStage string
|
||||
if dayOffset < 0 {
|
||||
// Before due date: check pre-reminders
|
||||
daysBefore := -dayOffset
|
||||
if slices.Contains(preSchedule, daysBefore) {
|
||||
expectedStage = formatDaysBeforeStage(daysBefore)
|
||||
}
|
||||
} else if dayOffset == 0 {
|
||||
// Day of
|
||||
expectedStage = "day_of"
|
||||
} else {
|
||||
// Overdue
|
||||
if slices.Contains(overdueDays, dayOffset) {
|
||||
expectedStage = formatOverdueStage(dayOffset)
|
||||
}
|
||||
}
|
||||
|
||||
if stage != expectedStage {
|
||||
t.Errorf("Task %d (freq=%d, due=%s): day %s (offset=%d): got %q, expected %q",
|
||||
taskIdx+1, intervalDays, dueDate.Format("2006-01-02"),
|
||||
today.Format("2006-01-02"), dayOffset, stage, expectedStage)
|
||||
}
|
||||
|
||||
if stage != "" {
|
||||
totalNotifications++
|
||||
notificationsByStage[stage]++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.Logf("Full Simulation Results:")
|
||||
t.Logf(" Total tasks: 100")
|
||||
t.Logf(" Total days simulated: %d", totalDays)
|
||||
t.Logf(" Total notifications: %d", totalNotifications)
|
||||
t.Logf(" Notifications by stage:")
|
||||
for stage, count := range notificationsByStage {
|
||||
t.Logf(" %s: %d", stage, count)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user