package coach import ( "testing" "time" ) func TestCalculateTargetUsesLockedPolicy(t *testing.T) { tests := []struct { metric, level string baseline float64 want float64 }{ {"success_rate", "MEDIUM", 81, 86}, {"paid_count", "STRETCH", 1000, 1200}, {"avg_latency", "SMALL", 2000, 1900}, } for _, tt := range tests { got, err := CalculateTarget(tt.metric, tt.level, tt.baseline, 100) if err != nil || got != tt.want { t.Fatalf("%s: got %v err=%v want %v", tt.metric, got, err, tt.want) } } if _, err := CalculateTarget("success_rate", "SMALL", 80, 99); err == nil { t.Fatal("expected minimum sample rejection") } if _, err := CalculateTarget("avg_latency", "SMALL", 0, 100); err == nil { t.Fatal("zero-baseline decrease goal must be rejected") } if _, err := CalculateTarget("success_rate", "SMALL", 100, 100); err == nil { t.Fatal("capped rate goal with no target distance must be rejected") } } func TestGoalProgressRequiresActionableDistance(t *testing.T) { if progress, ok := GoalProgress("DECREASE", 0, 0, 0); ok || progress != 0 { t.Fatalf("zero-distance progress = %v, %t", progress, ok) } if progress, ok := GoalProgress("INCREASE", 80, 90, 85); !ok || progress != 50 { t.Fatalf("progress = %v, %t", progress, ok) } if progress, ok := GoalProgress("DECREASE", 100, 80, 70); !ok || progress != 100 { t.Fatalf("clamped decrease progress = %v, %t", progress, ok) } } func TestMetricCannotBeManuallyCompleted(t *testing.T) { if _, err := ValidTransition("METRIC", "IN_PROGRESS", "COMPLETE"); err == nil { t.Fatal("metric completion must be backend-only") } if got, err := ValidTransition("METRIC", "IN_PROGRESS", "ABANDON"); err != nil || got != "ABANDONED" { t.Fatalf("got %q err=%v", got, err) } } func TestMetricTargetReachedRequiresActualTarget(t *testing.T) { if MetricTargetReached("INCREASE", 109, 110) { t.Fatal("increase target completed before reaching target") } if !MetricTargetReached("INCREASE", 110, 110) { t.Fatal("increase target did not complete at target") } if MetricTargetReached("DECREASE", 91, 90) { t.Fatal("decrease target completed before reaching target") } if !MetricTargetReached("DECREASE", 90, 90) { t.Fatal("decrease target did not complete at target") } } func TestPeriodBoundsUsesSaturdayAndJalaliMonth(t *testing.T) { start, end := PeriodBounds("WEEKLY", time.Date(2026, 8, 21, 12, 0, 0, 0, time.UTC)) if start.Weekday() != time.Saturday || end.Weekday() != time.Friday { t.Fatalf("week %v..%v", start, end) } monthStart, monthEnd := PeriodBounds("MONTHLY", time.Date(2026, 3, 25, 0, 0, 0, 0, time.UTC)) if monthStart.Day() != 21 || monthEnd.Before(monthStart) { t.Fatalf("month %v..%v", monthStart, monthEnd) } } func TestManualRefreshPolicy(t *testing.T) { tests := []struct { name string cadence string weeklyUses int wantAllowed bool }{ {name: "first weekly refresh", cadence: "WEEKLY", weeklyUses: 0, wantAllowed: true}, {name: "second weekly refresh", cadence: "WEEKLY", weeklyUses: 1, wantAllowed: true}, {name: "third weekly refresh is limited", cadence: "WEEKLY", weeklyUses: 2, wantAllowed: false}, {name: "monthly refresh is scoped but not weekly-limited", cadence: "MONTHLY", weeklyUses: 9, wantAllowed: true}, {name: "daily manual refresh is not exposed", cadence: "DAILY", weeklyUses: 0, wantAllowed: false}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if got := ManualRefreshAllowed(tt.cadence, tt.weeklyUses); got != tt.wantAllowed { t.Fatalf("ManualRefreshAllowed(%q, %d) = %t, want %t", tt.cadence, tt.weeklyUses, got, tt.wantAllowed) } }) } } func TestNextTehranMidnight(t *testing.T) { now := time.Date(2026, 8, 21, 20, 29, 0, 0, time.UTC) want := time.Date(2026, 8, 22, 0, 0, 0, 0, time.FixedZone("Asia/Tehran", 3*60*60+30*60)) if got := NextTehranMidnight(now); !got.Equal(want) { t.Fatalf("NextTehranMidnight(%v) = %v, want %v", now, got, want) } } func TestDifficultyRequiresMerchantConfirmationSignal(t *testing.T) { level, _, ok := ProposeDifficulty(DifficultyHistory{Current: 2, RecentWeeklyRates: []float64{.8, .9}}) if !ok || level != 3 { t.Fatalf("got level=%d ok=%v", level, ok) } level, _, ok = ProposeDifficulty(DifficultyHistory{Current: 3, HardRejections: 2}) if !ok || level != 2 { t.Fatalf("got level=%d ok=%v", level, ok) } }