package executionpool import ( "errors" "testing" "time" ) func TestSelectorRejectsUnreachableAndPrefersNetwork(t *testing.T) { now := time.Now() selector := NewSelector() decision, err := selector.Select(SelectionRequest{Now: now, Candidates: []SelectionCandidate{ candidate("slow", RouteHealthy, now, 120*time.Millisecond, 8, 1), candidate("fast", RouteHealthy, now, 20*time.Millisecond, 8, 1), candidate("down", RouteUnreachable, now, time.Millisecond, 32, 0), }}) if err != nil { t.Fatal(err) } if decision.PoolID != "fast" { t.Fatalf("pool=%s, want fast", decision.PoolID) } if decision.Rejected["down"] != "route_unreachable" { t.Fatalf("down rejection=%q", decision.Rejected["down"]) } } func TestSelectorUsesCapacityAsHardGate(t *testing.T) { now := time.Now() selector := NewSelector() decision, err := selector.Select(SelectionRequest{Now: now, Candidates: []SelectionCandidate{ candidate("fast-full", RouteHealthy, now, 10*time.Millisecond, 2, 2), candidate("slower-free", RouteHealthy, now, 80*time.Millisecond, 8, 1), }}) if err != nil { t.Fatal(err) } if decision.PoolID != "slower-free" { t.Fatalf("pool=%s, want slower-free", decision.PoolID) } } func TestSelectorRejectsUnknownAndStale(t *testing.T) { now := time.Now() selector := NewSelector() _, err := selector.Select(SelectionRequest{Now: now, Candidates: []SelectionCandidate{ candidate("unknown", RouteUnknown, now, time.Millisecond, 8, 0), candidate("stale", RouteHealthy, now.Add(-time.Minute), time.Millisecond, 8, 0), }}) if !errors.Is(err, ErrNoEligiblePool) { t.Fatalf("err=%v, want ErrNoEligiblePool", err) } } func TestSelectorHonorsHysteresis(t *testing.T) { now := time.Now() selector := NewSelector() fast := candidate("fast", RouteHealthy, now, 20*time.Millisecond, 8, 1) fast.BetterWindows = 3 current := candidate("current", RouteHealthy, now, 80*time.Millisecond, 8, 1) decision, err := selector.Select(SelectionRequest{ Now: now, Candidates: []SelectionCandidate{fast, current}, CurrentPoolID: "current", CurrentPoolSince: now.Add(-time.Minute), }) if err != nil { t.Fatal(err) } if decision.PoolID != "current" { t.Fatalf("pool=%s, want current during dwell", decision.PoolID) } } func candidate(id string, state RouteState, sampled time.Time, latency time.Duration, capacity, active int) SelectionCandidate { return SelectionCandidate{ Pool: ExecutionPool{ID: id, State: PoolActive}, CapabilityMatched: true, Health: RouteHealth{ PoolID: id, State: state, SuccessRate: 1, ConnectTLSP95: latency, FirstByteP95: latency, UploadBytesPerSecond: 10 * 1024 * 1024, JitterP95: latency / 10, SampledAt: sampled, ExpiresAt: sampled.Add(45 * time.Second), }, Capacity: CapacitySnapshot{ PoolID: id, WorkerCount: 1, SafeCapacity: capacity, ActiveTasks: active, ResourceHeadroom: 0.8, Stability: 1, }, } }