|
1 | 1 | package auth |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "context" |
4 | 5 | "sync/atomic" |
5 | 6 | "testing" |
6 | 7 | "time" |
@@ -37,6 +38,25 @@ func TestFastSchedulerAcquirePrefersHealthyTier(t *testing.T) { |
37 | 38 | } |
38 | 39 | } |
39 | 40 |
|
| 41 | +func TestFastSchedulerSkipsDispatchPausedAccount(t *testing.T) { |
| 42 | + paused := newFastSchedulerTestAccount(1, HealthTierHealthy, 120, 2) |
| 43 | + atomic.StoreInt32(&paused.DispatchPaused, 1) |
| 44 | + fallback := newFastSchedulerTestAccount(2, HealthTierHealthy, 80, 2) |
| 45 | + |
| 46 | + scheduler := NewFastScheduler(2) |
| 47 | + scheduler.Rebuild([]*Account{paused, fallback}) |
| 48 | + |
| 49 | + got := scheduler.Acquire() |
| 50 | + if got == nil { |
| 51 | + t.Fatal("Acquire() returned nil") |
| 52 | + } |
| 53 | + defer scheduler.Release(got) |
| 54 | + |
| 55 | + if got.DBID != fallback.DBID { |
| 56 | + t.Fatalf("Acquire() picked dbID=%d, want %d", got.DBID, fallback.DBID) |
| 57 | + } |
| 58 | +} |
| 59 | + |
40 | 60 | func TestFastSchedulerRespectsConcurrencyLimit(t *testing.T) { |
41 | 61 | acc := newFastSchedulerTestAccount(1, HealthTierHealthy, 100, 1) |
42 | 62 |
|
@@ -108,6 +128,94 @@ func TestStoreNextExcludingRespectsAPIKeyWhitelist(t *testing.T) { |
108 | 128 | } |
109 | 129 | } |
110 | 130 |
|
| 131 | +func TestStoreNextSkipsDispatchPausedAccount(t *testing.T) { |
| 132 | + paused := newFastSchedulerTestAccount(1, HealthTierHealthy, 120, 1) |
| 133 | + atomic.StoreInt32(&paused.DispatchPaused, 1) |
| 134 | + fallback := newFastSchedulerTestAccount(2, HealthTierHealthy, 80, 1) |
| 135 | + |
| 136 | + store := &Store{ |
| 137 | + accounts: []*Account{paused, fallback}, |
| 138 | + maxConcurrency: 1, |
| 139 | + } |
| 140 | + |
| 141 | + got := store.Next() |
| 142 | + if got == nil { |
| 143 | + t.Fatal("Next() returned nil") |
| 144 | + } |
| 145 | + defer store.Release(got) |
| 146 | + |
| 147 | + if got.DBID != fallback.DBID { |
| 148 | + t.Fatalf("Next() picked dbID=%d, want %d", got.DBID, fallback.DBID) |
| 149 | + } |
| 150 | +} |
| 151 | + |
| 152 | +func TestDispatchPausedDoesNotBlockUsageProbe(t *testing.T) { |
| 153 | + paused := newFastSchedulerTestAccount(1, HealthTierHealthy, 120, 1) |
| 154 | + atomic.StoreInt32(&paused.DispatchPaused, 1) |
| 155 | + |
| 156 | + store := &Store{ |
| 157 | + accounts: []*Account{paused}, |
| 158 | + } |
| 159 | + var probed int32 |
| 160 | + store.SetUsageProbeFunc(func(_ context.Context, account *Account) error { |
| 161 | + if account.DBID != paused.DBID { |
| 162 | + t.Fatalf("usage probe account dbID=%d, want %d", account.DBID, paused.DBID) |
| 163 | + } |
| 164 | + atomic.AddInt32(&probed, 1) |
| 165 | + return nil |
| 166 | + }) |
| 167 | + |
| 168 | + store.parallelProbeUsage(context.Background()) |
| 169 | + |
| 170 | + if got := atomic.LoadInt32(&probed); got != 1 { |
| 171 | + t.Fatalf("usage probe calls = %d, want 1", got) |
| 172 | + } |
| 173 | +} |
| 174 | + |
| 175 | +func TestDispatchPausedDoesNotBlockRecoveryProbe(t *testing.T) { |
| 176 | + paused := newFastSchedulerTestAccount(1, HealthTierBanned, 120, 1) |
| 177 | + paused.RefreshToken = "rt" |
| 178 | + paused.ExpiresAt = time.Now().Add(time.Hour) |
| 179 | + atomic.StoreInt32(&paused.DispatchPaused, 1) |
| 180 | + |
| 181 | + store := &Store{ |
| 182 | + accounts: []*Account{paused}, |
| 183 | + } |
| 184 | + var probed int32 |
| 185 | + store.SetUsageProbeFunc(func(_ context.Context, account *Account) error { |
| 186 | + if account.DBID != paused.DBID { |
| 187 | + t.Fatalf("recovery probe account dbID=%d, want %d", account.DBID, paused.DBID) |
| 188 | + } |
| 189 | + atomic.AddInt32(&probed, 1) |
| 190 | + return nil |
| 191 | + }) |
| 192 | + |
| 193 | + store.parallelRecoveryProbe(context.Background()) |
| 194 | + |
| 195 | + if got := atomic.LoadInt32(&probed); got != 1 { |
| 196 | + t.Fatalf("recovery probe calls = %d, want 1", got) |
| 197 | + } |
| 198 | + if atomic.LoadInt32(&paused.DispatchPaused) != 1 { |
| 199 | + t.Fatal("recovery probe cleared DispatchPaused; enable/disable must remain independent") |
| 200 | + } |
| 201 | +} |
| 202 | + |
| 203 | +func TestDispatchPausedDoesNotBlockAutoClean(t *testing.T) { |
| 204 | + paused := newFastSchedulerTestAccount(1, HealthTierBanned, 120, 1) |
| 205 | + atomic.StoreInt32(&paused.DispatchPaused, 1) |
| 206 | + |
| 207 | + store := &Store{ |
| 208 | + accounts: []*Account{paused}, |
| 209 | + } |
| 210 | + |
| 211 | + if cleaned := store.CleanByRuntimeStatus(context.Background(), "unauthorized"); cleaned != 1 { |
| 212 | + t.Fatalf("CleanByRuntimeStatus cleaned %d accounts, want 1", cleaned) |
| 213 | + } |
| 214 | + if got := store.AccountCount(); got != 0 { |
| 215 | + t.Fatalf("AccountCount() = %d, want 0", got) |
| 216 | + } |
| 217 | +} |
| 218 | + |
111 | 219 | func TestStoreNextExcludingWithFilterRespectsPlanFilter(t *testing.T) { |
112 | 220 | plus := newFastSchedulerTestAccount(1, HealthTierHealthy, 120, 1) |
113 | 221 | plus.PlanType = "plus" |
|
0 commit comments