fix(quota): address ghcp review follow-ups

This commit is contained in:
Tam Nhu Tran
2026-02-20 22:53:49 +07:00
parent 2c5b2af55a
commit 8201204380
5 changed files with 257 additions and 12 deletions
+9 -5
View File
@@ -8,9 +8,14 @@
import * as fs from 'node:fs';
import { getAccountTokenPath, getProviderAccounts } from './account-manager';
import type { GhcpQuotaResult, GhcpQuotaSnapshot } from './quota-types';
import { clampPercent } from '../utils/percentage';
const GHCP_USAGE_URL = 'https://api.github.com/copilot_internal/user';
const GHCP_USAGE_TIMEOUT_MS = 10000;
/**
* Mirrors headers currently accepted by GitHub Copilot internal usage endpoint.
* Keep aligned with upstream Copilot client/API changes when quota calls break.
*/
const GHCP_USER_AGENT = 'GitHubCopilotChat/0.26.7';
const GHCP_API_VERSION = '2025-04-01';
@@ -42,11 +47,6 @@ interface TokenData {
};
}
function clampPercent(value: number): number {
if (!Number.isFinite(value)) return 0;
return Math.max(0, Math.min(100, value));
}
function normalizeSnapshot(raw?: RawGhcpQuotaSnapshot): GhcpQuotaSnapshot {
const entitlement = Number(raw?.entitlement ?? 0);
const remainingRaw = raw?.remaining ?? raw?.quota_remaining ?? 0;
@@ -161,6 +161,7 @@ function normalizeUsageResponse(raw: RawGhcpUsageResponse): GhcpQuotaResult {
export async function fetchGhcpQuota(accountId: string, verbose = false): Promise<GhcpQuotaResult> {
const { accessToken, error } = readGhcpAccessToken(accountId);
if (!accessToken) {
// Safe diagnostic: accountId + generic error only (never log token values/file contents).
if (verbose) console.error(`[!] ghcp quota token error (${accountId}): ${error}`);
return buildEmptyQuotaResult(error || 'Failed to load auth token', accountId);
}
@@ -231,3 +232,6 @@ export async function fetchAllGhcpQuotas(
);
return results;
}
// Export for testing
export { normalizeSnapshot as normalizeGhcpSnapshot, extractAccessToken as extractGhcpAccessToken };
+1 -5
View File
@@ -7,6 +7,7 @@
import * as http from 'http';
import type { CopilotQuotaSnapshot, CopilotUsage } from './types';
import { clampPercent } from '../utils/percentage';
interface RawCopilotQuotaSnapshot {
entitlement?: number;
@@ -25,11 +26,6 @@ interface RawCopilotUsage {
};
}
function clampPercent(value: number): number {
if (!Number.isFinite(value)) return 0;
return Math.max(0, Math.min(100, value));
}
function normalizeSnapshot(raw?: RawCopilotQuotaSnapshot): CopilotQuotaSnapshot {
const entitlement = Number(raw?.entitlement ?? 0);
const remaining = Number(raw?.remaining ?? 0);
+7
View File
@@ -0,0 +1,7 @@
/**
* Clamp percentage-like values to a safe 0-100 range.
*/
export function clampPercent(value: number): number {
if (!Number.isFinite(value)) return 0;
return Math.max(0, Math.min(100, value));
}
@@ -0,0 +1,232 @@
/**
* 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);
});
});
});
+8 -2
View File
@@ -590,10 +590,16 @@ export function isGeminiQuotaResult(quota: UnifiedQuotaResult): quota is GeminiC
/** Type guard: Check if quota result is from GitHub Copilot (ghcp) provider */
export function isGhcpQuotaResult(quota: UnifiedQuotaResult): quota is GhcpQuotaResult {
const candidate = quota as GhcpQuotaResult;
const snapshots = candidate.snapshots as Record<string, unknown> | null | undefined;
return (
'snapshots' in quota &&
typeof (quota as GhcpQuotaResult).snapshots === 'object' &&
(quota as GhcpQuotaResult).snapshots !== null
typeof snapshots === 'object' &&
snapshots !== null &&
'premiumInteractions' in snapshots &&
'chat' in snapshots &&
'completions' in snapshots
);
}