Files
ccs/tests/unit/cliproxy/quota-fetcher-ghcp.test.ts
T

233 lines
7.0 KiB
TypeScript

/**
* GitHub Copilot (GHCP) Quota Fetcher Unit Tests
*
* Covers normalization and token extraction edge cases.
*/
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';
import {
normalizeGhcpSnapshot,
extractGhcpAccessToken,
fetchGhcpQuota,
} from '../../../src/cliproxy/quota-fetcher-ghcp';
let tmpDir: string;
let originalCcsHome: string | undefined;
let originalFetch: typeof fetch;
function createGhcpAccount(
accountId: string,
tokenPayload: Record<string, unknown>,
tokenFile = `${accountId}.json`
): void {
const cliproxyDir = path.join(tmpDir, '.ccs', 'cliproxy');
const authDir = path.join(cliproxyDir, 'auth');
fs.mkdirSync(authDir, { recursive: true });
fs.writeFileSync(path.join(authDir, tokenFile), JSON.stringify(tokenPayload));
fs.writeFileSync(
path.join(cliproxyDir, 'accounts.json'),
JSON.stringify(
{
version: 1,
providers: {
ghcp: {
default: accountId,
accounts: {
[accountId]: {
nickname: accountId,
tokenFile,
createdAt: '2026-02-20T00:00:00.000Z',
lastUsedAt: '2026-02-20T00:00:00.000Z',
},
},
},
},
},
null,
2
)
);
}
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-ghcp-quota-test-'));
originalCcsHome = process.env.CCS_HOME;
process.env.CCS_HOME = tmpDir;
originalFetch = global.fetch;
});
afterEach(() => {
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('GHCP Quota Fetcher', () => {
describe('normalizeGhcpSnapshot', () => {
it('handles missing/undefined raw data', () => {
const snapshot = normalizeGhcpSnapshot();
expect(snapshot).toEqual({
entitlement: 0,
remaining: 0,
used: 0,
percentRemaining: 0,
percentUsed: 100,
unlimited: false,
overageCount: 0,
overagePermitted: false,
quotaId: null,
});
});
it('clamps percent_remaining to 0-100 range', () => {
const above = normalizeGhcpSnapshot({
entitlement: 100,
remaining: 80,
percent_remaining: 140,
});
const below = normalizeGhcpSnapshot({
entitlement: 100,
remaining: 80,
percent_remaining: -15,
});
expect(above.percentRemaining).toBe(100);
expect(above.percentUsed).toBe(0);
expect(below.percentRemaining).toBe(0);
expect(below.percentUsed).toBe(100);
});
it('calculates percentRemaining when API does not provide it', () => {
const snapshot = normalizeGhcpSnapshot({
entitlement: 80,
remaining: 20,
});
expect(snapshot.entitlement).toBe(80);
expect(snapshot.remaining).toBe(20);
expect(snapshot.used).toBe(60);
expect(snapshot.percentRemaining).toBe(25);
expect(snapshot.percentUsed).toBe(75);
});
it('handles non-finite entitlement values safely', () => {
const snapshot = normalizeGhcpSnapshot({
entitlement: Number.POSITIVE_INFINITY,
remaining: 25,
});
expect(snapshot.entitlement).toBe(0);
expect(snapshot.remaining).toBe(25);
expect(snapshot.used).toBe(0);
expect(snapshot.percentRemaining).toBe(0);
expect(snapshot.percentUsed).toBe(100);
});
});
describe('extractGhcpAccessToken', () => {
it('extracts from top-level access_token', () => {
const token = extractGhcpAccessToken({
access_token: ' top-level-token ',
});
expect(token).toBe('top-level-token');
});
it('extracts from nested token.access_token', () => {
const token = extractGhcpAccessToken({
token: {
access_token: 'nested-token',
},
});
expect(token).toBe('nested-token');
});
it('returns null for empty/whitespace tokens', () => {
const emptyTopLevel = extractGhcpAccessToken({ access_token: ' ' });
const emptyNested = extractGhcpAccessToken({
token: { access_token: ' ' },
});
expect(emptyTopLevel).toBeNull();
expect(emptyNested).toBeNull();
});
});
describe('fetchGhcpQuota', () => {
it('fetches and normalizes quota for a valid account token', async () => {
createGhcpAccount('ghcp-main', { access_token: 'top-level-token' });
global.fetch = mock((url: string, options?: RequestInit) => {
expect(url).toBe('https://api.github.com/copilot_internal/user');
expect(options?.method).toBe('GET');
expect(options?.headers).toEqual({
Accept: 'application/json',
Authorization: 'token top-level-token',
'User-Agent': 'GitHubCopilotChat/0.26.7',
'x-github-api-version': '2025-04-01',
});
return Promise.resolve(
new Response(
JSON.stringify({
copilot_plan: 'business',
quota_reset_date: '2026-02-28T00:00:00Z',
quota_snapshots: {
premium_interactions: { entitlement: 1000, remaining: 900 },
chat: { entitlement: 500, remaining: 100, percent_remaining: 20 },
completions: { entitlement: 250, remaining: 125 },
},
}),
{
status: 200,
headers: { 'Content-Type': 'application/json' },
}
)
);
}) as typeof fetch;
const result = await fetchGhcpQuota('ghcp-main');
expect(result.success).toBe(true);
expect(result.accountId).toBe('ghcp-main');
expect(result.planType).toBe('business');
expect(result.quotaResetDate).toBe('2026-02-28T00:00:00Z');
expect(result.snapshots.premiumInteractions.percentRemaining).toBe(90);
expect(result.snapshots.chat.percentRemaining).toBe(20);
expect(result.snapshots.completions.percentRemaining).toBe(50);
});
it('returns needsReauth on 401/403 responses', async () => {
createGhcpAccount('ghcp-auth', { access_token: 'token-auth' });
global.fetch = mock(() => Promise.resolve(new Response('', { status: 401 }))) as typeof fetch;
const result = await fetchGhcpQuota('ghcp-auth');
expect(result.success).toBe(false);
expect(result.needsReauth).toBe(true);
expect(result.error).toBe('Authentication expired or invalid');
});
it('fails fast when token file has no valid access token', async () => {
createGhcpAccount('ghcp-missing-token', { access_token: ' ' });
const fetchMock = mock(() => Promise.resolve(new Response('', { status: 200 })));
global.fetch = fetchMock as typeof fetch;
const result = await fetchGhcpQuota('ghcp-missing-token');
expect(result.success).toBe(false);
expect(result.error).toBe('No access token in auth file');
expect(fetchMock).toHaveBeenCalledTimes(0);
});
});
});