mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-14 12:02:27 +08:00
147 lines
8.0 KiB
Go
147 lines
8.0 KiB
Go
//go:build unit
|
|
|
|
package service
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/Wei-Shaw/sub2api/internal/config"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestClassifyClineErrorRequiresStructuredFields(t *testing.T) {
|
|
for name, body := range map[string]string{
|
|
"code in model message": `{"error":{"code":"invalid_request_error","message":"Model INFERENCE_CAP_ERROR not found"}}`,
|
|
"code suffix": `{"error":{"code":"model-SPEND_LIMIT_EXCEEDED","message":"Invalid model"}}`,
|
|
"credits message without code": `{"error":{"message":"Insufficient credits"}}`,
|
|
"spend message without code": `{"error":{"message":"SPEND_LIMIT_EXCEEDED"}}`,
|
|
"metadata code": `{"error":{"code":429,"message":"Rate limit exceeded","metadata":{"code":"SPEND_LIMIT_EXCEEDED"}}}`,
|
|
"metadata pass message": `{"error":{"message":"Invalid model","metadata":{"message":"You have reached your monthly ClinePass limit. Please try again later."}}}`,
|
|
"metadata free message": `{"error":{"message":"Invalid model","metadata":{"message":"Free limit reached on model x. Try again in 3h 20m"}}}`,
|
|
"root request code": `{"error":{"message":"Invalid model"},"request":{"model":"insufficient_credits"}}`,
|
|
"root message": `{"message":"You have reached your monthly ClinePass limit. Please try again later."}`,
|
|
"error string": `{"error":"SPEND_LIMIT_EXCEEDED"}`,
|
|
"plain code": `Model SPEND_LIMIT_EXCEEDED not found`,
|
|
"plain pass message": `You have reached your weekly ClinePass limit. Please try again later.`,
|
|
"malformed json": `{"error":{"code":"insufficient_credits"}`,
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
kind, _ := parseClineError([]byte(body))
|
|
require.Equal(t, clineErrorNone, kind)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHandleUpstreamError_Cline400DoesNotCoolWallet(t *testing.T) {
|
|
for name, body := range map[string]string{
|
|
"credits model echo": `{"error":{"code":"invalid_request_error","message":"Model insufficient_credits not found"}}`,
|
|
"spend model echo": `{"error":{"message":"Model SPEND_LIMIT_EXCEEDED not found"}}`,
|
|
"pass model echo": `{"error":{"message":"Model cline-pass/INFERENCE_CAP_ERROR not found"}}`,
|
|
"metadata echo": `{"error":{"code":400,"message":"Invalid model","metadata":{"model":"SPEND_LIMIT_EXCEEDED"}}}`,
|
|
"plain echo": `Model SPEND_LIMIT_EXCEEDED not found`,
|
|
"business code": clineInferenceCapBody,
|
|
"pass message": clinePassMonthlyBody,
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
account := clineTestAccount(400)
|
|
repo := &commandCodeRateLimitRepo{}
|
|
shouldDisable := NewRateLimitService(repo, nil, &config.Config{}, nil, nil).HandleUpstreamError(
|
|
context.Background(), account, http.StatusBadRequest, http.Header{}, []byte(body), "cline-pass/INFERENCE_CAP_ERROR")
|
|
|
|
require.False(t, shouldDisable)
|
|
require.Empty(t, repo.modelLimits)
|
|
require.Zero(t, repo.setErrorCalls)
|
|
require.Zero(t, repo.tempCalls)
|
|
require.False(t, account.isModelRateLimitedWithContext(context.Background(), "cline-pass/glm-5.3"))
|
|
require.False(t, account.isModelRateLimitedWithContext(context.Background(), "anthropic/claude-sonnet-4-6"))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHandleUpstreamError_Cline429EchoedKeywordsDoNotCoolWallet(t *testing.T) {
|
|
for name, body := range map[string]string{
|
|
"message without code": `{"error":{"message":"Model INFERENCE_CAP_ERROR not found"}}`,
|
|
"metadata model": `{"error":{"code":429,"message":"Rate limit exceeded","metadata":{"model":"SPEND_LIMIT_EXCEEDED"}}}`,
|
|
"request model": `{"error":{"code":429,"message":"Rate limit exceeded"},"request":{"model":"insufficient_credits"}}`,
|
|
"plain text": `Model SPEND_LIMIT_EXCEEDED not found`,
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
repo := &commandCodeRateLimitRepo{}
|
|
NewRateLimitService(repo, nil, &config.Config{}, nil, nil).HandleUpstreamError(context.Background(),
|
|
clineTestAccount(429), http.StatusTooManyRequests, http.Header{}, []byte(body), "deepseek/deepseek-v4-flash")
|
|
require.Empty(t, repo.modelLimits)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHandleUpstreamError_ClineStructuredErrorsPreserveCooldown(t *testing.T) {
|
|
for _, tc := range []struct {
|
|
name, model, body, scope, reason string
|
|
status int
|
|
wait time.Duration
|
|
}{
|
|
{
|
|
name: "credits code before echoed metadata", model: "deepseek/deepseek-v4-flash",
|
|
body: `{"error":{"code":"insufficient_credits","message":"Insufficient credits","metadata":{"model":"SPEND_LIMIT_EXCEEDED"}}}`,
|
|
scope: clineCreditsRateLimitKey, reason: clineCreditsReason, status: http.StatusPaymentRequired, wait: cnBalanceCheckCooldown(nil),
|
|
},
|
|
{
|
|
name: "spend code before pass wording", model: "deepseek/deepseek-v4-flash",
|
|
body: `{"error":{"code":"SPEND_LIMIT_EXCEEDED","message":"You have reached your monthly ClinePass limit. Please try again later."}}`,
|
|
scope: clineCreditsRateLimitKey, reason: clineSpendLimitReason, status: http.StatusTooManyRequests, wait: clineLimitRecheck,
|
|
},
|
|
{
|
|
name: "details spend code", model: "deepseek/deepseek-v4-flash",
|
|
body: `{"error":{"code":429,"message":"Rate limit exceeded","details":{"code":"SPEND_LIMIT_EXCEEDED"}}}`,
|
|
scope: clineCreditsRateLimitKey, reason: clineSpendLimitReason, status: http.StatusTooManyRequests, wait: clineLimitRecheck,
|
|
},
|
|
{
|
|
name: "inference code", model: "cline-pass/glm-5.3", body: clineInferenceCapBody,
|
|
scope: clinePassRateLimitKey, reason: clineInferenceCapReason, status: http.StatusTooManyRequests, wait: clineLimitRecheck,
|
|
},
|
|
{
|
|
name: "pass message ignores metadata window", model: "cline-pass/glm-5.3",
|
|
body: `{"error":{"code":429,"metadata":{"message":"You have reached your monthly ClinePass limit. Please try again later."},"message":"You have reached your 5-hour ClinePass limit. Please try again later."}}`,
|
|
scope: clinePassRateLimitKey, reason: clinePassLimitReason, status: http.StatusTooManyRequests, wait: clineLimitRecheck,
|
|
},
|
|
{
|
|
name: "details pass message", model: "cline-pass/glm-5.3",
|
|
body: `{"error":{"code":429,"message":"Rate limit exceeded","details":{"message":"You have reached your monthly ClinePass limit. Please try again later."}}}`,
|
|
scope: clinePassRateLimitKey, reason: clinePassLimitReason, status: http.StatusTooManyRequests, wait: clinePassMonthlyRecheck,
|
|
},
|
|
{
|
|
name: "details entitlement message", model: "cline-pass/glm-5.3",
|
|
body: `{"error":{"code":403,"message":"Forbidden","details":{"message":"The user is not subscribed to required model plan"}}}`,
|
|
scope: clinePassRateLimitKey, reason: clinePassUnavailableReason, status: http.StatusForbidden, wait: clineLimitRecheck,
|
|
},
|
|
{
|
|
name: "free message ignores metadata retry", model: "cline-free/kimi-k3",
|
|
body: `{"error":{"code":429,"metadata":{"message":"Try again in 20h"},"message":"Free limit reached on model cline-free/kimi-k3. Try again in 5m"}}`,
|
|
scope: "cline-free/kimi-k3", reason: clineFreeModelLimitReason, status: http.StatusTooManyRequests, wait: 5 * time.Minute,
|
|
},
|
|
{
|
|
name: "details free message", model: "cline-free/kimi-k3",
|
|
body: `{"error":{"code":429,"message":"Rate limit exceeded","details":{"message":"Free limit reached on model cline-free/kimi-k3. Try again in 3h 20m"}}}`,
|
|
scope: "cline-free/kimi-k3", reason: clineFreeModelLimitReason, status: http.StatusTooManyRequests, wait: 3*time.Hour + 20*time.Minute,
|
|
},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
repo := &commandCodeRateLimitRepo{}
|
|
account := clineTestAccount(430)
|
|
shouldDisable := NewRateLimitService(repo, nil, &config.Config{}, nil, nil).HandleUpstreamError(
|
|
context.Background(), account, tc.status, http.Header{}, []byte(tc.body), tc.model)
|
|
|
|
require.False(t, shouldDisable)
|
|
require.Len(t, repo.modelLimits, 1)
|
|
require.WithinDuration(t, time.Now().Add(tc.wait), repo.modelLimits[tc.scope], time.Second)
|
|
require.True(t, strings.HasPrefix(repo.reasons[tc.scope], tc.reason), repo.reasons[tc.scope])
|
|
require.Zero(t, repo.setErrorCalls)
|
|
require.Zero(t, repo.tempCalls)
|
|
})
|
|
}
|
|
}
|