diff options
Diffstat (limited to 'internal/executor/ratelimit_test.go')
| -rw-r--r-- | internal/executor/ratelimit_test.go | 170 |
1 files changed, 170 insertions, 0 deletions
diff --git a/internal/executor/ratelimit_test.go b/internal/executor/ratelimit_test.go new file mode 100644 index 0000000..f45216f --- /dev/null +++ b/internal/executor/ratelimit_test.go @@ -0,0 +1,170 @@ +package executor + +import ( + "context" + "errors" + "fmt" + "testing" + "time" +) + +// --- isRateLimitError tests --- + +func TestIsRateLimitError_RateLimitMessage(t *testing.T) { + err := errors.New("claude exited with error: rate limit exceeded") + if !isRateLimitError(err) { + t.Error("want true for 'rate limit exceeded', got false") + } +} + +func TestIsRateLimitError_TooManyRequests(t *testing.T) { + err := errors.New("too many requests to the API") + if !isRateLimitError(err) { + t.Error("want true for 'too many requests', got false") + } +} + +func TestIsRateLimitError_HTTP429(t *testing.T) { + err := errors.New("API returned status 429") + if !isRateLimitError(err) { + t.Error("want true for '429', got false") + } +} + +func TestIsRateLimitError_Overloaded(t *testing.T) { + err := errors.New("API overloaded, please retry later") + if !isRateLimitError(err) { + t.Error("want true for 'overloaded', got false") + } +} + +func TestIsRateLimitError_NonRateLimitError(t *testing.T) { + err := errors.New("claude exited with error: exit status 1") + if isRateLimitError(err) { + t.Error("want false for non-rate-limit error, got true") + } +} + +func TestIsRateLimitError_NilError(t *testing.T) { + if isRateLimitError(nil) { + t.Error("want false for nil error, got true") + } +} + +// --- parseRetryAfter tests --- + +func TestParseRetryAfter_RetryAfterSeconds(t *testing.T) { + msg := "rate limit exceeded, retry after 30 seconds" + d := parseRetryAfter(msg) + if d != 30*time.Second { + t.Errorf("want 30s, got %v", d) + } +} + +func TestParseRetryAfter_RetryAfterHeader(t *testing.T) { + msg := "rate_limit_error: retry-after: 60" + d := parseRetryAfter(msg) + if d != 60*time.Second { + t.Errorf("want 60s, got %v", d) + } +} + +func TestParseRetryAfter_NoRetryInfo(t *testing.T) { + msg := "rate limit exceeded" + d := parseRetryAfter(msg) + if d != 0 { + t.Errorf("want 0, got %v", d) + } +} + +// --- runWithBackoff tests --- + +func TestRunWithBackoff_SuccessOnFirstTry(t *testing.T) { + calls := 0 + fn := func() error { + calls++ + return nil + } + err := runWithBackoff(context.Background(), 3, time.Millisecond, fn) + if err != nil { + t.Errorf("want nil error, got %v", err) + } + if calls != 1 { + t.Errorf("want 1 call, got %d", calls) + } +} + +func TestRunWithBackoff_RetriesOnRateLimit(t *testing.T) { + calls := 0 + fn := func() error { + calls++ + if calls < 3 { + return fmt.Errorf("rate limit exceeded") + } + return nil + } + err := runWithBackoff(context.Background(), 3, time.Millisecond, fn) + if err != nil { + t.Errorf("want nil error, got %v", err) + } + if calls != 3 { + t.Errorf("want 3 calls, got %d", calls) + } +} + +func TestRunWithBackoff_GivesUpAfterMaxRetries(t *testing.T) { + calls := 0 + rateLimitErr := fmt.Errorf("rate limit exceeded") + fn := func() error { + calls++ + return rateLimitErr + } + err := runWithBackoff(context.Background(), 3, time.Millisecond, fn) + if err == nil { + t.Fatal("want error after max retries, got nil") + } + // maxRetries=3: 1 initial call + 3 retries = 4 total calls + if calls != 4 { + t.Errorf("want 4 calls (1 initial + 3 retries), got %d", calls) + } +} + +func TestRunWithBackoff_DoesNotRetryNonRateLimitError(t *testing.T) { + calls := 0 + fn := func() error { + calls++ + return fmt.Errorf("permission denied") + } + err := runWithBackoff(context.Background(), 3, time.Millisecond, fn) + if err == nil { + t.Fatal("want error, got nil") + } + if calls != 1 { + t.Errorf("want 1 call (no retry for non-rate-limit), got %d", calls) + } +} + +func TestRunWithBackoff_ContextCancellation(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + calls := 0 + + fn := func() error { + calls++ + cancel() // cancel immediately after first call + return fmt.Errorf("rate limit exceeded") + } + + start := time.Now() + err := runWithBackoff(ctx, 3, time.Second, fn) // large delay confirms ctx preempts wait + elapsed := time.Since(start) + + if err == nil { + t.Fatal("want error on context cancellation, got nil") + } + if elapsed > 500*time.Millisecond { + t.Errorf("context cancellation too slow: %v (want < 500ms)", elapsed) + } + if calls != 1 { + t.Errorf("want 1 call before cancellation, got %d", calls) + } +} |
