/** * Codex Quota Fetcher Unit Tests * * Tests for Codex quota window parsing and transformation logic */ import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'; import * as fs from 'node:fs'; import * as os from 'node:os'; import * as path from 'node:path'; let tmpDir: string; let originalCcsHome: string | undefined; let originalFetch: typeof fetch; let moduleVersion = 0; let buildCodexQuotaWindows: typeof import('../../../src/cliproxy/quota-fetcher-codex').buildCodexQuotaWindows; let buildCodexCoreUsageSummary: typeof import('../../../src/cliproxy/quota-fetcher-codex').buildCodexCoreUsageSummary; let fetchCodexQuota: typeof import('../../../src/cliproxy/quota-fetcher-codex').fetchCodexQuota; let getUnknownCodexWindowLabels: typeof import('../../../src/cliproxy/quota-fetcher-codex').getUnknownCodexWindowLabels; let registerAccount: typeof import('../../../src/cliproxy/account-manager').registerAccount; function createCodexAccount( accountId: string, tokenPayload: Record, tokenFile = `codex-${accountId.replace(/[@.]/g, '_')}.json` ): void { const authDir = path.join(tmpDir, '.ccs', 'cliproxy', 'auth'); fs.mkdirSync(authDir, { recursive: true }); fs.writeFileSync(path.join(authDir, tokenFile), JSON.stringify(tokenPayload)); } beforeEach(async () => { moduleVersion += 1; mock.restore(); const configGenerator = await import( `../../../src/cliproxy/config-generator?codex-config-generator=${moduleVersion}` ); const accountManager = await import( `../../../src/cliproxy/account-manager?codex-account-manager=${moduleVersion}` ); mock.module('../../../src/cliproxy/config-generator', () => configGenerator); mock.module('../../../src/cliproxy/account-manager', () => accountManager); ({ registerAccount } = accountManager); ({ buildCodexQuotaWindows, buildCodexCoreUsageSummary, fetchCodexQuota, getUnknownCodexWindowLabels, } = await import(`../../../src/cliproxy/quota-fetcher-codex?codex-fetcher=${moduleVersion}`)); tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-codex-quota-test-')); originalCcsHome = process.env.CCS_HOME; process.env.CCS_HOME = tmpDir; originalFetch = global.fetch; }); afterEach(() => { mock.restore(); global.fetch = originalFetch; if (originalCcsHome !== undefined) { process.env.CCS_HOME = originalCcsHome; } else { delete process.env.CCS_HOME; } fs.rmSync(tmpDir, { recursive: true, force: true }); }); 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); }); }); describe('buildCodexCoreUsageSummary', () => { it('extracts 5h and weekly windows from labeled usage windows', () => { const windows = buildCodexQuotaWindows({ rate_limit: { primary_window: { used_percent: 35, reset_after_seconds: 18000, }, secondary_window: { used_percent: 60, reset_after_seconds: 604800, }, }, }); const summary = buildCodexCoreUsageSummary(windows); expect(summary.fiveHour?.label).toBe('Primary'); expect(summary.fiveHour?.remainingPercent).toBe(65); expect(summary.fiveHour?.resetAfterSeconds).toBe(18000); expect(summary.weekly?.label).toBe('Secondary'); expect(summary.weekly?.remainingPercent).toBe(40); expect(summary.weekly?.resetAfterSeconds).toBe(604800); }); it('falls back to shortest and longest reset windows when labels are unknown', () => { const windows = [ { label: 'Window A', usedPercent: 20, remainingPercent: 80, resetAfterSeconds: 18000, resetAt: '2026-02-15T15:00:00Z', }, { label: 'Window B', usedPercent: 45, remainingPercent: 55, resetAfterSeconds: 604800, resetAt: '2026-02-21T10:00:00Z', }, { label: 'Code Review (Primary)', usedPercent: 10, remainingPercent: 90, resetAfterSeconds: 3600, resetAt: '2026-02-15T11:00:00Z', }, ]; const summary = buildCodexCoreUsageSummary(windows); expect(summary.fiveHour?.label).toBe('Window A'); expect(summary.fiveHour?.resetAfterSeconds).toBe(18000); expect(summary.weekly?.label).toBe('Window B'); expect(summary.weekly?.resetAfterSeconds).toBe(604800); }); it('returns null summaries when no windows are available', () => { const summary = buildCodexCoreUsageSummary([]); expect(summary.fiveHour).toBeNull(); expect(summary.weekly).toBeNull(); }); }); describe('getUnknownCodexWindowLabels', () => { it('returns unknown labels and de-duplicates them', () => { const labels = getUnknownCodexWindowLabels([ { label: 'Window A', usedPercent: 1, remainingPercent: 99, resetAfterSeconds: 10, resetAt: null, }, { label: 'Window A', usedPercent: 2, remainingPercent: 98, resetAfterSeconds: 20, resetAt: null, }, { label: 'Primary', usedPercent: 3, remainingPercent: 97, resetAfterSeconds: 30, resetAt: null, }, ]); expect(labels).toEqual(['Window A']); }); it('returns empty array when all labels are recognized', () => { const labels = getUnknownCodexWindowLabels([ { label: 'Primary', usedPercent: 10, remainingPercent: 90, resetAfterSeconds: 100, resetAt: null, }, { label: 'Code Review (Secondary)', usedPercent: 20, remainingPercent: 80, resetAfterSeconds: 200, resetAt: null, }, ]); expect(labels).toEqual([]); }); }); describe('fetchCodexQuota failure mapping', () => { function createValidCodexAccount( email: string, accountId = `workspace-${email}`, tokenFile?: string ): void { createCodexAccount(email, { access_token: 'test-token', account_id: accountId, expired: '2099-01-01T00:00:00.000Z', email, type: 'codex', }, tokenFile); } it('maps deactivated workspace 402 responses to structured metadata', async () => { createValidCodexAccount('workspace@example.com', 'workspace-123'); global.fetch = mock(() => Promise.resolve( new Response(JSON.stringify({ detail: { code: 'deactivated_workspace' } }), { status: 402, headers: { 'Content-Type': 'application/json' }, }) ) ) as typeof fetch; const result = await fetchCodexQuota('workspace@example.com'); expect(result.success).toBe(false); expect(result.httpStatus).toBe(402); expect(result.errorCode).toBe('deactivated_workspace'); expect(result.error).toContain('Workspace deactivated'); expect(result.actionHint).toContain('active ChatGPT workspace'); expect(result.retryable).toBe(false); }); it('maps 401 responses to reauth-required metadata', async () => { createValidCodexAccount('reauth@example.com', 'workspace-reauth'); global.fetch = mock(() => Promise.resolve(new Response('', { status: 401 }))) as typeof fetch; const result = await fetchCodexQuota('reauth@example.com'); expect(result.success).toBe(false); expect(result.httpStatus).toBe(401); expect(result.errorCode).toBe('reauth_required'); expect(result.needsReauth).toBe(true); expect(result.actionHint).toContain('ccs cliproxy auth codex'); }); it('uses the registry token file for duplicate-email Codex accounts', async () => { createValidCodexAccount( 'kaidu.kd@gmail.com', 'workspace-team', 'codex-04a0f049-kaidu.kd@gmail.com-team.json' ); createValidCodexAccount( 'kaidu.kd@gmail.com', 'workspace-free', 'codex-kaidu.kd@gmail.com-free.json' ); registerAccount( 'codex', 'codex-04a0f049-kaidu.kd@gmail.com-team.json', 'kaidu.kd@gmail.com' ); const freeAccount = registerAccount( 'codex', 'codex-kaidu.kd@gmail.com-free.json', 'kaidu.kd@gmail.com' ); const fetchSpy = mock((input: RequestInfo | URL, init?: RequestInit) => Promise.resolve( new Response( JSON.stringify({ plan_type: 'free', rate_limit: { primary_window: { used_percent: 10, reset_after_seconds: 3600 }, }, }), { status: 200, headers: { 'Content-Type': 'application/json' }, } ) ) ) as typeof fetch; global.fetch = fetchSpy; const result = await fetchCodexQuota(freeAccount.id); const requestInit = fetchSpy.mock.calls[0]?.[1] as RequestInit | undefined; const headers = new Headers(requestInit?.headers); expect(result.success).toBe(true); expect(headers.get('ChatGPT-Account-Id')).toBe('workspace-free'); }); it('does not guess a duplicate-email Codex auth file when the registry entry is missing', async () => { createValidCodexAccount( 'kaidu.kd@gmail.com', 'workspace-team', 'codex-legacy-slot-a.json' ); createValidCodexAccount( 'kaidu.kd@gmail.com', 'workspace-free', 'codex-legacy-slot-b.json' ); const fetchSpy = mock(() => Promise.resolve( new Response( JSON.stringify({ plan_type: 'free', rate_limit: { primary_window: { used_percent: 10, reset_after_seconds: 3600 }, }, }), { status: 200, headers: { 'Content-Type': 'application/json' }, } ) ) ) as typeof fetch; global.fetch = fetchSpy; const result = await fetchCodexQuota('kaidu.kd@gmail.com#04a0f049-team'); expect(result.success).toBe(false); expect(result.errorCode).toBe('auth_file_missing'); expect(fetchSpy).not.toHaveBeenCalled(); }); it('maps 403 responses to forbidden metadata', async () => { createValidCodexAccount('forbidden@example.com', 'workspace-forbidden'); global.fetch = mock(() => Promise.resolve( new Response(JSON.stringify({ detail: { code: 'quota_api_forbidden' } }), { status: 403, headers: { 'Content-Type': 'application/json' }, }) ) ) as typeof fetch; const result = await fetchCodexQuota('forbidden@example.com'); expect(result.success).toBe(false); expect(result.httpStatus).toBe(403); expect(result.errorCode).toBe('quota_api_forbidden'); expect(result.isForbidden).toBe(true); expect(result.retryable).toBe(false); }); it('maps 429 responses to retryable rate-limit metadata', async () => { createValidCodexAccount('rate-limit@example.com', 'workspace-rate-limit'); global.fetch = mock(() => Promise.resolve( new Response(JSON.stringify({ detail: { code: 'rate_limited' } }), { status: 429, headers: { 'Content-Type': 'application/json' }, }) ) ) as typeof fetch; const result = await fetchCodexQuota('rate-limit@example.com'); expect(result.success).toBe(false); expect(result.httpStatus).toBe(429); expect(result.errorCode).toBe('rate_limited'); expect(result.retryable).toBe(true); expect(result.actionHint).toContain('Retry'); }); it('maps 5xx responses to retryable provider-unavailable metadata', async () => { createValidCodexAccount('outage@example.com', 'workspace-outage'); global.fetch = mock(() => Promise.resolve( new Response(JSON.stringify({ detail: { code: 'upstream_failure' } }), { status: 503, headers: { 'Content-Type': 'application/json' }, }) ) ) as typeof fetch; const result = await fetchCodexQuota('outage@example.com'); expect(result.success).toBe(false); expect(result.httpStatus).toBe(503); expect(result.errorCode).toBe('upstream_failure'); expect(result.retryable).toBe(true); expect(result.error).toContain('service unavailable'); }); it('maps unknown upstream statuses to a non-retryable structured error', async () => { createValidCodexAccount('teapot@example.com', 'workspace-teapot'); global.fetch = mock(() => Promise.resolve( new Response('{"message":"Strange upstream response"}', { status: 418, headers: { 'Content-Type': 'application/json' }, }) ) ) as typeof fetch; const result = await fetchCodexQuota('teapot@example.com'); expect(result.success).toBe(false); expect(result.httpStatus).toBe(418); expect(result.errorCode).toBe('unknown_upstream_error'); expect(result.retryable).toBe(false); expect(result.error).toBe('Strange upstream response'); }); it('sanitizes and truncates raw upstream error detail before returning it', async () => { createValidCodexAccount('sanitized@example.com', 'workspace-sanitized'); const leakedToken = 'secret-token-value-123'; const oversizedMessage = 'x'.repeat(400); global.fetch = mock(() => Promise.resolve( new Response( JSON.stringify({ message: 'Upstream failure', access_token: leakedToken, extra: oversizedMessage, }), { status: 418, headers: { 'Content-Type': 'application/json' }, } ) ) ) as typeof fetch; const result = await fetchCodexQuota('sanitized@example.com'); expect(result.success).toBe(false); expect(result.errorDetail).toBeDefined(); expect(result.errorDetail).not.toContain(leakedToken); expect(result.errorDetail).toContain('[redacted]'); expect(result.errorDetail?.endsWith('...[truncated]')).toBe(true); }); it('omits raw HTML upstream bodies from the returned error detail', async () => { createValidCodexAccount('html@example.com', 'workspace-html'); global.fetch = mock(() => Promise.resolve( new Response('bad gateway', { status: 503, headers: { 'Content-Type': 'text/html' }, }) ) ) as typeof fetch; const result = await fetchCodexQuota('html@example.com'); expect(result.success).toBe(false); expect(result.errorDetail).toBe('[HTML error response omitted]'); }); }); });