From 2db0cbd292a8e0ec1067d98a1e880b049fe1df82 Mon Sep 17 00:00:00 2001 From: yan9651688 <174289968+yan9651688@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:04:18 +0800 Subject: [PATCH] fix(grok): pause accounts after manual test payment failure Manual Grok connection tests previously surfaced upstream HTTP 402 errors without changing account availability. Persist the same 30-minute payment-required cooldown used by the live forwarding path so refreshed account lists no longer present the account as schedulable. Constraint: Keep manual-test HTTP 402 handling aligned with existing Grok forwarding semantics. Rejected: Mark the account permanently error | payment state can recover and the forwarding path intentionally uses a bounded cooldown. Confidence: high Scope-risk: narrow Directive: Keep the manual-test cooldown reason and duration aligned with handleGrokAccountUpstreamError. Tested: go test -tags=unit ./internal/service -count=1; go vet -tags=unit ./internal/service; production package compile check Not-tested: Live xAI account with an exhausted subscription Related: #4794 --- .../internal/service/account_test_service.go | 10 ++++++ .../service/openai_gateway_grok_test.go | 31 +++++++++++++++++++ 2 files changed, 41 insertions(+) diff --git a/backend/internal/service/account_test_service.go b/backend/internal/service/account_test_service.go index 29f6114500..3200174b2c 100644 --- a/backend/internal/service/account_test_service.go +++ b/backend/internal/service/account_test_service.go @@ -794,6 +794,16 @@ func (s *AccountTestService) testGrokAccountConnection(c *gin.Context, account * if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) + if resp.StatusCode == http.StatusPaymentRequired && s.accountRepo != nil { + stateCtx, cancel := openAIAccountStateContext(ctx) + defer cancel() + _ = s.accountRepo.SetTempUnschedulable( + stateCtx, + account.ID, + time.Now().Add(30*time.Minute), + "grok payment required", + ) + } return s.sendErrorAndEnd(c, fmt.Sprintf("Grok Responses API returned %d: %s", resp.StatusCode, string(body))) } diff --git a/backend/internal/service/openai_gateway_grok_test.go b/backend/internal/service/openai_gateway_grok_test.go index d13cfa19fc..09a0df4019 100644 --- a/backend/internal/service/openai_gateway_grok_test.go +++ b/backend/internal/service/openai_gateway_grok_test.go @@ -2006,6 +2006,37 @@ func TestAccountTestServiceGrokAPIKeyAllowsConfiguredHTTPWhenGlobalPolicyDoes(t require.Contains(t, recorder.Body.String(), `"type":"test_complete"`) } +func TestAccountTestServiceGrokOAuthPaymentRequiredTemporarilyUnschedulesAccount(t *testing.T) { + gin.SetMode(gin.TestMode) + + account := healthyGrokOAuthGatewayTestAccount(56, "access-token") + repo := &grokQuotaAccountRepo{} + upstream := &httpUpstreamRecorder{resp: &http.Response{ + StatusCode: http.StatusPaymentRequired, + Header: http.Header{"Content-Type": []string{"application/json"}}, + Body: io.NopCloser(strings.NewReader(`{"code":"personal-team-blocked:spending-limit"}`)), + }} + svc := &AccountTestService{ + accountRepo: repo, + grokTokenProvider: NewGrokTokenProvider(repo, nil), + httpUpstream: upstream, + } + recorder := httptest.NewRecorder() + c, _ := gin.CreateTestContext(recorder) + c.Request = httptest.NewRequest(http.MethodPost, "/api/v1/admin/accounts/56/test", nil) + before := time.Now() + + err := svc.testGrokAccountConnection(c, account, "grok") + + require.Error(t, err) + require.Equal(t, 1, repo.tempUnschedCalls) + require.Equal(t, account.ID, repo.lastTempUnschedID) + require.Equal(t, "grok payment required", repo.lastTempUnschedReason) + require.WithinDuration(t, before.Add(30*time.Minute), repo.lastTempUnschedUntil, time.Second) + require.Contains(t, recorder.Body.String(), `"type":"error"`) + require.Contains(t, recorder.Body.String(), "Grok Responses API returned 402") +} + func TestForwardAsChatCompletionsForGrokStreamingUsesRawXAIChatCompletions(t *testing.T) { gin.SetMode(gin.TestMode)