/** * @license * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { describe, it, expect } from 'vitest'; import type { GeminiUserTier } from '../code_assist/types.js'; import { buildG1Url, getG1CreditBalance, G1_CREDIT_TYPE, G1_UTM_CAMPAIGNS, isOverageEligibleModel, shouldAutoUseCredits, shouldShowEmptyWalletMenu, shouldShowOverageMenu, wrapInAccountChooser, } from './billing.js'; describe('billing', () => { describe('wrapInAccountChooser', () => { it('should wrap URL with AccountChooser redirect', () => { const result = wrapInAccountChooser( 'user@gmail.com', 'https://one.google.com/ai/activity', ); expect(result).toBe( 'https://accounts.google.com/AccountChooser?Email=user%40gmail.com&continue=https%3A%2F%2Fone.google.com%2Fai%2Factivity', ); }); it('should handle special characters in email', () => { const result = wrapInAccountChooser( 'user+test@example.com', 'https://example.com', ); expect(result).toContain('Email=user%2Btest%40example.com'); }); }); describe('buildG1Url', () => { it('should build activity URL with UTM params wrapped in AccountChooser', () => { const result = buildG1Url( 'activity', 'user@gmail.com', G1_UTM_CAMPAIGNS.MANAGE_ACTIVITY, ); // Should contain AccountChooser prefix expect(result).toContain('https://accounts.google.com/AccountChooser'); expect(result).toContain('Email=user%40gmail.com'); // The continue URL should contain the G1 activity path and UTM params expect(result).toContain('one.google.com%2Fai%2Factivity'); expect(result).toContain('utm_source%3Dgemini_cli'); expect(result).toContain( 'utm_campaign%3Dhydrogen_cli_settings_ai_credits_activity_page', ); }); it('should build credits URL with UTM params wrapped in AccountChooser', () => { const result = buildG1Url( 'credits', 'test@example.com', G1_UTM_CAMPAIGNS.EMPTY_WALLET_ADD_CREDITS, ); expect(result).toContain('https://accounts.google.com/AccountChooser'); expect(result).toContain('one.google.com%2Fai%2Fcredits'); expect(result).toContain( 'utm_campaign%3Dhydrogen_cli_insufficient_credits_add_credits', ); }); }); describe('getG1CreditBalance', () => { it('should return null for null tier', () => { expect(getG1CreditBalance(null)).toBeNull(); }); it('should return null for undefined tier', () => { expect(getG1CreditBalance(undefined)).toBeNull(); }); it('should return null for tier without availableCredits', () => { const tier: GeminiUserTier = { id: 'PERSONAL' }; expect(getG1CreditBalance(tier)).toBeNull(); }); it('should return null for empty availableCredits array', () => { const tier: GeminiUserTier = { id: 'PERSONAL', availableCredits: [] }; expect(getG1CreditBalance(tier)).toBeNull(); }); it('should return null when no G1 credit type found', () => { const tier: GeminiUserTier = { id: 'PERSONAL', availableCredits: [ { creditType: 'CREDIT_TYPE_UNSPECIFIED', creditAmount: '100' }, ], }; expect(getG1CreditBalance(tier)).toBeNull(); }); it('should return G1 credit balance when present', () => { const tier: GeminiUserTier = { id: 'PERSONAL', availableCredits: [{ creditType: G1_CREDIT_TYPE, creditAmount: '500' }], }; expect(getG1CreditBalance(tier)).toBe(500); }); it('should return G1 credit balance when multiple credit types present', () => { const tier: GeminiUserTier = { id: 'PERSONAL', availableCredits: [ { creditType: 'CREDIT_TYPE_UNSPECIFIED', creditAmount: '100' }, { creditType: G1_CREDIT_TYPE, creditAmount: '750' }, ], }; expect(getG1CreditBalance(tier)).toBe(750); }); it('should return 0 for invalid credit amount', () => { const tier: GeminiUserTier = { id: 'PERSONAL', availableCredits: [ { creditType: G1_CREDIT_TYPE, creditAmount: 'invalid' }, ], }; expect(getG1CreditBalance(tier)).toBe(0); }); it('should handle large credit amounts (int64 as string)', () => { const tier: GeminiUserTier = { id: 'PERSONAL', availableCredits: [ { creditType: G1_CREDIT_TYPE, creditAmount: '9999999999' }, ], }; expect(getG1CreditBalance(tier)).toBe(9999999999); }); it('should sum multiple credits of the same G1 type', () => { const tier: GeminiUserTier = { id: 'PERSONAL', availableCredits: [ { creditType: G1_CREDIT_TYPE, creditAmount: '1000' }, { creditType: G1_CREDIT_TYPE, creditAmount: '8' }, ], }; expect(getG1CreditBalance(tier)).toBe(1008); }); }); describe('shouldAutoUseCredits', () => { it('should return true when strategy is always and balance > 0', () => { expect(shouldAutoUseCredits('always', 100)).toBe(true); }); it('should return false when strategy is always but balance is 0', () => { expect(shouldAutoUseCredits('always', 0)).toBe(false); }); it('should return false when strategy is ask', () => { expect(shouldAutoUseCredits('ask', 100)).toBe(false); }); it('should return false when strategy is never', () => { expect(shouldAutoUseCredits('never', 100)).toBe(false); }); it('should return false when creditBalance is null (ineligible)', () => { expect(shouldAutoUseCredits('always', null)).toBe(false); }); }); describe('shouldShowOverageMenu', () => { it('should return true when strategy is ask and balance > 0', () => { expect(shouldShowOverageMenu('ask', 100)).toBe(true); }); it('should return false when strategy is ask but balance is 0', () => { expect(shouldShowOverageMenu('ask', 0)).toBe(false); }); it('should return false when strategy is always', () => { expect(shouldShowOverageMenu('always', 100)).toBe(false); }); it('should return false when strategy is never', () => { expect(shouldShowOverageMenu('never', 100)).toBe(false); }); it('should return false when creditBalance is null (ineligible)', () => { expect(shouldShowOverageMenu('ask', null)).toBe(false); }); }); describe('shouldShowEmptyWalletMenu', () => { it('should return true when strategy is ask and balance is 0', () => { expect(shouldShowEmptyWalletMenu('ask', 0)).toBe(true); }); it('should return true when strategy is always and balance is 0', () => { expect(shouldShowEmptyWalletMenu('always', 0)).toBe(true); }); it('should return false when strategy is never', () => { expect(shouldShowEmptyWalletMenu('never', 0)).toBe(false); }); it('should return false when balance > 0', () => { expect(shouldShowEmptyWalletMenu('ask', 100)).toBe(false); }); it('should return false when creditBalance is null (ineligible)', () => { expect(shouldShowEmptyWalletMenu('ask', null)).toBe(false); }); }); describe('isOverageEligibleModel', () => { it('should return true for gemini-3-pro-preview', () => { expect(isOverageEligibleModel('gemini-3-pro-preview')).toBe(true); }); it('should return true for gemini-3.1-pro-preview', () => { expect(isOverageEligibleModel('gemini-3.1-pro-preview')).toBe(true); }); it('should return false for gemini-3.1-pro-preview-customtools', () => { expect(isOverageEligibleModel('gemini-3.1-pro-preview-customtools')).toBe( false, ); }); it('should return true for gemini-3-flash-preview', () => { expect(isOverageEligibleModel('gemini-3-flash-preview')).toBe(true); }); it('should return false for gemini-2.5-pro', () => { expect(isOverageEligibleModel('gemini-2.5-pro')).toBe(false); }); it('should return false for gemini-2.5-flash', () => { expect(isOverageEligibleModel('gemini-2.5-flash')).toBe(false); }); it('should return false for custom model names', () => { expect(isOverageEligibleModel('my-custom-model')).toBe(false); }); }); });