Files

124 lines
4.3 KiB
Go

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)
}
}