package service import ( "context" "net/http" "testing" "time" ) type accountUsageCodexProbeRepo struct { stubOpenAIAccountRepo updateExtraCh chan map[string]any rateLimitCh chan time.Time } func (r *accountUsageCodexProbeRepo) UpdateExtra(_ context.Context, _ int64, updates map[string]any) error { if r.updateExtraCh != nil { copied := make(map[string]any, len(updates)) for k, v := range updates { copied[k] = v } r.updateExtraCh <- copied } return nil } func (r *accountUsageCodexProbeRepo) SetRateLimited(_ context.Context, _ int64, resetAt time.Time) error { if r.rateLimitCh != nil { r.rateLimitCh <- resetAt } return nil } func TestShouldRefreshOpenAICodexSnapshot(t *testing.T) { t.Parallel() rateLimitedUntil := time.Now().Add(5 * time.Minute) now := time.Now() usage := &UsageInfo{ FiveHour: &UsageProgress{Utilization: 0}, SevenDay: &UsageProgress{Utilization: 0}, } if !shouldRefreshOpenAICodexSnapshot(&Account{RateLimitResetAt: &rateLimitedUntil}, usage, now) { t.Fatal("expected rate-limited account to force codex snapshot refresh") } if shouldRefreshOpenAICodexSnapshot(&Account{}, usage, now) { t.Fatal("expected complete non-rate-limited usage to skip codex snapshot refresh") } if !shouldRefreshOpenAICodexSnapshot(&Account{}, &UsageInfo{FiveHour: nil, SevenDay: &UsageProgress{}}, now) { t.Fatal("expected missing 5h snapshot to require refresh") } staleAt := now.Add(-(openAIProbeCacheTTL + time.Minute)).Format(time.RFC3339) if !shouldRefreshOpenAICodexSnapshot(&Account{ Platform: PlatformOpenAI, Type: AccountTypeOAuth, Extra: map[string]any{ "openai_oauth_responses_websockets_v2_enabled": true, "codex_usage_updated_at": staleAt, }, }, usage, now) { t.Fatal("expected stale ws snapshot to trigger refresh") } } func TestExtractOpenAICodexProbeUpdatesAccepts429WithCodexHeaders(t *testing.T) { t.Parallel() headers := make(http.Header) headers.Set("x-codex-primary-used-percent", "100") headers.Set("x-codex-primary-reset-after-seconds", "604800") headers.Set("x-codex-primary-window-minutes", "10080") headers.Set("x-codex-secondary-used-percent", "100") headers.Set("x-codex-secondary-reset-after-seconds", "18000") headers.Set("x-codex-secondary-window-minutes", "300") updates, err := extractOpenAICodexProbeUpdates(&http.Response{StatusCode: http.StatusTooManyRequests, Header: headers}) if err != nil { t.Fatalf("extractOpenAICodexProbeUpdates() error = %v", err) } if len(updates) == 0 { t.Fatal("expected codex probe updates from 429 headers") } if got := updates["codex_5h_used_percent"]; got != 100.0 { t.Fatalf("codex_5h_used_percent = %v, want 100", got) } if got := updates["codex_7d_used_percent"]; got != 100.0 { t.Fatalf("codex_7d_used_percent = %v, want 100", got) } } func TestExtractOpenAICodexProbeSnapshotAccepts429WithResetAt(t *testing.T) { t.Parallel() headers := make(http.Header) headers.Set("x-codex-primary-used-percent", "100") headers.Set("x-codex-primary-reset-after-seconds", "604800") headers.Set("x-codex-primary-window-minutes", "10080") headers.Set("x-codex-secondary-used-percent", "100") headers.Set("x-codex-secondary-reset-after-seconds", "18000") headers.Set("x-codex-secondary-window-minutes", "300") updates, resetAt, err := extractOpenAICodexProbeSnapshot(&http.Response{StatusCode: http.StatusTooManyRequests, Header: headers}) if err != nil { t.Fatalf("extractOpenAICodexProbeSnapshot() error = %v", err) } if len(updates) == 0 { t.Fatal("expected codex probe updates from 429 headers") } if resetAt == nil { t.Fatal("expected resetAt from exhausted codex headers") } } func TestAccountUsageService_PersistOpenAICodexProbeSnapshotSetsRateLimit(t *testing.T) { t.Parallel() repo := &accountUsageCodexProbeRepo{ updateExtraCh: make(chan map[string]any, 1), rateLimitCh: make(chan time.Time, 1), } svc := &AccountUsageService{accountRepo: repo} resetAt := time.Now().Add(2 * time.Hour).UTC().Truncate(time.Second) svc.persistOpenAICodexProbeSnapshot(321, map[string]any{ "codex_7d_used_percent": 100.0, "codex_7d_reset_at": resetAt.Format(time.RFC3339), }, &resetAt) select { case updates := <-repo.updateExtraCh: if got := updates["codex_7d_used_percent"]; got != 100.0 { t.Fatalf("codex_7d_used_percent = %v, want 100", got) } case <-time.After(2 * time.Second): t.Fatal("waiting for codex probe extra persistence timed out") } select { case got := <-repo.rateLimitCh: if got.Before(resetAt.Add(-time.Second)) || got.After(resetAt.Add(time.Second)) { t.Fatalf("rate limit resetAt = %v, want around %v", got, resetAt) } case <-time.After(2 * time.Second): t.Fatal("waiting for codex probe rate limit persistence timed out") } }