refactor(antigravity): unify TestConnection with dispatch retry loop

TestConnection now reuses antigravityRetryLoop instead of a standalone
HTTP loop, gaining credits overages, smart retry, and 429/503 backoff
for free. AccountSwitchError is caught and surfaced as a friendly
message. Also populates RateLimitedModel in TempUnscheduled switch error.

Test fixes:
- Use RATE_LIMIT_EXCEEDED in 503 short-delay test to avoid 60x1s timeout
- Clamp waitDuration=0 instead of 999s to avoid 15s max-wait timeout
- Enhance mockSmartRetryUpstream with repeatLast and body caching
This commit is contained in:
erio
2026-03-17 01:47:08 +08:00
parent f42c8f2abe
commit a6f99cf534
3 changed files with 115 additions and 77 deletions

View File

@@ -32,11 +32,13 @@ func (c *stubSmartRetryCache) DeleteSessionAccountID(_ context.Context, groupID
// mockSmartRetryUpstream 用于 handleSmartRetry 测试的 mock upstream
type mockSmartRetryUpstream struct {
responses []*http.Response
errors []error
callIdx int
calls []string
requestBodies [][]byte
responses []*http.Response
responseBodies [][]byte // 缓存的 response body 字节(用于 repeatLast 重建)
errors []error
callIdx int
calls []string
requestBodies [][]byte
repeatLast bool // 超出范围时重复最后一个响应
}
func (m *mockSmartRetryUpstream) Do(req *http.Request, proxyURL string, accountID int64, accountConcurrency int) (*http.Response, error) {
@@ -50,10 +52,45 @@ func (m *mockSmartRetryUpstream) Do(req *http.Request, proxyURL string, accountI
m.requestBodies = append(m.requestBodies, nil)
}
m.callIdx++
if idx < len(m.responses) {
return m.responses[idx], m.errors[idx]
// 确定使用哪个索引
respIdx := idx
if respIdx >= len(m.responses) {
if !m.repeatLast || len(m.responses) == 0 {
return nil, nil
}
respIdx = len(m.responses) - 1
}
return nil, nil
resp := m.responses[respIdx]
respErr := m.errors[respIdx]
if resp == nil {
return nil, respErr
}
// 首次调用时缓存 body 字节
if respIdx >= len(m.responseBodies) {
for len(m.responseBodies) <= respIdx {
m.responseBodies = append(m.responseBodies, nil)
}
}
if m.responseBodies[respIdx] == nil && resp.Body != nil {
bodyBytes, _ := io.ReadAll(resp.Body)
_ = resp.Body.Close()
m.responseBodies[respIdx] = bodyBytes
}
// 用缓存的 body 字节重建新的 reader
var body io.ReadCloser
if m.responseBodies[respIdx] != nil {
body = io.NopCloser(bytes.NewReader(m.responseBodies[respIdx]))
}
return &http.Response{
StatusCode: resp.StatusCode,
Header: resp.Header.Clone(),
Body: body,
}, respErr
}
func (m *mockSmartRetryUpstream) DoWithTLS(req *http.Request, proxyURL string, accountID int64, accountConcurrency int, enableTLSFingerprint bool) (*http.Response, error) {