package retry import ( "net/http" "testing" "time" ) // ============================================================ // SubscriptionAwareRetry 测试 // ============================================================ func TestSubscriptionAwareRetry_SubscriberNoRetry(t *testing.T) { s := &SubscriptionAwareRetry{ IsSubscriber: func() bool { return true }, IsEnterprise: func() bool { return false }, } err := makeErr("rate_limit") d := s.ShouldRetry(err, 1, defaultCtx()) if d == nil || d.Retry { t.Error("subscriber should not retry 429") } } func TestSubscriptionAwareRetry_EnterpriseCanRetry(t *testing.T) { s := &SubscriptionAwareRetry{ IsSubscriber: func() bool { return true }, IsEnterprise: func() bool { return true }, } err := makeErr("rate_limit") d := s.ShouldRetry(err, 1, defaultCtx()) if d != nil { t.Error("enterprise should pass through (nil = no opinion)") } } func TestSubscriptionAwareRetry_NonSubscriberPasses(t *testing.T) { s := &SubscriptionAwareRetry{ IsSubscriber: func() bool { return false }, } err := makeErr("rate_limit") d := s.ShouldRetry(err, 1, defaultCtx()) if d != nil { t.Error("non-subscriber should pass through") } } func TestSubscriptionAwareRetry_NonRateLimitPasses(t *testing.T) { s := &SubscriptionAwareRetry{ IsSubscriber: func() bool { return true }, } err := makeErr("server_error") d := s.ShouldRetry(err, 1, defaultCtx()) if d != nil { t.Error("non-ratelimit should pass through") } } // ============================================================ // FastModeCooldown 测试 // ============================================================ func TestFastModeCooldown_ShortRetry(t *testing.T) { f := DefaultFastModeCooldown() f.IsFastMode = func() bool { return true } err := &mockRetryError{ category: "rate_limit", retryable: true, retryInfo: &RetryInfo{Retryable: true, After: 5 * time.Second}, } d := f.ShouldRetry(err, 1, defaultCtx()) if d == nil || !d.Retry { t.Fatal("short retry should succeed") } if d.Delay != 5*time.Second { t.Errorf("delay = %v, want 5s", d.Delay) } if d.Reason != "fast mode short retry (preserving prompt cache)" { t.Errorf("reason = %q", d.Reason) } } func TestFastModeCooldown_LongRetryCooldown(t *testing.T) { var cooldownReason string f := DefaultFastModeCooldown() f.IsFastMode = func() bool { return true } f.OnCooldownStart = func(d time.Duration, reason string) { cooldownReason = reason } err := &mockRetryError{ category: "rate_limit", retryable: true, retryInfo: &RetryInfo{Retryable: true, After: 60 * time.Second}, } d := f.ShouldRetry(err, 1, defaultCtx()) if d == nil || !d.Retry { t.Fatal("cooldown should still retry (at standard speed)") } if d.Delay != 0 { t.Errorf("should retry immediately (model already switched), got %v", d.Delay) } if cooldownReason != "rate_limit" { t.Errorf("cooldown reason = %q, want 'rate_limit'", cooldownReason) } } func TestFastModeCooldown_OverageDisable(t *testing.T) { var disableReason string f := DefaultFastModeCooldown() f.IsFastMode = func() bool { return true } f.OnPermanentDisable = func(reason string) { disableReason = reason } headers := http.Header{} headers.Set("anthropic-ratelimit-unified-overage-disabled-reason", "quota_exceeded") err := &mockRetryError{ category: "rate_limit", retryable: true, retryInfo: &RetryInfo{Retryable: true}, headers: headers, } d := f.ShouldRetry(err, 1, defaultCtx()) if d == nil || !d.Retry { t.Fatal("should retry at standard speed") } if disableReason != "quota_exceeded" { t.Errorf("disable reason = %q, want 'quota_exceeded'", disableReason) } } func TestFastModeCooldown_NotFastMode(t *testing.T) { f := DefaultFastModeCooldown() f.IsFastMode = func() bool { return false } err := makeErr("rate_limit") d := f.ShouldRetry(err, 1, defaultCtx()) if d != nil { t.Error("not in fast mode should pass through") } } // ============================================================ // ModelFallback 测试 // ============================================================ func TestModelFallback_TriggeredWithFallback(t *testing.T) { m := &ModelFallback{ConsecutiveThreshold: 3} err := makeErr("server_overload") ctx := &RetryContext{ ConsecutiveCounts: map[string]int{"server_overload": 3}, FallbackModel: "sonnet-4", } d := m.ShouldRetry(err, 1, ctx) if d == nil { t.Fatal("should give decision at threshold") } if d.Retry { t.Error("should not retry (fallback signal)") } if d.Reason != "model_fallback:sonnet-4" { t.Errorf("reason = %q", d.Reason) } } func TestModelFallback_NotTriggeredBelowThreshold(t *testing.T) { m := &ModelFallback{ConsecutiveThreshold: 3} err := makeErr("server_overload") ctx := &RetryContext{ ConsecutiveCounts: map[string]int{"server_overload": 2}, FallbackModel: "sonnet-4", } d := m.ShouldRetry(err, 1, ctx) if d != nil { t.Error("below threshold should pass through") } } func TestModelFallback_NoFallbackModel(t *testing.T) { m := &ModelFallback{ConsecutiveThreshold: 3} err := makeErr("server_overload") ctx := &RetryContext{ ConsecutiveCounts: map[string]int{"server_overload": 5}, FallbackModel: "", // 没有降级模型 } d := m.ShouldRetry(err, 1, ctx) if d != nil { t.Error("no fallback model should pass through") } } // ============================================================ // NewAnthropicRetryPolicy 集成测试 // ============================================================ func TestAnthropicRetryPolicy_BasicFlow(t *testing.T) { p := NewAnthropicRetryPolicy(AnthropicRetryOpts{ MaxRetries: 5, }) // 前景 + 可重试 → 应该重试 err := &mockRetryError{ category: "server_error", retryable: true, retryInfo: &RetryInfo{Retryable: true}, } d := p.ShouldRetry(err, 1, defaultCtx()) if d == nil || !d.Retry { t.Error("should retry server error") } // 后台 + 过载 → 应该放弃 bgCtx := &RetryContext{IsForeground: false, ConsecutiveCounts: make(map[string]int)} d2 := p.ShouldRetry(makeErr("server_overload"), 1, bgCtx) if d2 == nil || d2.Retry { t.Error("background overload should be dropped") } } func TestAnthropicRetryPolicy_SubscriberRateLimit(t *testing.T) { p := NewAnthropicRetryPolicy(AnthropicRetryOpts{ MaxRetries: 5, IsSubscriber: func() bool { return true }, IsEnterprise: func() bool { return false }, }) err := makeErr("rate_limit") d := p.ShouldRetry(err, 1, defaultCtx()) if d == nil || d.Retry { t.Error("subscriber rate limit should not retry") } }