summaryrefslogtreecommitdiff
path: root/packages/core/src/utils/retry.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/core/src/utils/retry.test.ts')
-rw-r--r--packages/core/src/utils/retry.test.ts199
1 files changed, 199 insertions, 0 deletions
diff --git a/packages/core/src/utils/retry.test.ts b/packages/core/src/utils/retry.test.ts
index 4c269987..39f62981 100644
--- a/packages/core/src/utils/retry.test.ts
+++ b/packages/core/src/utils/retry.test.ts
@@ -7,6 +7,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { retryWithBackoff } from './retry.js';
+import { setSimulate429 } from './testUtils.js';
// Define an interface for the error with a status property
interface HttpError extends Error {
@@ -42,10 +43,15 @@ class NonRetryableError extends Error {
describe('retryWithBackoff', () => {
beforeEach(() => {
vi.useFakeTimers();
+ // Disable 429 simulation for tests
+ setSimulate429(false);
+ // Suppress unhandled promise rejection warnings for tests that expect errors
+ console.warn = vi.fn();
});
afterEach(() => {
vi.restoreAllMocks();
+ vi.useRealTimers();
});
it('should return the result on the first attempt if successful', async () => {
@@ -231,4 +237,197 @@ describe('retryWithBackoff', () => {
expect(d).toBeLessThanOrEqual(100 * 1.3);
});
});
+
+ describe('Flash model fallback for OAuth users', () => {
+ it('should trigger fallback for OAuth personal users after persistent 429 errors', async () => {
+ const fallbackCallback = vi.fn().mockResolvedValue('gemini-2.5-flash');
+
+ let fallbackOccurred = false;
+ const mockFn = vi.fn().mockImplementation(async () => {
+ if (!fallbackOccurred) {
+ const error: HttpError = new Error('Rate limit exceeded');
+ error.status = 429;
+ throw error;
+ }
+ return 'success';
+ });
+
+ const promise = retryWithBackoff(mockFn, {
+ maxAttempts: 3,
+ initialDelayMs: 100,
+ onPersistent429: async (authType?: string) => {
+ fallbackOccurred = true;
+ return await fallbackCallback(authType);
+ },
+ authType: 'oauth-personal',
+ });
+
+ // Advance all timers to complete retries
+ await vi.runAllTimersAsync();
+
+ // Should succeed after fallback
+ await expect(promise).resolves.toBe('success');
+
+ // Verify callback was called with correct auth type
+ expect(fallbackCallback).toHaveBeenCalledWith('oauth-personal');
+
+ // Should retry again after fallback
+ expect(mockFn).toHaveBeenCalledTimes(4); // 3 initial attempts + 1 after fallback
+ });
+
+ it('should trigger fallback for OAuth enterprise users after persistent 429 errors', async () => {
+ const fallbackCallback = vi.fn().mockResolvedValue('gemini-2.5-flash');
+
+ let fallbackOccurred = false;
+ const mockFn = vi.fn().mockImplementation(async () => {
+ if (!fallbackOccurred) {
+ const error: HttpError = new Error('Rate limit exceeded');
+ error.status = 429;
+ throw error;
+ }
+ return 'success';
+ });
+
+ const promise = retryWithBackoff(mockFn, {
+ maxAttempts: 3,
+ initialDelayMs: 100,
+ onPersistent429: async (authType?: string) => {
+ fallbackOccurred = true;
+ return await fallbackCallback(authType);
+ },
+ authType: 'oauth-enterprise',
+ });
+
+ await vi.runAllTimersAsync();
+
+ await expect(promise).resolves.toBe('success');
+ expect(fallbackCallback).toHaveBeenCalledWith('oauth-enterprise');
+ });
+
+ it('should NOT trigger fallback for API key users', async () => {
+ const fallbackCallback = vi.fn();
+
+ const mockFn = vi.fn(async () => {
+ const error: HttpError = new Error('Rate limit exceeded');
+ error.status = 429;
+ throw error;
+ });
+
+ const promise = retryWithBackoff(mockFn, {
+ maxAttempts: 3,
+ initialDelayMs: 100,
+ onPersistent429: fallbackCallback,
+ authType: 'gemini-api-key',
+ });
+
+ // Handle the promise properly to avoid unhandled rejections
+ const resultPromise = promise.catch((error) => error);
+ await vi.runAllTimersAsync();
+ const result = await resultPromise;
+
+ // Should fail after all retries without fallback
+ expect(result).toBeInstanceOf(Error);
+ expect(result.message).toBe('Rate limit exceeded');
+
+ // Callback should not be called for API key users
+ expect(fallbackCallback).not.toHaveBeenCalled();
+ });
+
+ it('should reset attempt counter and continue after successful fallback', async () => {
+ let fallbackCalled = false;
+ const fallbackCallback = vi.fn().mockImplementation(async () => {
+ fallbackCalled = true;
+ return 'gemini-2.5-flash';
+ });
+
+ const mockFn = vi.fn().mockImplementation(async () => {
+ if (!fallbackCalled) {
+ const error: HttpError = new Error('Rate limit exceeded');
+ error.status = 429;
+ throw error;
+ }
+ return 'success';
+ });
+
+ const promise = retryWithBackoff(mockFn, {
+ maxAttempts: 3,
+ initialDelayMs: 100,
+ onPersistent429: fallbackCallback,
+ authType: 'oauth-personal',
+ });
+
+ await vi.runAllTimersAsync();
+
+ await expect(promise).resolves.toBe('success');
+ expect(fallbackCallback).toHaveBeenCalledOnce();
+ });
+
+ it('should continue with original error if fallback is rejected', async () => {
+ const fallbackCallback = vi.fn().mockResolvedValue(null); // User rejected fallback
+
+ const mockFn = vi.fn(async () => {
+ const error: HttpError = new Error('Rate limit exceeded');
+ error.status = 429;
+ throw error;
+ });
+
+ const promise = retryWithBackoff(mockFn, {
+ maxAttempts: 3,
+ initialDelayMs: 100,
+ onPersistent429: fallbackCallback,
+ authType: 'oauth-personal',
+ });
+
+ // Handle the promise properly to avoid unhandled rejections
+ const resultPromise = promise.catch((error) => error);
+ await vi.runAllTimersAsync();
+ const result = await resultPromise;
+
+ // Should fail with original error when fallback is rejected
+ expect(result).toBeInstanceOf(Error);
+ expect(result.message).toBe('Rate limit exceeded');
+ expect(fallbackCallback).toHaveBeenCalledWith('oauth-personal');
+ });
+
+ it('should handle mixed error types (only count consecutive 429s)', async () => {
+ const fallbackCallback = vi.fn().mockResolvedValue('gemini-2.5-flash');
+ let attempts = 0;
+ let fallbackOccurred = false;
+
+ const mockFn = vi.fn().mockImplementation(async () => {
+ attempts++;
+ if (fallbackOccurred) {
+ return 'success';
+ }
+ if (attempts === 1) {
+ // First attempt: 500 error (resets consecutive count)
+ const error: HttpError = new Error('Server error');
+ error.status = 500;
+ throw error;
+ } else {
+ // Remaining attempts: 429 errors
+ const error: HttpError = new Error('Rate limit exceeded');
+ error.status = 429;
+ throw error;
+ }
+ });
+
+ const promise = retryWithBackoff(mockFn, {
+ maxAttempts: 5,
+ initialDelayMs: 100,
+ onPersistent429: async (authType?: string) => {
+ fallbackOccurred = true;
+ return await fallbackCallback(authType);
+ },
+ authType: 'oauth-personal',
+ });
+
+ await vi.runAllTimersAsync();
+
+ await expect(promise).resolves.toBe('success');
+
+ // Should trigger fallback after 4 consecutive 429s (attempts 2-5)
+ expect(fallbackCallback).toHaveBeenCalledWith('oauth-personal');
+ });
+ });
});