|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import { describe, it, expect, beforeEach, vi } from 'vitest'; |
|
|
import { Config } from '../config/config.js'; |
|
|
import { |
|
|
setSimulate429, |
|
|
disableSimulationAfterFallback, |
|
|
shouldSimulate429, |
|
|
createSimulated429Error, |
|
|
resetRequestCounter, |
|
|
} from './testUtils.js'; |
|
|
import { DEFAULT_GEMINI_FLASH_MODEL } from '../config/models.js'; |
|
|
import { retryWithBackoff } from './retry.js'; |
|
|
import { AuthType } from '../core/contentGenerator.js'; |
|
|
|
|
|
describe('Flash Fallback Integration', () => { |
|
|
let config: Config; |
|
|
|
|
|
beforeEach(() => { |
|
|
config = new Config({ |
|
|
sessionId: 'test-session', |
|
|
targetDir: '/test', |
|
|
debugMode: false, |
|
|
cwd: '/test', |
|
|
model: 'gemini-2.5-pro', |
|
|
}); |
|
|
|
|
|
|
|
|
setSimulate429(false); |
|
|
resetRequestCounter(); |
|
|
}); |
|
|
|
|
|
it('should automatically accept fallback', async () => { |
|
|
|
|
|
const flashFallbackHandler = async (): Promise<boolean> => true; |
|
|
|
|
|
config.setFlashFallbackHandler(flashFallbackHandler); |
|
|
|
|
|
|
|
|
const result = await config.flashFallbackHandler!( |
|
|
'gemini-2.5-pro', |
|
|
DEFAULT_GEMINI_FLASH_MODEL, |
|
|
); |
|
|
|
|
|
|
|
|
expect(result).toBe(true); |
|
|
}); |
|
|
|
|
|
it('should trigger fallback after 2 consecutive 429 errors for OAuth users', async () => { |
|
|
let fallbackCalled = false; |
|
|
let fallbackModel = ''; |
|
|
|
|
|
|
|
|
const mockApiCall = vi |
|
|
.fn() |
|
|
.mockRejectedValueOnce(createSimulated429Error()) |
|
|
.mockRejectedValueOnce(createSimulated429Error()) |
|
|
.mockResolvedValueOnce('success after fallback'); |
|
|
|
|
|
|
|
|
const mockFallbackHandler = vi.fn(async (_authType?: string) => { |
|
|
fallbackCalled = true; |
|
|
fallbackModel = DEFAULT_GEMINI_FLASH_MODEL; |
|
|
return fallbackModel; |
|
|
}); |
|
|
|
|
|
|
|
|
const result = await retryWithBackoff(mockApiCall, { |
|
|
maxAttempts: 2, |
|
|
initialDelayMs: 1, |
|
|
maxDelayMs: 10, |
|
|
shouldRetry: (error: Error) => { |
|
|
const status = (error as Error & { status?: number }).status; |
|
|
return status === 429; |
|
|
}, |
|
|
onPersistent429: mockFallbackHandler, |
|
|
authType: AuthType.LOGIN_WITH_GOOGLE_PERSONAL, |
|
|
}); |
|
|
|
|
|
|
|
|
expect(fallbackCalled).toBe(true); |
|
|
expect(fallbackModel).toBe(DEFAULT_GEMINI_FLASH_MODEL); |
|
|
expect(mockFallbackHandler).toHaveBeenCalledWith( |
|
|
AuthType.LOGIN_WITH_GOOGLE_PERSONAL, |
|
|
); |
|
|
expect(result).toBe('success after fallback'); |
|
|
|
|
|
expect(mockApiCall).toHaveBeenCalledTimes(3); |
|
|
}); |
|
|
|
|
|
it('should not trigger fallback for API key users', async () => { |
|
|
let fallbackCalled = false; |
|
|
|
|
|
|
|
|
const mockApiCall = vi.fn().mockRejectedValue(createSimulated429Error()); |
|
|
|
|
|
|
|
|
const mockFallbackHandler = vi.fn(async () => { |
|
|
fallbackCalled = true; |
|
|
return DEFAULT_GEMINI_FLASH_MODEL; |
|
|
}); |
|
|
|
|
|
|
|
|
try { |
|
|
await retryWithBackoff(mockApiCall, { |
|
|
maxAttempts: 5, |
|
|
initialDelayMs: 10, |
|
|
maxDelayMs: 100, |
|
|
shouldRetry: (error: Error) => { |
|
|
const status = (error as Error & { status?: number }).status; |
|
|
return status === 429; |
|
|
}, |
|
|
onPersistent429: mockFallbackHandler, |
|
|
authType: AuthType.USE_GEMINI, |
|
|
}); |
|
|
} catch (error) { |
|
|
|
|
|
expect((error as Error).message).toContain('Rate limit exceeded'); |
|
|
} |
|
|
|
|
|
|
|
|
expect(fallbackCalled).toBe(false); |
|
|
expect(mockFallbackHandler).not.toHaveBeenCalled(); |
|
|
}); |
|
|
|
|
|
it('should properly disable simulation state after fallback', () => { |
|
|
|
|
|
setSimulate429(true); |
|
|
|
|
|
|
|
|
expect(shouldSimulate429()).toBe(true); |
|
|
|
|
|
|
|
|
disableSimulationAfterFallback(); |
|
|
|
|
|
|
|
|
expect(shouldSimulate429()).toBe(false); |
|
|
}); |
|
|
}); |
|
|
|