package health import ( "errors" "testing" "time" healthDomain "proxy-pool/internal/domain/health" proxyDomain "proxy-pool/internal/domain/proxy" ) func TestPlannerPrioritizesAndBoundsOverdueCandidates(t *testing.T) { planner, err := NewPlanner(SchedulePolicy{ Interval: time.Minute, Jitter: 20, MaxInFlight: 3, Timeout: 4 * time.Second, MaxAttempts: 2, }) if err != nil { t.Fatalf("NewPlanner(): %v", err) } now := time.Date(2026, 7, 31, 17, 0, 0, 0, time.UTC) plans, err := planner.Plan(now, 1, 10, []Candidate{ {ProxyID: "available", State: proxyDomain.StateAvailable, Level: healthDomain.LevelBasic, DueAt: now.Add(-time.Second)}, {ProxyID: "suspect", State: proxyDomain.StateSuspect, Level: healthDomain.LevelEgress, TargetURL: "https://egress.example/identity", DueAt: now.Add(-2 * time.Second)}, {ProxyID: "fetched", State: proxyDomain.StateFetched, Level: healthDomain.LevelBasic, DueAt: now.Add(-3 * time.Second)}, {ProxyID: "future", State: proxyDomain.StateFetched, Level: healthDomain.LevelBasic, DueAt: now.Add(time.Second)}, }) if err != nil || len(plans) != 2 { t.Fatalf("Plan() = (%+v, %v)", plans, err) } if plans[0].Candidate.ProxyID != "fetched" || plans[0].Priority != PriorityFetched || plans[1].Candidate.ProxyID != "suspect" || plans[1].Priority != PrioritySuspect || plans[0].Deadline != now.Add(4*time.Second) || plans[0].Attempts != 2 { t.Fatalf("plans = %+v", plans) } } func TestPlannerSchedulesUnhealthyForRecovery(t *testing.T) { planner, err := NewPlanner(SchedulePolicy{ Interval: time.Minute, MaxInFlight: 1, Timeout: time.Second, MaxAttempts: 1, }) if err != nil { t.Fatalf("NewPlanner(): %v", err) } now := time.Date(2026, 7, 31, 17, 0, 0, 0, time.UTC) plans, err := planner.Plan(now, 0, 1, []Candidate{{ ProxyID: "unhealthy", State: proxyDomain.StateUnhealthy, Level: healthDomain.LevelBasic, DueAt: now, }}) if err != nil || len(plans) != 1 || plans[0].Priority != PriorityUnhealthy { t.Fatalf("Plan(unhealthy) = (%+v, %v)", plans, err) } } func TestPlannerDerivesStableBoundedJitter(t *testing.T) { planner, err := NewPlanner(SchedulePolicy{ Interval: time.Minute, Jitter: 20, MaxInFlight: 1, Timeout: time.Second, MaxAttempts: 1, }) if err != nil { t.Fatalf("NewPlanner(): %v", err) } checkedAt := time.Date(2026, 7, 31, 17, 0, 0, 0, time.UTC) first, err := planner.NextDue(checkedAt, "proxy-a\x00BASIC") if err != nil { t.Fatalf("NextDue(first): %v", err) } second, err := planner.NextDue(checkedAt, "proxy-a\x00BASIC") if err != nil || second != first { t.Fatalf("NextDue(stable) = (%v, %v), want %v", second, err, first) } base := checkedAt.Add(time.Minute) if delta := first.Sub(base); delta < -12*time.Second || delta > 12*time.Second { t.Fatalf("NextDue jitter delta = %s, want [-12s, 12s]", delta) } } func TestPlannerRejectsInvalidCandidatesWithoutPartialPlan(t *testing.T) { planner, err := NewPlanner(SchedulePolicy{ Interval: time.Minute, MaxInFlight: 1, Timeout: time.Second, MaxAttempts: 1, }) if err != nil { t.Fatalf("NewPlanner(): %v", err) } _, err = planner.Plan(time.Now(), 0, 1, []Candidate{{ ProxyID: "proxy-a", State: proxyDomain.StateFetched, Level: healthDomain.LevelTarget, RoutingName: "route-a", TargetURL: "not-a-url", DueAt: time.Now(), }}) if !errors.Is(err, ErrInvalidScheduleRequest) { t.Fatalf("Plan(invalid target) error = %v, want ErrInvalidScheduleRequest", err) } }