mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 12:15:57 +00:00
233 lines
7.0 KiB
TypeScript
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);
|
|
});
|
|
});
|
|
});
|