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) } }