124 lines
4.3 KiB
Go
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)
|
|
}
|
|
}
|