mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-08 02:19:02 +00:00
test(cliproxy): add unit tests for quota fetchers and auth utilities
Add comprehensive unit tests for: - buildCodexQuotaWindows(): window parsing, clamping, reset time calculation - buildGeminiCliBuckets(): bucket grouping, model series, token types - resolveGeminiCliProjectId(): project ID extraction from account field - sanitizeEmail(): email to filename conversion - isTokenExpired(): token expiry validation Also removes unused 'preferred' field from GEMINI_CLI_GROUPS (YAGNI) 39 new tests covering edge cases: - Percentage clamping (0-100, 0-1) - Missing/null fields - camelCase/snake_case API responses - Empty inputs - Invalid date strings Addresses code review feedback on PR #395
This commit is contained in:
@@ -25,18 +25,15 @@ const GEMINI_CLI_GROUPS: Record<
|
|||||||
{
|
{
|
||||||
label: string;
|
label: string;
|
||||||
models: string[];
|
models: string[];
|
||||||
preferred: string;
|
|
||||||
}
|
}
|
||||||
> = {
|
> = {
|
||||||
'gemini-flash-series': {
|
'gemini-flash-series': {
|
||||||
label: 'Gemini Flash Series',
|
label: 'Gemini Flash Series',
|
||||||
models: ['gemini-3-flash-preview', 'gemini-2.5-flash', 'gemini-2.5-flash-lite'],
|
models: ['gemini-3-flash-preview', 'gemini-2.5-flash', 'gemini-2.5-flash-lite'],
|
||||||
preferred: 'gemini-3-flash-preview',
|
|
||||||
},
|
},
|
||||||
'gemini-pro-series': {
|
'gemini-pro-series': {
|
||||||
label: 'Gemini Pro Series',
|
label: 'Gemini Pro Series',
|
||||||
models: ['gemini-3-pro-preview', 'gemini-2.5-pro'],
|
models: ['gemini-3-pro-preview', 'gemini-2.5-pro'],
|
||||||
preferred: 'gemini-3-pro-preview',
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
/**
|
||||||
|
* Auth Utilities Unit Tests
|
||||||
|
*
|
||||||
|
* Tests for shared authentication utility functions
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'bun:test';
|
||||||
|
import { sanitizeEmail, isTokenExpired } from '../../../src/cliproxy/auth-utils';
|
||||||
|
|
||||||
|
describe('Auth Utilities', () => {
|
||||||
|
describe('sanitizeEmail', () => {
|
||||||
|
it('should replace @ with underscore', () => {
|
||||||
|
const result = sanitizeEmail('user@example.com');
|
||||||
|
expect(result).not.toContain('@');
|
||||||
|
expect(result).toContain('user_example');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should replace . with underscore', () => {
|
||||||
|
const result = sanitizeEmail('user@example.com');
|
||||||
|
expect(result).not.toContain('.');
|
||||||
|
expect(result).toBe('user_example_com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle multiple dots', () => {
|
||||||
|
const result = sanitizeEmail('user.name@sub.example.com');
|
||||||
|
expect(result).toBe('user_name_sub_example_com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle email without dots in domain', () => {
|
||||||
|
const result = sanitizeEmail('user@localhost');
|
||||||
|
expect(result).toBe('user_localhost');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty string', () => {
|
||||||
|
const result = sanitizeEmail('');
|
||||||
|
expect(result).toBe('');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isTokenExpired', () => {
|
||||||
|
it('should return false for undefined input', () => {
|
||||||
|
const result = isTokenExpired(undefined);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for empty string', () => {
|
||||||
|
const result = isTokenExpired('');
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for past date', () => {
|
||||||
|
const pastDate = new Date(Date.now() - 3600000).toISOString(); // 1 hour ago
|
||||||
|
const result = isTokenExpired(pastDate);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for future date', () => {
|
||||||
|
const futureDate = new Date(Date.now() + 3600000).toISOString(); // 1 hour from now
|
||||||
|
const result = isTokenExpired(futureDate);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for invalid date string', () => {
|
||||||
|
// new Date('invalid') returns Invalid Date, getTime() returns NaN
|
||||||
|
// NaN < Date.now() is false
|
||||||
|
const result = isTokenExpired('not-a-date');
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle ISO date strings', () => {
|
||||||
|
const pastISO = '2020-01-01T00:00:00.000Z';
|
||||||
|
expect(isTokenExpired(pastISO)).toBe(true);
|
||||||
|
|
||||||
|
const futureISO = '2030-01-01T00:00:00.000Z';
|
||||||
|
expect(isTokenExpired(futureISO)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle Unix timestamp strings', () => {
|
||||||
|
// JavaScript Date can parse numeric strings as timestamps
|
||||||
|
const pastTimestamp = String(Date.now() - 86400000); // Yesterday
|
||||||
|
// Note: Date parsing of pure numbers as strings is inconsistent
|
||||||
|
// This test documents the actual behavior
|
||||||
|
const result = isTokenExpired(pastTimestamp);
|
||||||
|
// The behavior depends on how Date parses the string
|
||||||
|
expect(typeof result).toBe('boolean');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,190 @@
|
|||||||
|
/**
|
||||||
|
* Codex Quota Fetcher Unit Tests
|
||||||
|
*
|
||||||
|
* Tests for Codex quota window parsing and transformation logic
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'bun:test';
|
||||||
|
import { buildCodexQuotaWindows } from '../../../src/cliproxy/quota-fetcher-codex';
|
||||||
|
|
||||||
|
describe('Codex Quota Fetcher', () => {
|
||||||
|
describe('buildCodexQuotaWindows', () => {
|
||||||
|
it('should parse snake_case API response', () => {
|
||||||
|
const response = {
|
||||||
|
rate_limit: {
|
||||||
|
primary_window: {
|
||||||
|
used_percent: 25,
|
||||||
|
reset_after_seconds: 3600,
|
||||||
|
},
|
||||||
|
secondary_window: {
|
||||||
|
used_percent: 50,
|
||||||
|
reset_after_seconds: 86400,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const windows = buildCodexQuotaWindows(response);
|
||||||
|
|
||||||
|
expect(windows).toHaveLength(2);
|
||||||
|
expect(windows[0].label).toBe('Primary');
|
||||||
|
expect(windows[0].usedPercent).toBe(25);
|
||||||
|
expect(windows[0].remainingPercent).toBe(75);
|
||||||
|
expect(windows[0].resetAfterSeconds).toBe(3600);
|
||||||
|
expect(windows[1].label).toBe('Secondary');
|
||||||
|
expect(windows[1].usedPercent).toBe(50);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should parse camelCase API response', () => {
|
||||||
|
const response = {
|
||||||
|
rateLimit: {
|
||||||
|
primaryWindow: {
|
||||||
|
usedPercent: 30,
|
||||||
|
resetAfterSeconds: 7200,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const windows = buildCodexQuotaWindows(response);
|
||||||
|
|
||||||
|
expect(windows).toHaveLength(1);
|
||||||
|
expect(windows[0].usedPercent).toBe(30);
|
||||||
|
expect(windows[0].resetAfterSeconds).toBe(7200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle code review rate limits', () => {
|
||||||
|
const response = {
|
||||||
|
code_review_rate_limit: {
|
||||||
|
primary_window: {
|
||||||
|
used_percent: 80,
|
||||||
|
reset_after_seconds: 1800,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const windows = buildCodexQuotaWindows(response);
|
||||||
|
|
||||||
|
expect(windows).toHaveLength(1);
|
||||||
|
expect(windows[0].label).toBe('Code Review (Primary)');
|
||||||
|
expect(windows[0].usedPercent).toBe(80);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clamp usedPercent to 0-100 range', () => {
|
||||||
|
const response = {
|
||||||
|
rate_limit: {
|
||||||
|
primary_window: {
|
||||||
|
used_percent: 150, // Over 100
|
||||||
|
reset_after_seconds: null,
|
||||||
|
},
|
||||||
|
secondary_window: {
|
||||||
|
used_percent: -20, // Negative
|
||||||
|
reset_after_seconds: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const windows = buildCodexQuotaWindows(response);
|
||||||
|
|
||||||
|
expect(windows[0].usedPercent).toBe(100);
|
||||||
|
expect(windows[0].remainingPercent).toBe(0);
|
||||||
|
expect(windows[1].usedPercent).toBe(0);
|
||||||
|
expect(windows[1].remainingPercent).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle null reset_after_seconds', () => {
|
||||||
|
const response = {
|
||||||
|
rate_limit: {
|
||||||
|
primary_window: {
|
||||||
|
used_percent: 10,
|
||||||
|
reset_after_seconds: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const windows = buildCodexQuotaWindows(response);
|
||||||
|
|
||||||
|
expect(windows[0].resetAfterSeconds).toBeNull();
|
||||||
|
expect(windows[0].resetAt).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should calculate resetAt from positive seconds', () => {
|
||||||
|
const response = {
|
||||||
|
rate_limit: {
|
||||||
|
primary_window: {
|
||||||
|
used_percent: 10,
|
||||||
|
reset_after_seconds: 3600, // 1 hour
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const before = Date.now();
|
||||||
|
const windows = buildCodexQuotaWindows(response);
|
||||||
|
const after = Date.now();
|
||||||
|
|
||||||
|
expect(windows[0].resetAt).not.toBeNull();
|
||||||
|
const resetTime = new Date(windows[0].resetAt!).getTime();
|
||||||
|
expect(resetTime).toBeGreaterThanOrEqual(before + 3600000);
|
||||||
|
expect(resetTime).toBeLessThanOrEqual(after + 3600000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not calculate resetAt for zero or negative seconds', () => {
|
||||||
|
const response = {
|
||||||
|
rate_limit: {
|
||||||
|
primary_window: {
|
||||||
|
used_percent: 10,
|
||||||
|
reset_after_seconds: 0,
|
||||||
|
},
|
||||||
|
secondary_window: {
|
||||||
|
used_percent: 20,
|
||||||
|
reset_after_seconds: -100,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const windows = buildCodexQuotaWindows(response);
|
||||||
|
|
||||||
|
expect(windows[0].resetAt).toBeNull();
|
||||||
|
expect(windows[1].resetAt).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array for empty response', () => {
|
||||||
|
const windows = buildCodexQuotaWindows({});
|
||||||
|
expect(windows).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty array for missing rate limit', () => {
|
||||||
|
const response = {
|
||||||
|
plan_type: 'plus',
|
||||||
|
};
|
||||||
|
|
||||||
|
const windows = buildCodexQuotaWindows(response);
|
||||||
|
expect(windows).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle missing window data gracefully', () => {
|
||||||
|
const response = {
|
||||||
|
rate_limit: {
|
||||||
|
primary_window: undefined,
|
||||||
|
secondary_window: null,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const windows = buildCodexQuotaWindows(response as never);
|
||||||
|
expect(windows).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should default usedPercent to 0 when missing', () => {
|
||||||
|
const response = {
|
||||||
|
rate_limit: {
|
||||||
|
primary_window: {
|
||||||
|
reset_after_seconds: 3600,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const windows = buildCodexQuotaWindows(response);
|
||||||
|
|
||||||
|
expect(windows[0].usedPercent).toBe(0);
|
||||||
|
expect(windows[0].remainingPercent).toBe(100);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
/**
|
||||||
|
* Gemini CLI Quota Fetcher Unit Tests
|
||||||
|
*
|
||||||
|
* Tests for Gemini CLI bucket parsing and transformation logic
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { describe, it, expect } from 'bun:test';
|
||||||
|
import {
|
||||||
|
buildGeminiCliBuckets,
|
||||||
|
resolveGeminiCliProjectId,
|
||||||
|
} from '../../../src/cliproxy/quota-fetcher-gemini-cli';
|
||||||
|
|
||||||
|
describe('Gemini CLI Quota Fetcher', () => {
|
||||||
|
describe('resolveGeminiCliProjectId', () => {
|
||||||
|
it('should extract project ID from account field', () => {
|
||||||
|
const account = 'user@example.com (cloudaicompanion-abc-123)';
|
||||||
|
const projectId = resolveGeminiCliProjectId(account);
|
||||||
|
expect(projectId).toBe('cloudaicompanion-abc-123');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return last parenthetical when multiple exist', () => {
|
||||||
|
const account = 'user (org) (cloudaicompanion-xyz-789)';
|
||||||
|
const projectId = resolveGeminiCliProjectId(account);
|
||||||
|
expect(projectId).toBe('cloudaicompanion-xyz-789');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null for account without parentheses', () => {
|
||||||
|
const account = 'user@example.com';
|
||||||
|
const projectId = resolveGeminiCliProjectId(account);
|
||||||
|
expect(projectId).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return null for empty string', () => {
|
||||||
|
const projectId = resolveGeminiCliProjectId('');
|
||||||
|
expect(projectId).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle nested parentheses', () => {
|
||||||
|
const account = 'user@example.com (project-id)';
|
||||||
|
const projectId = resolveGeminiCliProjectId(account);
|
||||||
|
expect(projectId).toBe('project-id');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buildGeminiCliBuckets', () => {
|
||||||
|
it('should group models by series', () => {
|
||||||
|
const rawBuckets = [
|
||||||
|
{ model_id: 'gemini-3-flash-preview', remaining_fraction: 0.8 },
|
||||||
|
{ model_id: 'gemini-2.5-flash', remaining_fraction: 0.6 },
|
||||||
|
{ model_id: 'gemini-3-pro-preview', remaining_fraction: 0.9 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const buckets = buildGeminiCliBuckets(rawBuckets);
|
||||||
|
|
||||||
|
// Should have 2 groups: Flash Series and Pro Series
|
||||||
|
expect(buckets.length).toBeGreaterThanOrEqual(2);
|
||||||
|
|
||||||
|
const flashBucket = buckets.find((b) => b.label === 'Gemini Flash Series');
|
||||||
|
expect(flashBucket).toBeDefined();
|
||||||
|
// Takes minimum remaining fraction (0.6)
|
||||||
|
expect(flashBucket!.remainingFraction).toBe(0.6);
|
||||||
|
expect(flashBucket!.remainingPercent).toBe(60);
|
||||||
|
|
||||||
|
const proBucket = buckets.find((b) => b.label === 'Gemini Pro Series');
|
||||||
|
expect(proBucket).toBeDefined();
|
||||||
|
expect(proBucket!.remainingFraction).toBe(0.9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle camelCase API response', () => {
|
||||||
|
const rawBuckets = [
|
||||||
|
{ modelId: 'gemini-3-flash-preview', remainingFraction: 0.75 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const buckets = buildGeminiCliBuckets(rawBuckets);
|
||||||
|
|
||||||
|
expect(buckets).toHaveLength(1);
|
||||||
|
expect(buckets[0].remainingFraction).toBe(0.75);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clamp remainingFraction to 0-1 range', () => {
|
||||||
|
const rawBuckets = [
|
||||||
|
{ model_id: 'gemini-3-flash-preview', remaining_fraction: 1.5 },
|
||||||
|
{ model_id: 'gemini-3-pro-preview', remaining_fraction: -0.2 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const buckets = buildGeminiCliBuckets(rawBuckets);
|
||||||
|
|
||||||
|
const flashBucket = buckets.find((b) => b.label === 'Gemini Flash Series');
|
||||||
|
expect(flashBucket!.remainingFraction).toBe(1);
|
||||||
|
expect(flashBucket!.remainingPercent).toBe(100);
|
||||||
|
|
||||||
|
const proBucket = buckets.find((b) => b.label === 'Gemini Pro Series');
|
||||||
|
expect(proBucket!.remainingFraction).toBe(0);
|
||||||
|
expect(proBucket!.remainingPercent).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should group by token type', () => {
|
||||||
|
const rawBuckets = [
|
||||||
|
{ model_id: 'gemini-3-flash-preview', token_type: 'input', remaining_fraction: 0.8 },
|
||||||
|
{ model_id: 'gemini-3-flash-preview', token_type: 'output', remaining_fraction: 0.5 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const buckets = buildGeminiCliBuckets(rawBuckets);
|
||||||
|
|
||||||
|
// Should have separate buckets for input and output
|
||||||
|
expect(buckets.length).toBe(2);
|
||||||
|
const inputBucket = buckets.find((b) => b.tokenType === 'input');
|
||||||
|
const outputBucket = buckets.find((b) => b.tokenType === 'output');
|
||||||
|
expect(inputBucket).toBeDefined();
|
||||||
|
expect(outputBucket).toBeDefined();
|
||||||
|
expect(inputBucket!.remainingFraction).toBe(0.8);
|
||||||
|
expect(outputBucket!.remainingFraction).toBe(0.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore deprecated models', () => {
|
||||||
|
const rawBuckets = [
|
||||||
|
{ model_id: 'gemini-2.0-flash-deprecated', remaining_fraction: 0.1 },
|
||||||
|
{ model_id: 'gemini-3-flash-preview', remaining_fraction: 0.9 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const buckets = buildGeminiCliBuckets(rawBuckets);
|
||||||
|
|
||||||
|
// Only gemini-3-flash-preview should be included
|
||||||
|
expect(buckets).toHaveLength(1);
|
||||||
|
expect(buckets[0].remainingFraction).toBe(0.9);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should categorize unknown models as "other"', () => {
|
||||||
|
const rawBuckets = [{ model_id: 'unknown-model-xyz', remaining_fraction: 0.7 }];
|
||||||
|
|
||||||
|
const buckets = buildGeminiCliBuckets(rawBuckets);
|
||||||
|
|
||||||
|
expect(buckets).toHaveLength(1);
|
||||||
|
expect(buckets[0].label).toBe('Other Models');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle empty buckets array', () => {
|
||||||
|
const buckets = buildGeminiCliBuckets([]);
|
||||||
|
expect(buckets).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip buckets with empty model_id', () => {
|
||||||
|
const rawBuckets = [
|
||||||
|
{ model_id: '', remaining_fraction: 0.5 },
|
||||||
|
{ model_id: 'gemini-3-flash-preview', remaining_fraction: 0.8 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const buckets = buildGeminiCliBuckets(rawBuckets);
|
||||||
|
|
||||||
|
expect(buckets).toHaveLength(1);
|
||||||
|
expect(buckets[0].remainingFraction).toBe(0.8);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep earliest reset time when merging', () => {
|
||||||
|
const rawBuckets = [
|
||||||
|
{
|
||||||
|
model_id: 'gemini-3-flash-preview',
|
||||||
|
remaining_fraction: 0.8,
|
||||||
|
reset_time: '2026-01-30T12:00:00Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
model_id: 'gemini-2.5-flash',
|
||||||
|
remaining_fraction: 0.6,
|
||||||
|
reset_time: '2026-01-30T10:00:00Z', // Earlier
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const buckets = buildGeminiCliBuckets(rawBuckets);
|
||||||
|
|
||||||
|
const flashBucket = buckets.find((b) => b.label === 'Gemini Flash Series');
|
||||||
|
expect(flashBucket!.resetTime).toBe('2026-01-30T10:00:00Z');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should default remainingFraction to 1 when missing', () => {
|
||||||
|
const rawBuckets = [{ model_id: 'gemini-3-flash-preview' }];
|
||||||
|
|
||||||
|
const buckets = buildGeminiCliBuckets(rawBuckets);
|
||||||
|
|
||||||
|
expect(buckets[0].remainingFraction).toBe(1);
|
||||||
|
expect(buckets[0].remainingPercent).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should collect modelIds in bucket', () => {
|
||||||
|
const rawBuckets = [
|
||||||
|
{ model_id: 'gemini-3-flash-preview', remaining_fraction: 0.8 },
|
||||||
|
{ model_id: 'gemini-2.5-flash', remaining_fraction: 0.6 },
|
||||||
|
];
|
||||||
|
|
||||||
|
const buckets = buildGeminiCliBuckets(rawBuckets);
|
||||||
|
|
||||||
|
const flashBucket = buckets.find((b) => b.label === 'Gemini Flash Series');
|
||||||
|
expect(flashBucket!.modelIds).toContain('gemini-3-flash-preview');
|
||||||
|
expect(flashBucket!.modelIds).toContain('gemini-2.5-flash');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user