package executor import ( "context" "io" "net/http" "net/http/httptest" "strings" "sync" "testing" "time" "github.com/router-for-me/CLIProxyAPI/v6/internal/config" cliproxyauth "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/auth" cliproxyexecutor "github.com/router-for-me/CLIProxyAPI/v6/sdk/cliproxy/executor" sdktranslator "github.com/router-for-me/CLIProxyAPI/v6/sdk/translator" ) func resetAntigravityCreditsRetryState() { antigravityCreditsExhaustedByAuth = sync.Map{} antigravityPreferCreditsByModel = sync.Map{} } func TestClassifyAntigravity429(t *testing.T) { t.Run("quota exhausted", func(t *testing.T) { body := []byte(`{"error":{"status":"RESOURCE_EXHAUSTED","message":"QUOTA_EXHAUSTED"}}`) if got := classifyAntigravity429(body); got != antigravity429QuotaExhausted { t.Fatalf("classifyAntigravity429() = %q, want %q", got, antigravity429QuotaExhausted) } }) t.Run("structured rate limit", func(t *testing.T) { body := []byte(`{ "error": { "status": "RESOURCE_EXHAUSTED", "details": [ {"@type": "type.googleapis.com/google.rpc.ErrorInfo", "reason": "RATE_LIMIT_EXCEEDED"}, {"@type": "type.googleapis.com/google.rpc.RetryInfo", "retryDelay": "0.5s"} ] } }`) if got := classifyAntigravity429(body); got != antigravity429RateLimited { t.Fatalf("classifyAntigravity429() = %q, want %q", got, antigravity429RateLimited) } }) t.Run("structured quota exhausted", func(t *testing.T) { body := []byte(`{ "error": { "status": "RESOURCE_EXHAUSTED", "details": [ {"@type": "type.googleapis.com/google.rpc.ErrorInfo", "reason": "QUOTA_EXHAUSTED"} ] } }`) if got := classifyAntigravity429(body); got != antigravity429QuotaExhausted { t.Fatalf("classifyAntigravity429() = %q, want %q", got, antigravity429QuotaExhausted) } }) t.Run("unknown", func(t *testing.T) { body := []byte(`{"error":{"message":"too many requests"}}`) if got := classifyAntigravity429(body); got != antigravity429Unknown { t.Fatalf("classifyAntigravity429() = %q, want %q", got, antigravity429Unknown) } }) } func TestInjectEnabledCreditTypes(t *testing.T) { body := []byte(`{"model":"gemini-2.5-flash","request":{}}`) got := injectEnabledCreditTypes(body) if got == nil { t.Fatal("injectEnabledCreditTypes() returned nil") } if !strings.Contains(string(got), `"enabledCreditTypes":["GOOGLE_ONE_AI"]`) { t.Fatalf("injectEnabledCreditTypes() = %s, want enabledCreditTypes", string(got)) } if got := injectEnabledCreditTypes([]byte(`not json`)); got != nil { t.Fatalf("injectEnabledCreditTypes() for invalid json = %s, want nil", string(got)) } } func TestShouldMarkAntigravityCreditsExhausted(t *testing.T) { for _, body := range [][]byte{ []byte(`{"error":{"message":"Insufficient GOOGLE_ONE_AI credits"}}`), []byte(`{"error":{"message":"minimumCreditAmountForUsage requirement not met"}}`), []byte(`{"error":{"message":"Resource has been exhausted"}}`), } { if !shouldMarkAntigravityCreditsExhausted(http.StatusForbidden, body, nil) { t.Fatalf("shouldMarkAntigravityCreditsExhausted(%s) = false, want true", string(body)) } } if shouldMarkAntigravityCreditsExhausted(http.StatusServiceUnavailable, []byte(`{"error":{"message":"credits exhausted"}}`), nil) { t.Fatal("shouldMarkAntigravityCreditsExhausted() = true for 5xx, want false") } } func TestAntigravityExecute_RetriesQuotaExhaustedWithCredits(t *testing.T) { resetAntigravityCreditsRetryState() t.Cleanup(resetAntigravityCreditsRetryState) var ( mu sync.Mutex requestBodies []string ) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) _ = r.Body.Close() mu.Lock() requestBodies = append(requestBodies, string(body)) reqNum := len(requestBodies) mu.Unlock() if reqNum == 1 { w.WriteHeader(http.StatusTooManyRequests) _, _ = w.Write([]byte(`{"error":{"status":"RESOURCE_EXHAUSTED","message":"QUOTA_EXHAUSTED"}}`)) return } if !strings.Contains(string(body), `"enabledCreditTypes":["GOOGLE_ONE_AI"]`) { t.Fatalf("second request body missing enabledCreditTypes: %s", string(body)) } w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"response":{"candidates":[{"content":{"role":"model","parts":[{"text":"ok"}]}}],"usageMetadata":{"promptTokenCount":1,"candidatesTokenCount":1,"totalTokenCount":2}}}`)) })) defer server.Close() exec := NewAntigravityExecutor(&config.Config{ QuotaExceeded: config.QuotaExceeded{AntigravityCredits: true}, }) auth := &cliproxyauth.Auth{ ID: "auth-credits-ok", Attributes: map[string]string{ "base_url": server.URL, }, Metadata: map[string]any{ "access_token": "token", "project_id": "project-1", "expired": time.Now().Add(1 * time.Hour).Format(time.RFC3339), }, } resp, err := exec.Execute(context.Background(), auth, cliproxyexecutor.Request{ Model: "gemini-2.5-flash", Payload: []byte(`{"request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}}`), }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatAntigravity, }) if err != nil { t.Fatalf("Execute() error = %v", err) } if len(resp.Payload) == 0 { t.Fatal("Execute() returned empty payload") } mu.Lock() defer mu.Unlock() if len(requestBodies) != 2 { t.Fatalf("request count = %d, want 2", len(requestBodies)) } } func TestAntigravityExecute_SkipsCreditsRetryWhenAlreadyExhausted(t *testing.T) { resetAntigravityCreditsRetryState() t.Cleanup(resetAntigravityCreditsRetryState) var requestCount int server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { requestCount++ w.WriteHeader(http.StatusTooManyRequests) _, _ = w.Write([]byte(`{"error":{"status":"RESOURCE_EXHAUSTED","message":"QUOTA_EXHAUSTED"}}`)) })) defer server.Close() exec := NewAntigravityExecutor(&config.Config{ QuotaExceeded: config.QuotaExceeded{AntigravityCredits: true}, }) auth := &cliproxyauth.Auth{ ID: "auth-credits-exhausted", Attributes: map[string]string{ "base_url": server.URL, }, Metadata: map[string]any{ "access_token": "token", "project_id": "project-1", "expired": time.Now().Add(1 * time.Hour).Format(time.RFC3339), }, } markAntigravityCreditsExhausted(auth, time.Now()) _, err := exec.Execute(context.Background(), auth, cliproxyexecutor.Request{ Model: "gemini-2.5-flash", Payload: []byte(`{"request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}}`), }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatAntigravity, }) if err == nil { t.Fatal("Execute() error = nil, want 429") } sErr, ok := err.(statusErr) if !ok { t.Fatalf("Execute() error type = %T, want statusErr", err) } if got := sErr.StatusCode(); got != http.StatusTooManyRequests { t.Fatalf("Execute() status code = %d, want %d", got, http.StatusTooManyRequests) } if requestCount != 1 { t.Fatalf("request count = %d, want 1", requestCount) } } func TestAntigravityExecute_PrefersCreditsAfterSuccessfulFallback(t *testing.T) { resetAntigravityCreditsRetryState() t.Cleanup(resetAntigravityCreditsRetryState) var ( mu sync.Mutex requestBodies []string ) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) _ = r.Body.Close() mu.Lock() requestBodies = append(requestBodies, string(body)) reqNum := len(requestBodies) mu.Unlock() switch reqNum { case 1: w.WriteHeader(http.StatusTooManyRequests) _, _ = w.Write([]byte(`{"error":{"status":"RESOURCE_EXHAUSTED","details":[{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"QUOTA_EXHAUSTED"},{"@type":"type.googleapis.com/google.rpc.RetryInfo","retryDelay":"10s"}]}}`)) case 2, 3: if !strings.Contains(string(body), `"enabledCreditTypes":["GOOGLE_ONE_AI"]`) { t.Fatalf("request %d body missing enabledCreditTypes: %s", reqNum, string(body)) } w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"response":{"candidates":[{"content":{"role":"model","parts":[{"text":"OK"}]}}],"usageMetadata":{"promptTokenCount":1,"candidatesTokenCount":1,"totalTokenCount":2}}}`)) default: t.Fatalf("unexpected request count %d", reqNum) } })) defer server.Close() exec := NewAntigravityExecutor(&config.Config{ QuotaExceeded: config.QuotaExceeded{AntigravityCredits: true}, }) auth := &cliproxyauth.Auth{ ID: "auth-prefer-credits", Attributes: map[string]string{ "base_url": server.URL, }, Metadata: map[string]any{ "access_token": "token", "project_id": "project-1", "expired": time.Now().Add(1 * time.Hour).Format(time.RFC3339), }, } request := cliproxyexecutor.Request{ Model: "gemini-2.5-flash", Payload: []byte(`{"request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}}`), } opts := cliproxyexecutor.Options{SourceFormat: sdktranslator.FormatAntigravity} if _, err := exec.Execute(context.Background(), auth, request, opts); err != nil { t.Fatalf("first Execute() error = %v", err) } if _, err := exec.Execute(context.Background(), auth, request, opts); err != nil { t.Fatalf("second Execute() error = %v", err) } mu.Lock() defer mu.Unlock() if len(requestBodies) != 3 { t.Fatalf("request count = %d, want 3", len(requestBodies)) } if strings.Contains(requestBodies[0], `"enabledCreditTypes":["GOOGLE_ONE_AI"]`) { t.Fatalf("first request unexpectedly used credits: %s", requestBodies[0]) } if !strings.Contains(requestBodies[1], `"enabledCreditTypes":["GOOGLE_ONE_AI"]`) { t.Fatalf("fallback request missing credits: %s", requestBodies[1]) } if !strings.Contains(requestBodies[2], `"enabledCreditTypes":["GOOGLE_ONE_AI"]`) { t.Fatalf("preferred request missing credits: %s", requestBodies[2]) } } func TestAntigravityExecute_PreservesBaseURLFallbackAfterCreditsRetryFailure(t *testing.T) { resetAntigravityCreditsRetryState() t.Cleanup(resetAntigravityCreditsRetryState) var ( mu sync.Mutex firstCount int secondCount int ) firstServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) _ = r.Body.Close() mu.Lock() firstCount++ reqNum := firstCount mu.Unlock() switch reqNum { case 1: w.WriteHeader(http.StatusTooManyRequests) _, _ = w.Write([]byte(`{"error":{"status":"RESOURCE_EXHAUSTED","details":[{"@type":"type.googleapis.com/google.rpc.ErrorInfo","reason":"QUOTA_EXHAUSTED"}]}}`)) case 2: if !strings.Contains(string(body), `"enabledCreditTypes":["GOOGLE_ONE_AI"]`) { t.Fatalf("credits retry missing enabledCreditTypes: %s", string(body)) } w.WriteHeader(http.StatusForbidden) _, _ = w.Write([]byte(`{"error":{"message":"permission denied"}}`)) default: t.Fatalf("unexpected first server request count %d", reqNum) } })) defer firstServer.Close() secondServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mu.Lock() secondCount++ mu.Unlock() w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"response":{"candidates":[{"content":{"role":"model","parts":[{"text":"ok"}]}}],"usageMetadata":{"promptTokenCount":1,"candidatesTokenCount":1,"totalTokenCount":2}}}`)) })) defer secondServer.Close() exec := NewAntigravityExecutor(&config.Config{ QuotaExceeded: config.QuotaExceeded{AntigravityCredits: true}, }) auth := &cliproxyauth.Auth{ ID: "auth-baseurl-fallback", Attributes: map[string]string{ "base_url": firstServer.URL, }, Metadata: map[string]any{ "access_token": "token", "project_id": "project-1", "expired": time.Now().Add(1 * time.Hour).Format(time.RFC3339), }, } originalOrder := antigravityBaseURLFallbackOrder defer func() { antigravityBaseURLFallbackOrder = originalOrder }() antigravityBaseURLFallbackOrder = func(auth *cliproxyauth.Auth) []string { return []string{firstServer.URL, secondServer.URL} } resp, err := exec.Execute(context.Background(), auth, cliproxyexecutor.Request{ Model: "gemini-2.5-flash", Payload: []byte(`{"request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}}`), }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatAntigravity, }) if err != nil { t.Fatalf("Execute() error = %v", err) } if len(resp.Payload) == 0 { t.Fatal("Execute() returned empty payload") } if firstCount != 2 { t.Fatalf("first server request count = %d, want 2", firstCount) } if secondCount != 1 { t.Fatalf("second server request count = %d, want 1", secondCount) } } func TestAntigravityExecute_DoesNotDirectInjectCreditsWhenFlagDisabled(t *testing.T) { resetAntigravityCreditsRetryState() t.Cleanup(resetAntigravityCreditsRetryState) var requestBodies []string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) _ = r.Body.Close() requestBodies = append(requestBodies, string(body)) w.WriteHeader(http.StatusTooManyRequests) _, _ = w.Write([]byte(`{"error":{"status":"RESOURCE_EXHAUSTED","message":"QUOTA_EXHAUSTED"}}`)) })) defer server.Close() exec := NewAntigravityExecutor(&config.Config{ QuotaExceeded: config.QuotaExceeded{AntigravityCredits: false}, }) auth := &cliproxyauth.Auth{ ID: "auth-flag-disabled", Attributes: map[string]string{ "base_url": server.URL, }, Metadata: map[string]any{ "access_token": "token", "project_id": "project-1", "expired": time.Now().Add(1 * time.Hour).Format(time.RFC3339), }, } markAntigravityPreferCredits(auth, "gemini-2.5-flash", time.Now(), nil) _, err := exec.Execute(context.Background(), auth, cliproxyexecutor.Request{ Model: "gemini-2.5-flash", Payload: []byte(`{"request":{"contents":[{"role":"user","parts":[{"text":"hi"}]}]}}`), }, cliproxyexecutor.Options{ SourceFormat: sdktranslator.FormatAntigravity, }) if err == nil { t.Fatal("Execute() error = nil, want 429") } if len(requestBodies) != 1 { t.Fatalf("request count = %d, want 1", len(requestBodies)) } if strings.Contains(requestBodies[0], `"enabledCreditTypes":["GOOGLE_ONE_AI"]`) { t.Fatalf("request unexpectedly used enabledCreditTypes with flag disabled: %s", requestBodies[0]) } }