From a99522224f670d5db3de5c05c2661574cbca6d58 Mon Sep 17 00:00:00 2001 From: lyd123qw2008 <326643467@qq.com> Date: Sat, 21 Feb 2026 14:13:38 +0800 Subject: [PATCH] refactor(codex): make retry-after parsing deterministic for tests --- internal/runtime/executor/codex_executor.go | 5 +- .../executor/codex_executor_retry_test.go | 89 ++++++++++--------- 2 files changed, 50 insertions(+), 44 deletions(-) diff --git a/internal/runtime/executor/codex_executor.go b/internal/runtime/executor/codex_executor.go index 34dcad56..a0cbc0d5 100644 --- a/internal/runtime/executor/codex_executor.go +++ b/internal/runtime/executor/codex_executor.go @@ -675,20 +675,19 @@ func applyCodexHeaders(r *http.Request, auth *cliproxyauth.Auth, token string, s func newCodexStatusErr(statusCode int, body []byte) statusErr { err := statusErr{code: statusCode, msg: string(body)} - if retryAfter := parseCodexRetryAfter(statusCode, body); retryAfter != nil { + if retryAfter := parseCodexRetryAfter(statusCode, body, time.Now()); retryAfter != nil { err.retryAfter = retryAfter } return err } -func parseCodexRetryAfter(statusCode int, errorBody []byte) *time.Duration { +func parseCodexRetryAfter(statusCode int, errorBody []byte, now time.Time) *time.Duration { if statusCode != http.StatusTooManyRequests || len(errorBody) == 0 { return nil } if strings.TrimSpace(gjson.GetBytes(errorBody, "error.type").String()) != "usage_limit_reached" { return nil } - now := time.Now() if resetsAt := gjson.GetBytes(errorBody, "error.resets_at").Int(); resetsAt > 0 { resetAtTime := time.Unix(resetsAt, 0) if resetAtTime.After(now) { diff --git a/internal/runtime/executor/codex_executor_retry_test.go b/internal/runtime/executor/codex_executor_retry_test.go index 4a47796d..3e54ae7c 100644 --- a/internal/runtime/executor/codex_executor_retry_test.go +++ b/internal/runtime/executor/codex_executor_retry_test.go @@ -7,50 +7,57 @@ import ( "time" ) -func TestParseCodexRetryAfter_ResetsInSeconds(t *testing.T) { - body := []byte(`{"error":{"type":"usage_limit_reached","resets_in_seconds":123}}`) - retryAfter := parseCodexRetryAfter(http.StatusTooManyRequests, body) - if retryAfter == nil { - t.Fatalf("expected retryAfter, got nil") - } - if *retryAfter != 123*time.Second { - t.Fatalf("retryAfter = %v, want %v", *retryAfter, 123*time.Second) - } -} +func TestParseCodexRetryAfter(t *testing.T) { + now := time.Unix(1_700_000_000, 0) -func TestParseCodexRetryAfter_PrefersResetsAt(t *testing.T) { - resetAt := time.Now().Add(5 * time.Minute).Unix() - body := []byte(`{"error":{"type":"usage_limit_reached","resets_at":` + itoa(resetAt) + `,"resets_in_seconds":1}}`) - retryAfter := parseCodexRetryAfter(http.StatusTooManyRequests, body) - if retryAfter == nil { - t.Fatalf("expected retryAfter, got nil") - } - if *retryAfter < 4*time.Minute || *retryAfter > 6*time.Minute { - t.Fatalf("retryAfter = %v, want around 5m", *retryAfter) - } -} + t.Run("resets_in_seconds", func(t *testing.T) { + body := []byte(`{"error":{"type":"usage_limit_reached","resets_in_seconds":123}}`) + retryAfter := parseCodexRetryAfter(http.StatusTooManyRequests, body, now) + if retryAfter == nil { + t.Fatalf("expected retryAfter, got nil") + } + if *retryAfter != 123*time.Second { + t.Fatalf("retryAfter = %v, want %v", *retryAfter, 123*time.Second) + } + }) -func TestParseCodexRetryAfter_FallbackWhenResetsAtPast(t *testing.T) { - resetAt := time.Now().Add(-1 * time.Minute).Unix() - body := []byte(`{"error":{"type":"usage_limit_reached","resets_at":` + itoa(resetAt) + `,"resets_in_seconds":77}}`) - retryAfter := parseCodexRetryAfter(http.StatusTooManyRequests, body) - if retryAfter == nil { - t.Fatalf("expected retryAfter, got nil") - } - if *retryAfter != 77*time.Second { - t.Fatalf("retryAfter = %v, want %v", *retryAfter, 77*time.Second) - } -} + t.Run("prefers resets_at", func(t *testing.T) { + resetAt := now.Add(5 * time.Minute).Unix() + body := []byte(`{"error":{"type":"usage_limit_reached","resets_at":` + itoa(resetAt) + `,"resets_in_seconds":1}}`) + retryAfter := parseCodexRetryAfter(http.StatusTooManyRequests, body, now) + if retryAfter == nil { + t.Fatalf("expected retryAfter, got nil") + } + if *retryAfter != 5*time.Minute { + t.Fatalf("retryAfter = %v, want %v", *retryAfter, 5*time.Minute) + } + }) -func TestParseCodexRetryAfter_NonApplicableReturnsNil(t *testing.T) { - body := []byte(`{"error":{"type":"usage_limit_reached","resets_in_seconds":30}}`) - if got := parseCodexRetryAfter(http.StatusBadRequest, body); got != nil { - t.Fatalf("expected nil for non-429, got %v", *got) - } - body = []byte(`{"error":{"type":"server_error","resets_in_seconds":30}}`) - if got := parseCodexRetryAfter(http.StatusTooManyRequests, body); got != nil { - t.Fatalf("expected nil for non-usage_limit_reached, got %v", *got) - } + t.Run("fallback when resets_at is past", func(t *testing.T) { + resetAt := now.Add(-1 * time.Minute).Unix() + body := []byte(`{"error":{"type":"usage_limit_reached","resets_at":` + itoa(resetAt) + `,"resets_in_seconds":77}}`) + retryAfter := parseCodexRetryAfter(http.StatusTooManyRequests, body, now) + if retryAfter == nil { + t.Fatalf("expected retryAfter, got nil") + } + if *retryAfter != 77*time.Second { + t.Fatalf("retryAfter = %v, want %v", *retryAfter, 77*time.Second) + } + }) + + t.Run("non-429 status code", func(t *testing.T) { + body := []byte(`{"error":{"type":"usage_limit_reached","resets_in_seconds":30}}`) + if got := parseCodexRetryAfter(http.StatusBadRequest, body, now); got != nil { + t.Fatalf("expected nil for non-429, got %v", *got) + } + }) + + t.Run("non usage_limit_reached error type", func(t *testing.T) { + body := []byte(`{"error":{"type":"server_error","resets_in_seconds":30}}`) + if got := parseCodexRetryAfter(http.StatusTooManyRequests, body, now); got != nil { + t.Fatalf("expected nil for non-usage_limit_reached, got %v", *got) + } + }) } func itoa(v int64) string {