| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339 |
- package service
- import (
- "net/http"
- "strconv"
- "testing"
- "time"
- )
- func TestCalculateAnthropic429ResetTime_Only5hExceeded(t *testing.T) {
- headers := http.Header{}
- headers.Set("anthropic-ratelimit-unified-5h-utilization", "1.02")
- headers.Set("anthropic-ratelimit-unified-5h-reset", "1770998400")
- headers.Set("anthropic-ratelimit-unified-7d-utilization", "0.32")
- headers.Set("anthropic-ratelimit-unified-7d-reset", "1771549200")
- result := calculateAnthropic429ResetTime(headers)
- assertAnthropicResult(t, result, 1770998400)
- if result.fiveHourReset == nil || !result.fiveHourReset.Equal(time.Unix(1770998400, 0)) {
- t.Errorf("expected fiveHourReset=1770998400, got %v", result.fiveHourReset)
- }
- }
- func TestCalculateAnthropic429ResetTime_Only7dExceeded(t *testing.T) {
- headers := http.Header{}
- headers.Set("anthropic-ratelimit-unified-5h-utilization", "0.50")
- headers.Set("anthropic-ratelimit-unified-5h-reset", "1770998400")
- headers.Set("anthropic-ratelimit-unified-7d-utilization", "1.05")
- headers.Set("anthropic-ratelimit-unified-7d-reset", "1771549200")
- result := calculateAnthropic429ResetTime(headers)
- assertAnthropicResult(t, result, 1771549200)
- // fiveHourReset should still be populated for session window calculation
- if result.fiveHourReset == nil || !result.fiveHourReset.Equal(time.Unix(1770998400, 0)) {
- t.Errorf("expected fiveHourReset=1770998400, got %v", result.fiveHourReset)
- }
- }
- func TestCalculateAnthropic429ResetTime_BothExceeded(t *testing.T) {
- headers := http.Header{}
- headers.Set("anthropic-ratelimit-unified-5h-utilization", "1.10")
- headers.Set("anthropic-ratelimit-unified-5h-reset", "1770998400")
- headers.Set("anthropic-ratelimit-unified-7d-utilization", "1.02")
- headers.Set("anthropic-ratelimit-unified-7d-reset", "1771549200")
- result := calculateAnthropic429ResetTime(headers)
- assertAnthropicResult(t, result, 1771549200)
- }
- func TestCalculateAnthropic429ResetTime_NoPerWindowHeaders(t *testing.T) {
- headers := http.Header{}
- headers.Set("anthropic-ratelimit-unified-reset", "1771549200")
- result := calculateAnthropic429ResetTime(headers)
- if result != nil {
- t.Errorf("expected nil result when no per-window headers, got resetAt=%v", result.resetAt)
- }
- }
- func TestCalculateAnthropic429ResetTime_NoHeaders(t *testing.T) {
- result := calculateAnthropic429ResetTime(http.Header{})
- if result != nil {
- t.Errorf("expected nil result for empty headers, got resetAt=%v", result.resetAt)
- }
- }
- func TestCalculateAnthropic429ResetTime_SurpassedThreshold(t *testing.T) {
- headers := http.Header{}
- headers.Set("anthropic-ratelimit-unified-5h-surpassed-threshold", "true")
- headers.Set("anthropic-ratelimit-unified-5h-reset", "1770998400")
- headers.Set("anthropic-ratelimit-unified-7d-surpassed-threshold", "false")
- headers.Set("anthropic-ratelimit-unified-7d-reset", "1771549200")
- result := calculateAnthropic429ResetTime(headers)
- assertAnthropicResult(t, result, 1770998400)
- }
- func TestCalculateAnthropic429ResetTime_UtilizationExactlyOne(t *testing.T) {
- headers := http.Header{}
- headers.Set("anthropic-ratelimit-unified-5h-utilization", "1.0")
- headers.Set("anthropic-ratelimit-unified-5h-reset", "1770998400")
- headers.Set("anthropic-ratelimit-unified-7d-utilization", "0.5")
- headers.Set("anthropic-ratelimit-unified-7d-reset", "1771549200")
- result := calculateAnthropic429ResetTime(headers)
- assertAnthropicResult(t, result, 1770998400)
- }
- func TestCalculateAnthropic429ResetTime_NeitherExceeded_UsesShorter(t *testing.T) {
- headers := http.Header{}
- headers.Set("anthropic-ratelimit-unified-5h-utilization", "0.95")
- headers.Set("anthropic-ratelimit-unified-5h-reset", "1770998400") // sooner
- headers.Set("anthropic-ratelimit-unified-7d-utilization", "0.80")
- headers.Set("anthropic-ratelimit-unified-7d-reset", "1771549200") // later
- result := calculateAnthropic429ResetTime(headers)
- assertAnthropicResult(t, result, 1770998400)
- }
- func TestCalculateAnthropic429ResetTime_Only5hResetHeader(t *testing.T) {
- headers := http.Header{}
- headers.Set("anthropic-ratelimit-unified-5h-utilization", "1.05")
- headers.Set("anthropic-ratelimit-unified-5h-reset", "1770998400")
- result := calculateAnthropic429ResetTime(headers)
- assertAnthropicResult(t, result, 1770998400)
- }
- func TestCalculateAnthropic429ResetTime_Only7dResetHeader(t *testing.T) {
- headers := http.Header{}
- headers.Set("anthropic-ratelimit-unified-7d-utilization", "1.03")
- headers.Set("anthropic-ratelimit-unified-7d-reset", "1771549200")
- result := calculateAnthropic429ResetTime(headers)
- assertAnthropicResult(t, result, 1771549200)
- if result.fiveHourReset != nil {
- t.Errorf("expected fiveHourReset=nil when no 5h headers, got %v", result.fiveHourReset)
- }
- }
- func TestIsAnthropicWindowExceeded(t *testing.T) {
- tests := []struct {
- name string
- headers http.Header
- window string
- expected bool
- }{
- {
- name: "utilization above 1.0",
- headers: makeHeader("anthropic-ratelimit-unified-5h-utilization", "1.02"),
- window: "5h",
- expected: true,
- },
- {
- name: "utilization exactly 1.0",
- headers: makeHeader("anthropic-ratelimit-unified-5h-utilization", "1.0"),
- window: "5h",
- expected: true,
- },
- {
- name: "utilization below 1.0",
- headers: makeHeader("anthropic-ratelimit-unified-5h-utilization", "0.99"),
- window: "5h",
- expected: false,
- },
- {
- name: "surpassed-threshold true",
- headers: makeHeader("anthropic-ratelimit-unified-7d-surpassed-threshold", "true"),
- window: "7d",
- expected: true,
- },
- {
- name: "surpassed-threshold True (case insensitive)",
- headers: makeHeader("anthropic-ratelimit-unified-7d-surpassed-threshold", "True"),
- window: "7d",
- expected: true,
- },
- {
- name: "surpassed-threshold false",
- headers: makeHeader("anthropic-ratelimit-unified-7d-surpassed-threshold", "false"),
- window: "7d",
- expected: false,
- },
- {
- name: "no headers",
- headers: http.Header{},
- window: "5h",
- expected: false,
- },
- }
- for _, tc := range tests {
- t.Run(tc.name, func(t *testing.T) {
- got := isAnthropicWindowExceeded(tc.headers, tc.window)
- if got != tc.expected {
- t.Errorf("expected %v, got %v", tc.expected, got)
- }
- })
- }
- }
- func TestSelectAnthropicFableWindowLimit_RejectedStatus(t *testing.T) {
- now := time.Now()
- reset := now.Add(80 * time.Hour).Truncate(time.Second)
- headers := http.Header{}
- headers.Set("anthropic-ratelimit-unified-7d_oi-status", "rejected")
- headers.Set("anthropic-ratelimit-unified-7d_oi-utilization", "1.0")
- headers.Set("anthropic-ratelimit-unified-7d_oi-surpassed-threshold", "1.0")
- headers.Set("anthropic-ratelimit-unified-7d_oi-reset", strconv.FormatInt(reset.Unix(), 10))
- limit := selectAnthropicFableWindowLimit(headers, now)
- if limit == nil {
- t.Fatal("expected non-nil limit")
- }
- if !limit.resetAt.Equal(reset) {
- t.Errorf("expected resetAt=%v, got %v", reset, limit.resetAt)
- }
- if limit.reason != anthropicFableWindowReason {
- t.Errorf("expected reason=%q, got %q", anthropicFableWindowReason, limit.reason)
- }
- }
- func TestSelectAnthropicFableWindowLimit_UtilizationOnly(t *testing.T) {
- // 无 status 头时,utilization >= 1.0 也应视为超限
- now := time.Now()
- reset := now.Add(3 * 24 * time.Hour).Truncate(time.Second)
- headers := http.Header{}
- headers.Set("anthropic-ratelimit-unified-7d_oi-utilization", "1.0")
- headers.Set("anthropic-ratelimit-unified-7d_oi-reset", strconv.FormatInt(reset.Unix(), 10))
- limit := selectAnthropicFableWindowLimit(headers, now)
- if limit == nil {
- t.Fatal("expected non-nil limit")
- }
- if !limit.resetAt.Equal(reset) {
- t.Errorf("expected resetAt=%v, got %v", reset, limit.resetAt)
- }
- }
- func TestSelectAnthropicFableWindowLimit_AllowedReturnsNil(t *testing.T) {
- now := time.Now()
- headers := http.Header{}
- headers.Set("anthropic-ratelimit-unified-7d_oi-status", "allowed")
- headers.Set("anthropic-ratelimit-unified-7d_oi-utilization", "0.56")
- headers.Set("anthropic-ratelimit-unified-7d_oi-reset", strconv.FormatInt(now.Add(80*time.Hour).Unix(), 10))
- if limit := selectAnthropicFableWindowLimit(headers, now); limit != nil {
- t.Errorf("expected nil limit for allowed window, got %+v", limit)
- }
- }
- func TestSelectAnthropicFableWindowLimit_NoHeadersReturnsNil(t *testing.T) {
- if limit := selectAnthropicFableWindowLimit(http.Header{}, time.Now()); limit != nil {
- t.Errorf("expected nil limit for empty headers, got %+v", limit)
- }
- }
- func TestSelectAnthropicFableWindowLimit_FallsBackToAggregateReset(t *testing.T) {
- // 7d_oi-reset 缺失时回退聚合 anthropic-ratelimit-unified-reset
- now := time.Now()
- reset := now.Add(80 * time.Hour).Truncate(time.Second)
- headers := http.Header{}
- headers.Set("anthropic-ratelimit-unified-7d_oi-status", "rejected")
- headers.Set("anthropic-ratelimit-unified-reset", strconv.FormatInt(reset.Unix(), 10))
- limit := selectAnthropicFableWindowLimit(headers, now)
- if limit == nil {
- t.Fatal("expected non-nil limit via aggregate reset fallback")
- }
- if !limit.resetAt.Equal(reset) {
- t.Errorf("expected resetAt=%v, got %v", reset, limit.resetAt)
- }
- }
- func TestSelectAnthropicFableWindowLimit_RejectedWithoutAnyResetReturnsNil(t *testing.T) {
- headers := http.Header{}
- headers.Set("anthropic-ratelimit-unified-7d_oi-status", "rejected")
- if limit := selectAnthropicFableWindowLimit(headers, time.Now()); limit != nil {
- t.Errorf("expected nil limit when no reset time available, got %+v", limit)
- }
- }
- func TestParseAnthropicAggregateReset(t *testing.T) {
- now := time.Now()
- future := now.Add(80 * time.Hour).Truncate(time.Second)
- tests := []struct {
- name string
- value string
- want time.Time
- wantOK bool
- }{
- {"valid seconds", strconv.FormatInt(future.Unix(), 10), future, true},
- {"valid milliseconds", strconv.FormatInt(future.UnixMilli(), 10), future, true},
- {"empty", "", time.Time{}, false},
- {"garbage", "abc", time.Time{}, false},
- {"in the past", strconv.FormatInt(now.Add(-time.Hour).Unix(), 10), time.Time{}, false},
- {"too far in the future", strconv.FormatInt(now.Add(30*24*time.Hour).Unix(), 10), time.Time{}, false},
- }
- for _, tc := range tests {
- t.Run(tc.name, func(t *testing.T) {
- headers := http.Header{}
- if tc.value != "" {
- headers.Set("anthropic-ratelimit-unified-reset", tc.value)
- }
- got, ok := parseAnthropicAggregateReset(headers, now)
- if ok != tc.wantOK {
- t.Fatalf("expected ok=%v, got %v", tc.wantOK, ok)
- }
- if ok && !got.Equal(tc.want) {
- t.Errorf("expected %v, got %v", tc.want, got)
- }
- })
- }
- }
- func TestIsAnthropicWindowRejected(t *testing.T) {
- headers := http.Header{}
- headers.Set("anthropic-ratelimit-unified-7d_oi-status", "Rejected")
- headers.Set("anthropic-ratelimit-unified-5h-status", "allowed")
- if !isAnthropicWindowRejected(headers, "7d_oi") {
- t.Error("expected 7d_oi to be rejected (case insensitive)")
- }
- if isAnthropicWindowRejected(headers, "5h") {
- t.Error("expected 5h not rejected")
- }
- if isAnthropicWindowRejected(headers, "7d") {
- t.Error("expected missing 7d status not rejected")
- }
- }
- // assertAnthropicResult is a test helper that verifies the result is non-nil and
- // has the expected resetAt unix timestamp.
- func assertAnthropicResult(t *testing.T, result *anthropic429Result, wantUnix int64) {
- t.Helper()
- if result == nil {
- t.Fatal("expected non-nil result")
- return // unreachable, but satisfies staticcheck SA5011
- }
- want := time.Unix(wantUnix, 0)
- if !result.resetAt.Equal(want) {
- t.Errorf("expected resetAt=%v, got %v", want, result.resetAt)
- }
- }
- func makeHeader(key, value string) http.Header {
- h := http.Header{}
- h.Set(key, value)
- return h
- }
|