mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 08:17:11 +00:00
test(quota): add extensive test suite for quota caching system
- Add quota-response-cache.test.ts (22 tests): - Cache set/get operations - TTL expiration handling - Provider and account isolation - Cache invalidation patterns - High-volume concurrent access - Add quota-caching-integration.test.ts (15 tests): - GeminiCliQuotaResult caching with bucket preservation - CodexQuotaResult caching with window preservation - Cross-provider isolation verification - Error state caching for visibility - needsReauth flag handling Total: 37 new tests for quota caching behavior
This commit is contained in:
@@ -0,0 +1,348 @@
|
||||
/**
|
||||
* Quota Caching Integration Tests
|
||||
*
|
||||
* Tests for quota response caching behavior across providers:
|
||||
* - Cache hit/miss scenarios
|
||||
* - Cache invalidation patterns
|
||||
* - TTL expiration behavior
|
||||
* - Provider isolation
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import {
|
||||
getCachedQuota,
|
||||
setCachedQuota,
|
||||
invalidateQuotaCache,
|
||||
clearQuotaCache,
|
||||
getQuotaCacheStats,
|
||||
QUOTA_CACHE_TTL_MS,
|
||||
} from '../../../src/cliproxy/quota-response-cache';
|
||||
import type { GeminiCliQuotaResult, CodexQuotaResult } from '../../../src/cliproxy/quota-types';
|
||||
|
||||
describe('Quota Caching Integration', () => {
|
||||
beforeEach(() => {
|
||||
clearQuotaCache();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearQuotaCache();
|
||||
});
|
||||
|
||||
describe('GeminiCliQuotaResult caching', () => {
|
||||
const createGeminiQuota = (
|
||||
remainingPercent: number,
|
||||
options: Partial<GeminiCliQuotaResult> = {}
|
||||
): GeminiCliQuotaResult => ({
|
||||
success: true,
|
||||
buckets: [
|
||||
{
|
||||
id: 'gemini-flash-series::combined',
|
||||
label: 'Gemini Flash Series',
|
||||
tokenType: null,
|
||||
remainingFraction: remainingPercent / 100,
|
||||
remainingPercent,
|
||||
resetTime: null,
|
||||
modelIds: ['gemini-3-flash-preview'],
|
||||
},
|
||||
],
|
||||
projectId: 'test-project-123',
|
||||
lastUpdated: Date.now(),
|
||||
accountId: 'test@example.com',
|
||||
...options,
|
||||
});
|
||||
|
||||
it('should cache successful Gemini quota result', () => {
|
||||
const quota = createGeminiQuota(75);
|
||||
setCachedQuota('gemini', 'user@example.com', quota);
|
||||
|
||||
const cached = getCachedQuota<GeminiCliQuotaResult>('gemini', 'user@example.com');
|
||||
expect(cached).not.toBeNull();
|
||||
expect(cached?.success).toBe(true);
|
||||
expect(cached?.buckets[0].remainingPercent).toBe(75);
|
||||
});
|
||||
|
||||
it('should NOT cache quota with needsReauth flag', () => {
|
||||
const quota = createGeminiQuota(0, {
|
||||
success: false,
|
||||
needsReauth: true,
|
||||
error: 'Token expired',
|
||||
});
|
||||
|
||||
// In real usage, we would not cache reauth results
|
||||
// This test verifies the data structure
|
||||
setCachedQuota('gemini', 'user@example.com', quota);
|
||||
const cached = getCachedQuota<GeminiCliQuotaResult>('gemini', 'user@example.com');
|
||||
expect(cached?.needsReauth).toBe(true);
|
||||
});
|
||||
|
||||
it('should preserve all Gemini bucket fields through cache', () => {
|
||||
const quota = createGeminiQuota(50, {
|
||||
buckets: [
|
||||
{
|
||||
id: 'gemini-pro-series::input',
|
||||
label: 'Gemini Pro Series',
|
||||
tokenType: 'input',
|
||||
remainingFraction: 0.5,
|
||||
remainingPercent: 50,
|
||||
resetTime: '2026-01-30T12:00:00Z',
|
||||
modelIds: ['gemini-3-pro-preview', 'gemini-2.5-pro'],
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
setCachedQuota('gemini', 'user@example.com', quota);
|
||||
const cached = getCachedQuota<GeminiCliQuotaResult>('gemini', 'user@example.com');
|
||||
|
||||
expect(cached?.buckets[0].tokenType).toBe('input');
|
||||
expect(cached?.buckets[0].resetTime).toBe('2026-01-30T12:00:00Z');
|
||||
expect(cached?.buckets[0].modelIds).toContain('gemini-3-pro-preview');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CodexQuotaResult caching', () => {
|
||||
const createCodexQuota = (
|
||||
primaryUsed: number,
|
||||
secondaryUsed?: number,
|
||||
options: Partial<CodexQuotaResult> = {}
|
||||
): CodexQuotaResult => ({
|
||||
success: true,
|
||||
windows: [
|
||||
{
|
||||
label: 'Primary',
|
||||
usedPercent: primaryUsed,
|
||||
remainingPercent: 100 - primaryUsed,
|
||||
resetAfterSeconds: 3600,
|
||||
resetAt: new Date(Date.now() + 3600000).toISOString(),
|
||||
},
|
||||
...(secondaryUsed !== undefined
|
||||
? [
|
||||
{
|
||||
label: 'Secondary',
|
||||
usedPercent: secondaryUsed,
|
||||
remainingPercent: 100 - secondaryUsed,
|
||||
resetAfterSeconds: 86400,
|
||||
resetAt: new Date(Date.now() + 86400000).toISOString(),
|
||||
},
|
||||
]
|
||||
: []),
|
||||
],
|
||||
planType: 'plus',
|
||||
lastUpdated: Date.now(),
|
||||
accountId: 'test@example.com',
|
||||
...options,
|
||||
});
|
||||
|
||||
it('should cache successful Codex quota result', () => {
|
||||
const quota = createCodexQuota(30, 10);
|
||||
setCachedQuota('codex', 'user@example.com', quota);
|
||||
|
||||
const cached = getCachedQuota<CodexQuotaResult>('codex', 'user@example.com');
|
||||
expect(cached).not.toBeNull();
|
||||
expect(cached?.success).toBe(true);
|
||||
expect(cached?.windows).toHaveLength(2);
|
||||
expect(cached?.windows[0].usedPercent).toBe(30);
|
||||
});
|
||||
|
||||
it('should preserve planType through cache', () => {
|
||||
const quota = createCodexQuota(20, undefined, { planType: 'team' });
|
||||
setCachedQuota('codex', 'user@example.com', quota);
|
||||
|
||||
const cached = getCachedQuota<CodexQuotaResult>('codex', 'user@example.com');
|
||||
expect(cached?.planType).toBe('team');
|
||||
});
|
||||
|
||||
it('should handle Codex quota with code review limits', () => {
|
||||
const quota: CodexQuotaResult = {
|
||||
success: true,
|
||||
windows: [
|
||||
{
|
||||
label: 'Primary',
|
||||
usedPercent: 25,
|
||||
remainingPercent: 75,
|
||||
resetAfterSeconds: 3600,
|
||||
resetAt: null,
|
||||
},
|
||||
{
|
||||
label: 'Code Review (Primary)',
|
||||
usedPercent: 80,
|
||||
remainingPercent: 20,
|
||||
resetAfterSeconds: 1800,
|
||||
resetAt: null,
|
||||
},
|
||||
],
|
||||
planType: 'plus',
|
||||
lastUpdated: Date.now(),
|
||||
accountId: 'user@example.com',
|
||||
};
|
||||
|
||||
setCachedQuota('codex', 'user@example.com', quota);
|
||||
const cached = getCachedQuota<CodexQuotaResult>('codex', 'user@example.com');
|
||||
|
||||
expect(cached?.windows).toHaveLength(2);
|
||||
const codeReview = cached?.windows.find((w) => w.label.includes('Code Review'));
|
||||
expect(codeReview?.usedPercent).toBe(80);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cross-provider isolation', () => {
|
||||
it('should isolate Gemini and Codex cache for same email', () => {
|
||||
const geminiQuota: GeminiCliQuotaResult = {
|
||||
success: true,
|
||||
buckets: [
|
||||
{
|
||||
id: 'gemini-flash::combined',
|
||||
label: 'Flash',
|
||||
tokenType: null,
|
||||
remainingFraction: 0.9,
|
||||
remainingPercent: 90,
|
||||
resetTime: null,
|
||||
modelIds: [],
|
||||
},
|
||||
],
|
||||
projectId: 'proj',
|
||||
lastUpdated: Date.now(),
|
||||
accountId: 'shared@example.com',
|
||||
};
|
||||
|
||||
const codexQuota: CodexQuotaResult = {
|
||||
success: true,
|
||||
windows: [
|
||||
{
|
||||
label: 'Primary',
|
||||
usedPercent: 10,
|
||||
remainingPercent: 90,
|
||||
resetAfterSeconds: 3600,
|
||||
resetAt: null,
|
||||
},
|
||||
],
|
||||
planType: 'plus',
|
||||
lastUpdated: Date.now(),
|
||||
accountId: 'shared@example.com',
|
||||
};
|
||||
|
||||
setCachedQuota('gemini', 'shared@example.com', geminiQuota);
|
||||
setCachedQuota('codex', 'shared@example.com', codexQuota);
|
||||
|
||||
const cachedGemini = getCachedQuota<GeminiCliQuotaResult>('gemini', 'shared@example.com');
|
||||
const cachedCodex = getCachedQuota<CodexQuotaResult>('codex', 'shared@example.com');
|
||||
|
||||
expect(cachedGemini?.buckets).toBeDefined();
|
||||
expect(cachedCodex?.windows).toBeDefined();
|
||||
expect((cachedGemini as unknown as CodexQuotaResult).windows).toBeUndefined();
|
||||
expect((cachedCodex as unknown as GeminiCliQuotaResult).buckets).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should allow invalidating one provider without affecting others', () => {
|
||||
setCachedQuota('gemini', 'user@example.com', { success: true, buckets: [] } as never);
|
||||
setCachedQuota('codex', 'user@example.com', { success: true, windows: [] } as never);
|
||||
setCachedQuota('agy', 'user@example.com', { success: true, quotas: [] } as never);
|
||||
|
||||
invalidateQuotaCache('gemini', 'user@example.com');
|
||||
|
||||
expect(getCachedQuota('gemini', 'user@example.com')).toBeNull();
|
||||
expect(getCachedQuota('codex', 'user@example.com')).not.toBeNull();
|
||||
expect(getCachedQuota('agy', 'user@example.com')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('cache TTL behavior', () => {
|
||||
it('should use 2-minute TTL by default', () => {
|
||||
expect(QUOTA_CACHE_TTL_MS).toBe(120000);
|
||||
});
|
||||
|
||||
it('should allow custom TTL on retrieval', () => {
|
||||
setCachedQuota('gemini', 'user@example.com', { success: true } as never);
|
||||
|
||||
// With very long TTL, should find it
|
||||
expect(getCachedQuota('gemini', 'user@example.com', 10000000)).not.toBeNull();
|
||||
|
||||
// With 0 TTL, should be expired
|
||||
expect(getCachedQuota('gemini', 'user@example.com', 0)).toBeNull();
|
||||
});
|
||||
|
||||
it('should clean up expired entries lazily on access', async () => {
|
||||
setCachedQuota('gemini', 'user1@example.com', { id: 1 });
|
||||
setCachedQuota('gemini', 'user2@example.com', { id: 2 });
|
||||
|
||||
expect(getQuotaCacheStats().size).toBe(2);
|
||||
|
||||
// Access with very short TTL to trigger expiration cleanup
|
||||
await new Promise((r) => setTimeout(r, 10));
|
||||
getCachedQuota('gemini', 'user1@example.com', 5);
|
||||
|
||||
// Only user1 entry should be deleted (the one we accessed)
|
||||
expect(getQuotaCacheStats().size).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error state caching', () => {
|
||||
it('should cache failed quota results for visibility', () => {
|
||||
const failedQuota: GeminiCliQuotaResult = {
|
||||
success: false,
|
||||
buckets: [],
|
||||
projectId: null,
|
||||
lastUpdated: Date.now(),
|
||||
error: 'Rate limited',
|
||||
accountId: 'user@example.com',
|
||||
};
|
||||
|
||||
setCachedQuota('gemini', 'user@example.com', failedQuota);
|
||||
const cached = getCachedQuota<GeminiCliQuotaResult>('gemini', 'user@example.com');
|
||||
|
||||
expect(cached?.success).toBe(false);
|
||||
expect(cached?.error).toBe('Rate limited');
|
||||
});
|
||||
|
||||
it('should preserve error message through cache round-trip', () => {
|
||||
const errorQuota: CodexQuotaResult = {
|
||||
success: false,
|
||||
windows: [],
|
||||
planType: null,
|
||||
lastUpdated: Date.now(),
|
||||
error: 'API error: 503',
|
||||
accountId: 'user@example.com',
|
||||
};
|
||||
|
||||
setCachedQuota('codex', 'user@example.com', errorQuota);
|
||||
const cached = getCachedQuota<CodexQuotaResult>('codex', 'user@example.com');
|
||||
|
||||
expect(cached?.error).toBe('API error: 503');
|
||||
});
|
||||
});
|
||||
|
||||
describe('high-volume scenarios', () => {
|
||||
it('should handle 50+ accounts efficiently', () => {
|
||||
const numAccounts = 50;
|
||||
const providers = ['gemini', 'codex', 'agy'];
|
||||
|
||||
// Populate cache
|
||||
for (let i = 0; i < numAccounts; i++) {
|
||||
for (const provider of providers) {
|
||||
setCachedQuota(provider, `user${i}@example.com`, {
|
||||
success: true,
|
||||
id: `${provider}-${i}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const stats = getQuotaCacheStats();
|
||||
expect(stats.size).toBe(numAccounts * providers.length);
|
||||
|
||||
// Verify random access
|
||||
const cached = getCachedQuota<{ id: string }>('codex', 'user25@example.com');
|
||||
expect(cached?.id).toBe('codex-25');
|
||||
});
|
||||
|
||||
it('should handle rapid cache updates', () => {
|
||||
const iterations = 100;
|
||||
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
setCachedQuota('gemini', 'user@example.com', { iteration: i });
|
||||
}
|
||||
|
||||
const cached = getCachedQuota<{ iteration: number }>('gemini', 'user@example.com');
|
||||
expect(cached?.iteration).toBe(iterations - 1);
|
||||
expect(getQuotaCacheStats().size).toBe(1); // Only one entry, updated 100 times
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,296 @@
|
||||
/**
|
||||
* Quota Response Cache Unit Tests
|
||||
*
|
||||
* Tests for in-memory quota caching with TTL expiration
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
||||
import {
|
||||
getCachedQuota,
|
||||
setCachedQuota,
|
||||
invalidateQuotaCache,
|
||||
invalidateProviderCache,
|
||||
clearQuotaCache,
|
||||
getQuotaCacheStats,
|
||||
QUOTA_CACHE_TTL_MS,
|
||||
} from '../../../src/cliproxy/quota-response-cache';
|
||||
|
||||
interface TestQuota {
|
||||
success: boolean;
|
||||
buckets: { label: string; remainingPercent: number }[];
|
||||
}
|
||||
|
||||
describe('Quota Response Cache', () => {
|
||||
beforeEach(() => {
|
||||
clearQuotaCache();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearQuotaCache();
|
||||
});
|
||||
|
||||
describe('setCachedQuota and getCachedQuota', () => {
|
||||
it('should store and retrieve quota data', () => {
|
||||
const quota: TestQuota = {
|
||||
success: true,
|
||||
buckets: [{ label: 'Flash', remainingPercent: 80 }],
|
||||
};
|
||||
|
||||
setCachedQuota('gemini', 'user@example.com', quota);
|
||||
const cached = getCachedQuota<TestQuota>('gemini', 'user@example.com');
|
||||
|
||||
expect(cached).not.toBeNull();
|
||||
expect(cached?.success).toBe(true);
|
||||
expect(cached?.buckets[0].remainingPercent).toBe(80);
|
||||
});
|
||||
|
||||
it('should return null for non-existent cache entry', () => {
|
||||
const cached = getCachedQuota('gemini', 'nonexistent@example.com');
|
||||
expect(cached).toBeNull();
|
||||
});
|
||||
|
||||
it('should isolate cache entries by provider', () => {
|
||||
const geminiQuota: TestQuota = { success: true, buckets: [] };
|
||||
const codexQuota = { success: true, windows: [] };
|
||||
|
||||
setCachedQuota('gemini', 'user@example.com', geminiQuota);
|
||||
setCachedQuota('codex', 'user@example.com', codexQuota);
|
||||
|
||||
const cached1 = getCachedQuota<TestQuota>('gemini', 'user@example.com');
|
||||
const cached2 = getCachedQuota<{ windows: unknown[] }>('codex', 'user@example.com');
|
||||
|
||||
expect(cached1?.buckets).toBeDefined();
|
||||
expect(cached2?.windows).toBeDefined();
|
||||
});
|
||||
|
||||
it('should isolate cache entries by account', () => {
|
||||
const quota1: TestQuota = { success: true, buckets: [{ label: 'A', remainingPercent: 50 }] };
|
||||
const quota2: TestQuota = { success: true, buckets: [{ label: 'B', remainingPercent: 90 }] };
|
||||
|
||||
setCachedQuota('gemini', 'user1@example.com', quota1);
|
||||
setCachedQuota('gemini', 'user2@example.com', quota2);
|
||||
|
||||
const cached1 = getCachedQuota<TestQuota>('gemini', 'user1@example.com');
|
||||
const cached2 = getCachedQuota<TestQuota>('gemini', 'user2@example.com');
|
||||
|
||||
expect(cached1?.buckets[0].label).toBe('A');
|
||||
expect(cached2?.buckets[0].label).toBe('B');
|
||||
});
|
||||
|
||||
it('should update existing cache entry', () => {
|
||||
const quota1: TestQuota = { success: true, buckets: [{ label: 'X', remainingPercent: 30 }] };
|
||||
const quota2: TestQuota = { success: true, buckets: [{ label: 'X', remainingPercent: 70 }] };
|
||||
|
||||
setCachedQuota('gemini', 'user@example.com', quota1);
|
||||
setCachedQuota('gemini', 'user@example.com', quota2);
|
||||
|
||||
const cached = getCachedQuota<TestQuota>('gemini', 'user@example.com');
|
||||
expect(cached?.buckets[0].remainingPercent).toBe(70);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cache TTL expiration', () => {
|
||||
it('should return data within TTL', () => {
|
||||
const quota: TestQuota = { success: true, buckets: [] };
|
||||
setCachedQuota('gemini', 'user@example.com', quota);
|
||||
|
||||
// Immediately retrieve (well within TTL)
|
||||
const cached = getCachedQuota<TestQuota>('gemini', 'user@example.com');
|
||||
expect(cached).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for expired cache with custom TTL', () => {
|
||||
const quota: TestQuota = { success: true, buckets: [] };
|
||||
setCachedQuota('gemini', 'user@example.com', quota);
|
||||
|
||||
// Request with 0ms TTL (effectively expired immediately)
|
||||
const cached = getCachedQuota<TestQuota>('gemini', 'user@example.com', 0);
|
||||
expect(cached).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for expired cache entry', async () => {
|
||||
const quota: TestQuota = { success: true, buckets: [] };
|
||||
setCachedQuota('gemini', 'user@example.com', quota);
|
||||
|
||||
// Wait briefly and use very short TTL
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
const cached = getCachedQuota<TestQuota>('gemini', 'user@example.com', 5);
|
||||
expect(cached).toBeNull();
|
||||
});
|
||||
|
||||
it('should delete expired entries on access', async () => {
|
||||
const quota: TestQuota = { success: true, buckets: [] };
|
||||
setCachedQuota('gemini', 'user@example.com', quota);
|
||||
|
||||
// First access should find it
|
||||
const stats1 = getQuotaCacheStats();
|
||||
expect(stats1.size).toBe(1);
|
||||
|
||||
// Access with short TTL should expire and delete
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
getCachedQuota('gemini', 'user@example.com', 5);
|
||||
|
||||
// Entry should be deleted
|
||||
const stats2 = getQuotaCacheStats();
|
||||
expect(stats2.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalidateQuotaCache', () => {
|
||||
it('should invalidate specific account cache', () => {
|
||||
const quota: TestQuota = { success: true, buckets: [] };
|
||||
setCachedQuota('gemini', 'user@example.com', quota);
|
||||
setCachedQuota('gemini', 'other@example.com', quota);
|
||||
|
||||
invalidateQuotaCache('gemini', 'user@example.com');
|
||||
|
||||
expect(getCachedQuota('gemini', 'user@example.com')).toBeNull();
|
||||
expect(getCachedQuota('gemini', 'other@example.com')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should be safe to call on non-existent entry', () => {
|
||||
// Should not throw
|
||||
invalidateQuotaCache('gemini', 'nonexistent@example.com');
|
||||
expect(getCachedQuota('gemini', 'nonexistent@example.com')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalidateProviderCache', () => {
|
||||
it('should invalidate all accounts for a provider', () => {
|
||||
const quota: TestQuota = { success: true, buckets: [] };
|
||||
setCachedQuota('gemini', 'user1@example.com', quota);
|
||||
setCachedQuota('gemini', 'user2@example.com', quota);
|
||||
setCachedQuota('codex', 'user1@example.com', quota);
|
||||
|
||||
invalidateProviderCache('gemini');
|
||||
|
||||
expect(getCachedQuota('gemini', 'user1@example.com')).toBeNull();
|
||||
expect(getCachedQuota('gemini', 'user2@example.com')).toBeNull();
|
||||
expect(getCachedQuota('codex', 'user1@example.com')).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should be safe to call for non-existent provider', () => {
|
||||
// Should not throw
|
||||
invalidateProviderCache('nonexistent');
|
||||
expect(getQuotaCacheStats().size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('clearQuotaCache', () => {
|
||||
it('should clear all cache entries', () => {
|
||||
const quota: TestQuota = { success: true, buckets: [] };
|
||||
setCachedQuota('gemini', 'user1@example.com', quota);
|
||||
setCachedQuota('gemini', 'user2@example.com', quota);
|
||||
setCachedQuota('codex', 'user@example.com', quota);
|
||||
setCachedQuota('agy', 'user@example.com', quota);
|
||||
|
||||
const statsBefore = getQuotaCacheStats();
|
||||
expect(statsBefore.size).toBe(4);
|
||||
|
||||
clearQuotaCache();
|
||||
|
||||
const statsAfter = getQuotaCacheStats();
|
||||
expect(statsAfter.size).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getQuotaCacheStats', () => {
|
||||
it('should return correct cache size', () => {
|
||||
const quota: TestQuota = { success: true, buckets: [] };
|
||||
setCachedQuota('gemini', 'user1@example.com', quota);
|
||||
setCachedQuota('codex', 'user2@example.com', quota);
|
||||
|
||||
const stats = getQuotaCacheStats();
|
||||
expect(stats.size).toBe(2);
|
||||
});
|
||||
|
||||
it('should return cache entry keys', () => {
|
||||
const quota: TestQuota = { success: true, buckets: [] };
|
||||
setCachedQuota('gemini', 'user@example.com', quota);
|
||||
setCachedQuota('codex', 'other@example.com', quota);
|
||||
|
||||
const stats = getQuotaCacheStats();
|
||||
expect(stats.entries).toContain('gemini:user@example.com');
|
||||
expect(stats.entries).toContain('codex:other@example.com');
|
||||
});
|
||||
|
||||
it('should return empty stats for empty cache', () => {
|
||||
const stats = getQuotaCacheStats();
|
||||
expect(stats.size).toBe(0);
|
||||
expect(stats.entries).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('QUOTA_CACHE_TTL_MS constant', () => {
|
||||
it('should be 2 minutes (120000ms)', () => {
|
||||
expect(QUOTA_CACHE_TTL_MS).toBe(2 * 60 * 1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cache key generation', () => {
|
||||
it('should handle special characters in account IDs', () => {
|
||||
const quota: TestQuota = { success: true, buckets: [] };
|
||||
const accountWithPlus = 'user+tag@example.com';
|
||||
const accountWithDots = 'first.last@example.com';
|
||||
|
||||
setCachedQuota('gemini', accountWithPlus, quota);
|
||||
setCachedQuota('gemini', accountWithDots, quota);
|
||||
|
||||
expect(getCachedQuota('gemini', accountWithPlus)).not.toBeNull();
|
||||
expect(getCachedQuota('gemini', accountWithDots)).not.toBeNull();
|
||||
});
|
||||
|
||||
it('should handle empty strings gracefully', () => {
|
||||
const quota: TestQuota = { success: true, buckets: [] };
|
||||
setCachedQuota('', '', quota);
|
||||
|
||||
// Should still work, even if unusual
|
||||
const stats = getQuotaCacheStats();
|
||||
expect(stats.entries).toContain(':');
|
||||
});
|
||||
});
|
||||
|
||||
describe('concurrent access patterns', () => {
|
||||
it('should handle rapid set/get operations', () => {
|
||||
const quota: TestQuota = { success: true, buckets: [] };
|
||||
|
||||
// Simulate rapid updates
|
||||
for (let i = 0; i < 100; i++) {
|
||||
setCachedQuota('gemini', 'user@example.com', {
|
||||
...quota,
|
||||
buckets: [{ label: `iter-${i}`, remainingPercent: i }],
|
||||
});
|
||||
}
|
||||
|
||||
const cached = getCachedQuota<TestQuota>('gemini', 'user@example.com');
|
||||
expect(cached?.buckets[0].label).toBe('iter-99');
|
||||
});
|
||||
|
||||
it('should handle multiple providers simultaneously', () => {
|
||||
const providers = ['gemini', 'codex', 'agy'];
|
||||
const accounts = ['user1@example.com', 'user2@example.com'];
|
||||
const quota: TestQuota = { success: true, buckets: [] };
|
||||
|
||||
// Set cache for all combinations
|
||||
for (const provider of providers) {
|
||||
for (const account of accounts) {
|
||||
setCachedQuota(provider, account, {
|
||||
...quota,
|
||||
buckets: [{ label: `${provider}-${account}`, remainingPercent: 50 }],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const stats = getQuotaCacheStats();
|
||||
expect(stats.size).toBe(6);
|
||||
|
||||
// Verify all entries exist
|
||||
for (const provider of providers) {
|
||||
for (const account of accounts) {
|
||||
const cached = getCachedQuota<TestQuota>(provider, account);
|
||||
expect(cached?.buckets[0].label).toBe(`${provider}-${account}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -144,6 +144,8 @@ export interface QuotaResult {
|
||||
isForbidden?: boolean;
|
||||
/** Error message if fetch failed */
|
||||
error?: string;
|
||||
/** True if token is expired and needs re-authentication */
|
||||
needsReauth?: boolean;
|
||||
}
|
||||
|
||||
/** Codex rate limit window */
|
||||
|
||||
Reference in New Issue
Block a user