Download packages/cli/src/ui/hooks/creditsFlowHandler.test.ts from SaylorTwift/gemini-cli: direct link, hf CLI and curl.
- Browser
- Download file 9.89 kB
-
https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/hooks/creditsFlowHandler.test.ts
- Command line
-
hf download hf://SaylorTwift/gemini-cli/packages/cli/src/ui/hooks/creditsFlowHandler.test.ts
-
curl -L -o creditsFlowHandler.test.ts https://huggingface.co/SaylorTwift/gemini-cli/resolve/main/packages/cli/src/ui/hooks/creditsFlowHandler.test.ts
9.89 kB
| /** | |
| * @license | |
| * Copyright 2026 Google LLC | |
| * SPDX-License-Identifier: Apache-2.0 | |
| */ | |
| import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; | |
| import { handleCreditsFlow } from './creditsFlowHandler.js'; | |
| import type { UseHistoryManagerReturn } from './useHistoryManager.js'; | |
| import { | |
| type Config, | |
| type GeminiUserTier, | |
| makeFakeConfig, | |
| getG1CreditBalance, | |
| shouldAutoUseCredits, | |
| shouldShowOverageMenu, | |
| shouldShowEmptyWalletMenu, | |
| shouldLaunchBrowser, | |
| logBillingEvent, | |
| G1_CREDIT_TYPE, | |
| UserTierId, | |
| } from '@google/gemini-cli-core'; | |
| import { MessageType } from '../types.js'; | |
| vi.mock('@google/gemini-cli-core', async (importOriginal) => { | |
| const actual = | |
| await importOriginal<typeof import('@google/gemini-cli-core')>(); | |
| return { | |
| ...actual, | |
| getG1CreditBalance: vi.fn(), | |
| shouldAutoUseCredits: vi.fn(), | |
| shouldShowOverageMenu: vi.fn(), | |
| shouldShowEmptyWalletMenu: vi.fn(), | |
| logBillingEvent: vi.fn(), | |
| openBrowserSecurely: vi.fn(), | |
| shouldLaunchBrowser: vi.fn().mockReturnValue(true), | |
| }; | |
| }); | |
| describe('handleCreditsFlow', () => { | |
| let mockConfig: Config; | |
| let mockHistoryManager: UseHistoryManagerReturn; | |
| let isDialogPending: React.MutableRefObject<boolean>; | |
| let mockSetOverageMenuRequest: ReturnType<typeof vi.fn>; | |
| let mockSetEmptyWalletRequest: ReturnType<typeof vi.fn>; | |
| let mockSetModelSwitchedFromQuotaError: ReturnType<typeof vi.fn>; | |
| const mockPaidTier: GeminiUserTier = { | |
| id: UserTierId.STANDARD, | |
| availableCredits: [{ creditType: G1_CREDIT_TYPE, creditAmount: '100' }], | |
| }; | |
| beforeEach(() => { | |
| mockConfig = makeFakeConfig(); | |
| mockHistoryManager = { | |
| addItem: vi.fn(), | |
| history: [], | |
| updateItem: vi.fn(), | |
| clearItems: vi.fn(), | |
| loadHistory: vi.fn(), | |
| }; | |
| isDialogPending = { current: false }; | |
| mockSetOverageMenuRequest = vi.fn(); | |
| mockSetEmptyWalletRequest = vi.fn(); | |
| mockSetModelSwitchedFromQuotaError = vi.fn(); | |
| vi.spyOn(mockConfig, 'setQuotaErrorOccurred'); | |
| vi.spyOn(mockConfig, 'setOverageStrategy'); | |
| vi.mocked(getG1CreditBalance).mockReturnValue(100); | |
| vi.mocked(shouldAutoUseCredits).mockReturnValue(false); | |
| vi.mocked(shouldShowOverageMenu).mockReturnValue(false); | |
| vi.mocked(shouldShowEmptyWalletMenu).mockReturnValue(false); | |
| }); | |
| afterEach(() => { | |
| vi.restoreAllMocks(); | |
| }); | |
| function makeArgs( | |
| overrides?: Partial<Parameters<typeof handleCreditsFlow>[0]>, | |
| ) { | |
| return { | |
| config: mockConfig, | |
| paidTier: mockPaidTier, | |
| overageStrategy: 'ask' as const, | |
| failedModel: 'gemini-3-pro-preview', | |
| fallbackModel: 'gemini-3-flash-preview', | |
| usageLimitReachedModel: 'all Pro models', | |
| resetTime: '3:45 PM', | |
| historyManager: mockHistoryManager, | |
| setModelSwitchedFromQuotaError: mockSetModelSwitchedFromQuotaError, | |
| isDialogPending, | |
| setOverageMenuRequest: mockSetOverageMenuRequest, | |
| setEmptyWalletRequest: mockSetEmptyWalletRequest, | |
| ...overrides, | |
| }; | |
| } | |
| it('should return null if credit balance is null (non-G1 user)', async () => { | |
| vi.mocked(getG1CreditBalance).mockReturnValue(null); | |
| const result = await handleCreditsFlow(makeArgs()); | |
| expect(result).toBeNull(); | |
| }); | |
| it('should return null if credits are already auto-used (strategy=always)', async () => { | |
| vi.mocked(shouldAutoUseCredits).mockReturnValue(true); | |
| const result = await handleCreditsFlow(makeArgs()); | |
| expect(result).toBeNull(); | |
| }); | |
| it('should show overage menu and return retry_with_credits when use_credits selected', async () => { | |
| vi.mocked(shouldShowOverageMenu).mockReturnValue(true); | |
| const flowPromise = handleCreditsFlow(makeArgs()); | |
| // Extract the resolve callback from the setOverageMenuRequest call | |
| expect(mockSetOverageMenuRequest).toHaveBeenCalledOnce(); | |
| const request = mockSetOverageMenuRequest.mock.calls[0][0]; | |
| expect(request.failedModel).toBe('all Pro models'); | |
| expect(request.creditBalance).toBe(100); | |
| // Simulate user choosing 'use_credits' | |
| request.resolve('use_credits'); | |
| const result = await flowPromise; | |
| expect(result).toBe('retry_with_credits'); | |
| expect(mockConfig.setOverageStrategy).toHaveBeenCalledWith('always'); | |
| expect(logBillingEvent).toHaveBeenCalled(); | |
| }); | |
| it('should show overage menu and return retry_always when use_fallback selected', async () => { | |
| vi.mocked(shouldShowOverageMenu).mockReturnValue(true); | |
| const flowPromise = handleCreditsFlow(makeArgs()); | |
| const request = mockSetOverageMenuRequest.mock.calls[0][0]; | |
| request.resolve('use_fallback'); | |
| const result = await flowPromise; | |
| expect(result).toBe('retry_always'); | |
| }); | |
| it('should show overage menu and return stop when stop selected', async () => { | |
| vi.mocked(shouldShowOverageMenu).mockReturnValue(true); | |
| const flowPromise = handleCreditsFlow(makeArgs()); | |
| const request = mockSetOverageMenuRequest.mock.calls[0][0]; | |
| request.resolve('stop'); | |
| const result = await flowPromise; | |
| expect(result).toBe('stop'); | |
| }); | |
| it('should return stop immediately if dialog is already pending (overage)', async () => { | |
| vi.mocked(shouldShowOverageMenu).mockReturnValue(true); | |
| isDialogPending.current = true; | |
| const result = await handleCreditsFlow(makeArgs()); | |
| expect(result).toBe('stop'); | |
| expect(mockSetOverageMenuRequest).not.toHaveBeenCalled(); | |
| }); | |
| it('should show empty wallet menu and return stop when get_credits selected', async () => { | |
| vi.mocked(shouldShowEmptyWalletMenu).mockReturnValue(true); | |
| const flowPromise = handleCreditsFlow(makeArgs()); | |
| expect(mockSetEmptyWalletRequest).toHaveBeenCalledOnce(); | |
| const request = mockSetEmptyWalletRequest.mock.calls[0][0]; | |
| expect(request.failedModel).toBe('all Pro models'); | |
| request.resolve('get_credits'); | |
| const result = await flowPromise; | |
| expect(result).toBe('stop'); | |
| expect(mockHistoryManager.addItem).toHaveBeenCalledWith( | |
| expect.objectContaining({ | |
| type: MessageType.INFO, | |
| text: expect.stringContaining('few minutes'), | |
| }), | |
| expect.any(Number), | |
| ); | |
| }); | |
| it('should show empty wallet menu and return retry_always when use_fallback selected', async () => { | |
| vi.mocked(shouldShowEmptyWalletMenu).mockReturnValue(true); | |
| const flowPromise = handleCreditsFlow(makeArgs()); | |
| const request = mockSetEmptyWalletRequest.mock.calls[0][0]; | |
| request.resolve('use_fallback'); | |
| const result = await flowPromise; | |
| expect(result).toBe('retry_always'); | |
| }); | |
| it('should return stop immediately if dialog is already pending (empty wallet)', async () => { | |
| vi.mocked(shouldShowEmptyWalletMenu).mockReturnValue(true); | |
| isDialogPending.current = true; | |
| const result = await handleCreditsFlow(makeArgs()); | |
| expect(result).toBe('stop'); | |
| expect(mockSetEmptyWalletRequest).not.toHaveBeenCalled(); | |
| }); | |
| it('should return null if no flow conditions are met', async () => { | |
| vi.mocked(getG1CreditBalance).mockReturnValue(100); | |
| vi.mocked(shouldAutoUseCredits).mockReturnValue(false); | |
| vi.mocked(shouldShowOverageMenu).mockReturnValue(false); | |
| vi.mocked(shouldShowEmptyWalletMenu).mockReturnValue(false); | |
| const result = await handleCreditsFlow(makeArgs()); | |
| expect(result).toBeNull(); | |
| }); | |
| it('should clear dialog state after overage menu resolves', async () => { | |
| vi.mocked(shouldShowOverageMenu).mockReturnValue(true); | |
| const flowPromise = handleCreditsFlow(makeArgs()); | |
| expect(isDialogPending.current).toBe(true); | |
| const request = mockSetOverageMenuRequest.mock.calls[0][0]; | |
| request.resolve('stop'); | |
| await flowPromise; | |
| expect(isDialogPending.current).toBe(false); | |
| // Verify null was set to clear the request | |
| expect(mockSetOverageMenuRequest).toHaveBeenCalledWith(null); | |
| }); | |
| it('should clear dialog state after empty wallet menu resolves', async () => { | |
| vi.mocked(shouldShowEmptyWalletMenu).mockReturnValue(true); | |
| const flowPromise = handleCreditsFlow(makeArgs()); | |
| expect(isDialogPending.current).toBe(true); | |
| const request = mockSetEmptyWalletRequest.mock.calls[0][0]; | |
| request.resolve('stop'); | |
| await flowPromise; | |
| expect(isDialogPending.current).toBe(false); | |
| expect(mockSetEmptyWalletRequest).toHaveBeenCalledWith(null); | |
| }); | |
| describe('headless mode (shouldLaunchBrowser=false)', () => { | |
| beforeEach(() => { | |
| vi.mocked(shouldLaunchBrowser).mockReturnValue(false); | |
| }); | |
| it('should show manage URL in history when manage selected in headless mode', async () => { | |
| vi.mocked(shouldShowOverageMenu).mockReturnValue(true); | |
| const flowPromise = handleCreditsFlow(makeArgs()); | |
| const request = mockSetOverageMenuRequest.mock.calls[0][0]; | |
| request.resolve('manage'); | |
| const result = await flowPromise; | |
| expect(result).toBe('stop'); | |
| expect(mockHistoryManager.addItem).toHaveBeenCalledWith( | |
| expect.objectContaining({ | |
| type: MessageType.INFO, | |
| text: expect.stringContaining('Please open this URL in a browser:'), | |
| }), | |
| expect.any(Number), | |
| ); | |
| }); | |
| it('should show credits URL in history when get_credits selected in headless mode', async () => { | |
| vi.mocked(shouldShowEmptyWalletMenu).mockReturnValue(true); | |
| const flowPromise = handleCreditsFlow(makeArgs()); | |
| const request = mockSetEmptyWalletRequest.mock.calls[0][0]; | |
| // Trigger onGetCredits callback and wait for it | |
| await request.onGetCredits(); | |
| expect(mockHistoryManager.addItem).toHaveBeenCalledWith( | |
| expect.objectContaining({ | |
| type: MessageType.INFO, | |
| text: expect.stringContaining('Please open this URL in a browser:'), | |
| }), | |
| expect.any(Number), | |
| ); | |
| request.resolve('get_credits'); | |
| await flowPromise; | |
| }); | |
| }); | |
| }); | |