2025-09-08 16:19:52 -04:00
|
|
|
/**
|
|
|
|
|
* @license
|
|
|
|
|
* Copyright 2025 Google LLC
|
|
|
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
vi,
|
|
|
|
|
describe,
|
|
|
|
|
it,
|
|
|
|
|
expect,
|
|
|
|
|
beforeEach,
|
|
|
|
|
afterEach,
|
|
|
|
|
type Mock,
|
|
|
|
|
} from 'vitest';
|
2025-10-28 10:32:15 -07:00
|
|
|
import { act } from 'react';
|
|
|
|
|
import { renderHook } from '../../test-utils/render.js';
|
2025-09-08 16:19:52 -04:00
|
|
|
import {
|
|
|
|
|
type Config,
|
|
|
|
|
type FallbackModelHandler,
|
2025-10-30 11:50:26 -07:00
|
|
|
type FallbackIntent,
|
2025-09-08 16:19:52 -04:00
|
|
|
UserTierId,
|
|
|
|
|
AuthType,
|
2025-10-24 11:09:06 -07:00
|
|
|
TerminalQuotaError,
|
2025-09-08 16:19:52 -04:00
|
|
|
makeFakeConfig,
|
2025-10-24 11:09:06 -07:00
|
|
|
type GoogleApiError,
|
|
|
|
|
RetryableQuotaError,
|
2025-09-08 16:19:52 -04:00
|
|
|
} from '@google/gemini-cli-core';
|
|
|
|
|
import { useQuotaAndFallback } from './useQuotaAndFallback.js';
|
|
|
|
|
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
2025-11-06 13:43:21 -05:00
|
|
|
import { MessageType } from '../types.js';
|
2025-09-08 16:19:52 -04:00
|
|
|
|
|
|
|
|
// Use a type alias for SpyInstance as it's not directly exported
|
|
|
|
|
type SpyInstance = ReturnType<typeof vi.spyOn>;
|
|
|
|
|
|
|
|
|
|
describe('useQuotaAndFallback', () => {
|
|
|
|
|
let mockConfig: Config;
|
|
|
|
|
let mockHistoryManager: UseHistoryManagerReturn;
|
|
|
|
|
let mockSetModelSwitchedFromQuotaError: Mock;
|
|
|
|
|
let setFallbackHandlerSpy: SpyInstance;
|
2025-10-24 11:09:06 -07:00
|
|
|
let mockGoogleApiError: GoogleApiError;
|
2025-09-08 16:19:52 -04:00
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
mockConfig = makeFakeConfig();
|
2025-10-24 11:09:06 -07:00
|
|
|
mockGoogleApiError = {
|
|
|
|
|
code: 429,
|
|
|
|
|
message: 'mock error',
|
|
|
|
|
details: [],
|
|
|
|
|
};
|
2025-09-08 16:19:52 -04:00
|
|
|
|
|
|
|
|
// Spy on the method that requires the private field and mock its return.
|
|
|
|
|
// This is cleaner than modifying the config class for tests.
|
|
|
|
|
vi.spyOn(mockConfig, 'getContentGeneratorConfig').mockReturnValue({
|
|
|
|
|
authType: AuthType.LOGIN_WITH_GOOGLE,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
mockHistoryManager = {
|
|
|
|
|
addItem: vi.fn(),
|
|
|
|
|
history: [],
|
|
|
|
|
updateItem: vi.fn(),
|
|
|
|
|
clearItems: vi.fn(),
|
|
|
|
|
loadHistory: vi.fn(),
|
|
|
|
|
};
|
|
|
|
|
mockSetModelSwitchedFromQuotaError = vi.fn();
|
|
|
|
|
|
|
|
|
|
setFallbackHandlerSpy = vi.spyOn(mockConfig, 'setFallbackModelHandler');
|
|
|
|
|
vi.spyOn(mockConfig, 'setQuotaErrorOccurred');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
vi.clearAllMocks();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should register a fallback handler on initialization', () => {
|
|
|
|
|
renderHook(() =>
|
|
|
|
|
useQuotaAndFallback({
|
|
|
|
|
config: mockConfig,
|
|
|
|
|
historyManager: mockHistoryManager,
|
|
|
|
|
userTier: UserTierId.FREE,
|
|
|
|
|
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(setFallbackHandlerSpy).toHaveBeenCalledTimes(1);
|
|
|
|
|
expect(setFallbackHandlerSpy.mock.calls[0][0]).toBeInstanceOf(Function);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Fallback Handler Logic', () => {
|
|
|
|
|
// Helper function to render the hook and extract the registered handler
|
|
|
|
|
const getRegisteredHandler = (
|
|
|
|
|
userTier: UserTierId = UserTierId.FREE,
|
|
|
|
|
): FallbackModelHandler => {
|
|
|
|
|
renderHook(
|
|
|
|
|
(props) =>
|
|
|
|
|
useQuotaAndFallback({
|
|
|
|
|
config: mockConfig,
|
|
|
|
|
historyManager: mockHistoryManager,
|
|
|
|
|
userTier: props.userTier,
|
|
|
|
|
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
|
|
|
|
|
}),
|
|
|
|
|
{ initialProps: { userTier } },
|
|
|
|
|
);
|
|
|
|
|
return setFallbackHandlerSpy.mock.calls[0][0] as FallbackModelHandler;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
it('should return null and take no action if authType is not LOGIN_WITH_GOOGLE', async () => {
|
|
|
|
|
// Override the default mock from beforeEach for this specific test
|
|
|
|
|
vi.spyOn(mockConfig, 'getContentGeneratorConfig').mockReturnValue({
|
|
|
|
|
authType: AuthType.USE_GEMINI,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const handler = getRegisteredHandler();
|
|
|
|
|
const result = await handler('gemini-pro', 'gemini-flash', new Error());
|
|
|
|
|
|
|
|
|
|
expect(result).toBeNull();
|
|
|
|
|
expect(mockHistoryManager.addItem).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-06 17:13:38 -05:00
|
|
|
describe('Flash Model Fallback', () => {
|
|
|
|
|
it('should show a terminal quota message and stop, without offering a fallback', async () => {
|
|
|
|
|
const handler = getRegisteredHandler();
|
|
|
|
|
const result = await handler(
|
|
|
|
|
'gemini-2.5-flash',
|
|
|
|
|
'gemini-2.5-flash',
|
|
|
|
|
new TerminalQuotaError('flash quota', mockGoogleApiError),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(result).toBe('stop');
|
|
|
|
|
expect(mockHistoryManager.addItem).toHaveBeenCalledTimes(1);
|
|
|
|
|
const message = (mockHistoryManager.addItem as Mock).mock.calls[0][0]
|
|
|
|
|
.text;
|
|
|
|
|
expect(message).toContain(
|
|
|
|
|
'You have reached your daily gemini-2.5-flash',
|
|
|
|
|
);
|
|
|
|
|
expect(message).not.toContain('continue with the fallback model');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should show a capacity message and stop', async () => {
|
|
|
|
|
const handler = getRegisteredHandler();
|
|
|
|
|
// let result: FallbackIntent | null = null;
|
|
|
|
|
const result = await handler(
|
|
|
|
|
'gemini-2.5-flash',
|
|
|
|
|
'gemini-2.5-flash',
|
|
|
|
|
new Error('capacity'),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(result).toBe('stop');
|
|
|
|
|
expect(mockHistoryManager.addItem).toHaveBeenCalledTimes(1);
|
|
|
|
|
const message = (mockHistoryManager.addItem as Mock).mock.calls[0][0]
|
|
|
|
|
.text;
|
|
|
|
|
expect(message).toContain(
|
|
|
|
|
'Pardon Our Congestion! It looks like gemini-2.5-flash is very popular',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should show a capacity message and stop, even when already in fallback mode', async () => {
|
|
|
|
|
vi.spyOn(mockConfig, 'isInFallbackMode').mockReturnValue(true);
|
|
|
|
|
const handler = getRegisteredHandler();
|
|
|
|
|
const result = await handler(
|
|
|
|
|
'gemini-2.5-flash',
|
|
|
|
|
'gemini-2.5-flash',
|
|
|
|
|
new Error('capacity'),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
expect(result).toBe('stop');
|
|
|
|
|
expect(mockHistoryManager.addItem).toHaveBeenCalledTimes(1);
|
|
|
|
|
const message = (mockHistoryManager.addItem as Mock).mock.calls[0][0]
|
|
|
|
|
.text;
|
|
|
|
|
expect(message).toContain(
|
|
|
|
|
'Pardon Our Congestion! It looks like gemini-2.5-flash is very popular',
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-06 13:43:21 -05:00
|
|
|
describe('Interactive Fallback', () => {
|
|
|
|
|
// Pro Quota Errors
|
2025-09-08 16:19:52 -04:00
|
|
|
it('should set an interactive request and wait for user choice', async () => {
|
|
|
|
|
const { result } = renderHook(() =>
|
|
|
|
|
useQuotaAndFallback({
|
|
|
|
|
config: mockConfig,
|
|
|
|
|
historyManager: mockHistoryManager,
|
|
|
|
|
userTier: UserTierId.FREE,
|
|
|
|
|
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handler = setFallbackHandlerSpy.mock
|
|
|
|
|
.calls[0][0] as FallbackModelHandler;
|
|
|
|
|
|
|
|
|
|
// Call the handler but do not await it, to check the intermediate state
|
2025-10-30 11:50:26 -07:00
|
|
|
let promise: Promise<FallbackIntent | null>;
|
|
|
|
|
await act(() => {
|
|
|
|
|
promise = handler(
|
|
|
|
|
'gemini-pro',
|
|
|
|
|
'gemini-flash',
|
|
|
|
|
new TerminalQuotaError('pro quota', mockGoogleApiError),
|
|
|
|
|
);
|
|
|
|
|
});
|
2025-09-08 16:19:52 -04:00
|
|
|
|
|
|
|
|
// The hook should now have a pending request for the UI to handle
|
|
|
|
|
expect(result.current.proQuotaRequest).not.toBeNull();
|
|
|
|
|
expect(result.current.proQuotaRequest?.failedModel).toBe('gemini-pro');
|
|
|
|
|
|
|
|
|
|
// Simulate the user choosing to continue with the fallback model
|
2025-10-30 11:50:26 -07:00
|
|
|
await act(() => {
|
2025-11-06 13:43:21 -05:00
|
|
|
result.current.handleProQuotaChoice('retry');
|
2025-09-08 16:19:52 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// The original promise from the handler should now resolve
|
2025-10-30 11:50:26 -07:00
|
|
|
const intent = await promise!;
|
2025-09-08 16:19:52 -04:00
|
|
|
expect(intent).toBe('retry');
|
|
|
|
|
|
|
|
|
|
// The pending request should be cleared from the state
|
|
|
|
|
expect(result.current.proQuotaRequest).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle race conditions by stopping subsequent requests', async () => {
|
|
|
|
|
const { result } = renderHook(() =>
|
|
|
|
|
useQuotaAndFallback({
|
|
|
|
|
config: mockConfig,
|
|
|
|
|
historyManager: mockHistoryManager,
|
|
|
|
|
userTier: UserTierId.FREE,
|
|
|
|
|
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handler = setFallbackHandlerSpy.mock
|
|
|
|
|
.calls[0][0] as FallbackModelHandler;
|
|
|
|
|
|
2025-10-30 11:50:26 -07:00
|
|
|
let promise1: Promise<FallbackIntent | null>;
|
|
|
|
|
await act(() => {
|
|
|
|
|
promise1 = handler(
|
|
|
|
|
'gemini-pro',
|
|
|
|
|
'gemini-flash',
|
|
|
|
|
new TerminalQuotaError('pro quota 1', mockGoogleApiError),
|
|
|
|
|
);
|
|
|
|
|
});
|
2025-09-08 16:19:52 -04:00
|
|
|
|
|
|
|
|
const firstRequest = result.current.proQuotaRequest;
|
|
|
|
|
expect(firstRequest).not.toBeNull();
|
|
|
|
|
|
2025-10-30 11:50:26 -07:00
|
|
|
let result2: FallbackIntent | null;
|
|
|
|
|
await act(async () => {
|
|
|
|
|
result2 = await handler(
|
|
|
|
|
'gemini-pro',
|
|
|
|
|
'gemini-flash',
|
|
|
|
|
new TerminalQuotaError('pro quota 2', mockGoogleApiError),
|
|
|
|
|
);
|
|
|
|
|
});
|
2025-09-08 16:19:52 -04:00
|
|
|
|
|
|
|
|
// The lock should have stopped the second request
|
2025-10-30 11:50:26 -07:00
|
|
|
expect(result2!).toBe('stop');
|
2025-09-08 16:19:52 -04:00
|
|
|
expect(result.current.proQuotaRequest).toBe(firstRequest);
|
|
|
|
|
|
2025-10-30 11:50:26 -07:00
|
|
|
await act(() => {
|
2025-11-06 13:43:21 -05:00
|
|
|
result.current.handleProQuotaChoice('retry');
|
2025-09-08 16:19:52 -04:00
|
|
|
});
|
|
|
|
|
|
2025-10-30 11:50:26 -07:00
|
|
|
const intent1 = await promise1!;
|
2025-09-08 16:19:52 -04:00
|
|
|
expect(intent1).toBe('retry');
|
|
|
|
|
expect(result.current.proQuotaRequest).toBeNull();
|
|
|
|
|
});
|
2025-11-06 13:43:21 -05:00
|
|
|
|
|
|
|
|
// Non-Quota error test cases
|
|
|
|
|
const testCases = [
|
|
|
|
|
{
|
|
|
|
|
description: 'other error for FREE tier',
|
|
|
|
|
tier: UserTierId.FREE,
|
|
|
|
|
error: new Error('some error'),
|
|
|
|
|
expectedMessageSnippets: [
|
|
|
|
|
'🚦Pardon Our Congestion! It looks like model-A is very popular at the moment.',
|
|
|
|
|
'Please retry again later.',
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
description: 'other error for LEGACY tier',
|
|
|
|
|
tier: UserTierId.LEGACY, // Paid tier
|
|
|
|
|
error: new Error('some error'),
|
|
|
|
|
expectedMessageSnippets: [
|
|
|
|
|
'🚦Pardon Our Congestion! It looks like model-A is very popular at the moment.',
|
|
|
|
|
'Please retry again later.',
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
description: 'retryable quota error for FREE tier',
|
|
|
|
|
tier: UserTierId.FREE,
|
|
|
|
|
error: new RetryableQuotaError(
|
|
|
|
|
'retryable quota',
|
|
|
|
|
mockGoogleApiError,
|
|
|
|
|
5,
|
|
|
|
|
),
|
|
|
|
|
expectedMessageSnippets: [
|
|
|
|
|
'🚦Pardon Our Congestion! It looks like model-A is very popular at the moment.',
|
|
|
|
|
'Please retry again later.',
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
description: 'retryable quota error for LEGACY tier',
|
|
|
|
|
tier: UserTierId.LEGACY, // Paid tier
|
|
|
|
|
error: new RetryableQuotaError(
|
|
|
|
|
'retryable quota',
|
|
|
|
|
mockGoogleApiError,
|
|
|
|
|
5,
|
|
|
|
|
),
|
|
|
|
|
expectedMessageSnippets: [
|
|
|
|
|
'🚦Pardon Our Congestion! It looks like model-A is very popular at the moment.',
|
|
|
|
|
'Please retry again later.',
|
|
|
|
|
],
|
|
|
|
|
},
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const {
|
|
|
|
|
description,
|
|
|
|
|
tier,
|
|
|
|
|
error,
|
|
|
|
|
expectedMessageSnippets,
|
|
|
|
|
} of testCases) {
|
|
|
|
|
it(`should handle ${description} correctly`, async () => {
|
|
|
|
|
const { result } = renderHook(
|
|
|
|
|
(props) =>
|
|
|
|
|
useQuotaAndFallback({
|
|
|
|
|
config: mockConfig,
|
|
|
|
|
historyManager: mockHistoryManager,
|
|
|
|
|
userTier: props.tier,
|
|
|
|
|
setModelSwitchedFromQuotaError:
|
|
|
|
|
mockSetModelSwitchedFromQuotaError,
|
|
|
|
|
}),
|
|
|
|
|
{ initialProps: { tier } },
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handler = setFallbackHandlerSpy.mock
|
|
|
|
|
.calls[0][0] as FallbackModelHandler;
|
|
|
|
|
|
|
|
|
|
// Call the handler but do not await it, to check the intermediate state
|
|
|
|
|
let promise: Promise<FallbackIntent | null>;
|
|
|
|
|
await act(() => {
|
|
|
|
|
promise = handler('model-A', 'model-B', error);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// The hook should now have a pending request for the UI to handle
|
|
|
|
|
expect(result.current.proQuotaRequest).not.toBeNull();
|
|
|
|
|
expect(result.current.proQuotaRequest?.failedModel).toBe('model-A');
|
|
|
|
|
|
|
|
|
|
// Check that the correct initial message was added
|
|
|
|
|
expect(mockHistoryManager.addItem).toHaveBeenCalledWith(
|
|
|
|
|
expect.objectContaining({ type: MessageType.INFO }),
|
|
|
|
|
expect.any(Number),
|
|
|
|
|
);
|
|
|
|
|
const message = (mockHistoryManager.addItem as Mock).mock.calls[0][0]
|
|
|
|
|
.text;
|
|
|
|
|
for (const snippet of expectedMessageSnippets) {
|
|
|
|
|
expect(message).toContain(snippet);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Simulate the user choosing to continue with the fallback model
|
|
|
|
|
await act(() => {
|
|
|
|
|
result.current.handleProQuotaChoice('retry');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(mockSetModelSwitchedFromQuotaError).toHaveBeenCalledWith(true);
|
|
|
|
|
// The original promise from the handler should now resolve
|
|
|
|
|
const intent = await promise!;
|
|
|
|
|
expect(intent).toBe('retry');
|
|
|
|
|
|
|
|
|
|
// The pending request should be cleared from the state
|
|
|
|
|
expect(result.current.proQuotaRequest).toBeNull();
|
|
|
|
|
expect(mockConfig.setQuotaErrorOccurred).toHaveBeenCalledWith(true);
|
|
|
|
|
});
|
|
|
|
|
}
|
2025-09-08 16:19:52 -04:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('handleProQuotaChoice', () => {
|
|
|
|
|
it('should do nothing if there is no pending pro quota request', () => {
|
|
|
|
|
const { result } = renderHook(() =>
|
|
|
|
|
useQuotaAndFallback({
|
|
|
|
|
config: mockConfig,
|
|
|
|
|
historyManager: mockHistoryManager,
|
|
|
|
|
userTier: UserTierId.FREE,
|
|
|
|
|
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
act(() => {
|
2025-11-06 13:43:21 -05:00
|
|
|
result.current.handleProQuotaChoice('retry_later');
|
2025-09-08 16:19:52 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(mockHistoryManager.addItem).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
2025-11-06 13:43:21 -05:00
|
|
|
it('should resolve intent to "retry_later"', async () => {
|
2025-09-08 16:19:52 -04:00
|
|
|
const { result } = renderHook(() =>
|
|
|
|
|
useQuotaAndFallback({
|
|
|
|
|
config: mockConfig,
|
|
|
|
|
historyManager: mockHistoryManager,
|
|
|
|
|
userTier: UserTierId.FREE,
|
|
|
|
|
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handler = setFallbackHandlerSpy.mock
|
|
|
|
|
.calls[0][0] as FallbackModelHandler;
|
2025-10-30 11:50:26 -07:00
|
|
|
let promise: Promise<FallbackIntent | null>;
|
|
|
|
|
await act(() => {
|
|
|
|
|
promise = handler(
|
|
|
|
|
'gemini-pro',
|
|
|
|
|
'gemini-flash',
|
|
|
|
|
new TerminalQuotaError('pro quota', mockGoogleApiError),
|
|
|
|
|
);
|
|
|
|
|
});
|
2025-09-08 16:19:52 -04:00
|
|
|
|
2025-10-30 11:50:26 -07:00
|
|
|
await act(() => {
|
2025-11-06 13:43:21 -05:00
|
|
|
result.current.handleProQuotaChoice('retry_later');
|
2025-09-08 16:19:52 -04:00
|
|
|
});
|
|
|
|
|
|
2025-10-30 11:50:26 -07:00
|
|
|
const intent = await promise!;
|
2025-11-06 13:43:21 -05:00
|
|
|
expect(intent).toBe('retry_later');
|
2025-09-08 16:19:52 -04:00
|
|
|
expect(result.current.proQuotaRequest).toBeNull();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should resolve intent to "retry" and add info message on continue', async () => {
|
|
|
|
|
const { result } = renderHook(() =>
|
|
|
|
|
useQuotaAndFallback({
|
|
|
|
|
config: mockConfig,
|
|
|
|
|
historyManager: mockHistoryManager,
|
|
|
|
|
userTier: UserTierId.FREE,
|
|
|
|
|
setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handler = setFallbackHandlerSpy.mock
|
|
|
|
|
.calls[0][0] as FallbackModelHandler;
|
|
|
|
|
// The first `addItem` call is for the initial quota error message
|
2025-10-30 11:50:26 -07:00
|
|
|
let promise: Promise<FallbackIntent | null>;
|
|
|
|
|
await act(() => {
|
|
|
|
|
promise = handler(
|
|
|
|
|
'gemini-pro',
|
|
|
|
|
'gemini-flash',
|
|
|
|
|
new TerminalQuotaError('pro quota', mockGoogleApiError),
|
|
|
|
|
);
|
|
|
|
|
});
|
2025-09-08 16:19:52 -04:00
|
|
|
|
2025-10-30 11:50:26 -07:00
|
|
|
await act(() => {
|
2025-11-06 13:43:21 -05:00
|
|
|
result.current.handleProQuotaChoice('retry');
|
2025-09-08 16:19:52 -04:00
|
|
|
});
|
|
|
|
|
|
2025-10-30 11:50:26 -07:00
|
|
|
const intent = await promise!;
|
2025-09-08 16:19:52 -04:00
|
|
|
expect(intent).toBe('retry');
|
|
|
|
|
expect(result.current.proQuotaRequest).toBeNull();
|
|
|
|
|
|
|
|
|
|
// Check for the second "Switched to fallback model" message
|
|
|
|
|
expect(mockHistoryManager.addItem).toHaveBeenCalledTimes(2);
|
|
|
|
|
const lastCall = (mockHistoryManager.addItem as Mock).mock.calls[1][0];
|
|
|
|
|
expect(lastCall.type).toBe(MessageType.INFO);
|
|
|
|
|
expect(lastCall.text).toContain('Switched to fallback model.');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|