diff --git a/src/cliproxy/quota-fetcher.ts b/src/cliproxy/quota-fetcher.ts index 551c0889..06e7c392 100644 --- a/src/cliproxy/quota-fetcher.ts +++ b/src/cliproxy/quota-fetcher.ts @@ -698,6 +698,14 @@ async function fetchAvailableModels( error: 'Invalid quota response from provider', errorCode: 'provider_unavailable', retryable: true, + entitlement: buildProviderEntitlementEvidence({ + normalizedTier: 'unknown', + source: 'runtime_inference', + confidence: 'low', + accessState: 'unknown', + capacityState: 'temporarily_unavailable', + notes: 'Provider returned a 2xx response with an empty or invalid quota payload.', + }), }; } @@ -814,6 +822,7 @@ export async function fetchAccountQuota( httpStatus: lastProjectResult.httpStatus, needsReauth: lastProjectResult.needsReauth, isUnprovisioned: lastProjectResult.isUnprovisioned, + entitlement: lastProjectResult.entitlement, isExpired: authData.isExpired, expiresAt: authData.expiresAt || undefined, }; diff --git a/tests/unit/cliproxy/quota-fetcher-antigravity-failure.test.ts b/tests/unit/cliproxy/quota-fetcher-antigravity-failure.test.ts index 6f79048a..817e7f3a 100644 --- a/tests/unit/cliproxy/quota-fetcher-antigravity-failure.test.ts +++ b/tests/unit/cliproxy/quota-fetcher-antigravity-failure.test.ts @@ -28,4 +28,58 @@ describe('Antigravity quota failure metadata', () => { capacityState: 'rate_limited', }); }); + + it('preserves entitlement evidence when project lookup fails before quota fetch', async () => { + const moduleId = Date.now() + Math.random(); + const { fetchAccountQuota } = await import(`../../../src/cliproxy/quota-fetcher?agy-early=${moduleId}`); + const { getProviderAuthDir } = await import( + `../../../src/cliproxy/config-generator?agy-config=${moduleId}` + ); + const fs = await import('node:fs'); + const os = await import('node:os'); + const path = await import('node:path'); + + const tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-agy-failure-')); + const originalCcsHome = process.env.CCS_HOME; + process.env.CCS_HOME = tempHome; + + try { + const authDir = getProviderAuthDir('agy'); + fs.mkdirSync(authDir, { recursive: true }); + fs.writeFileSync( + path.join(authDir, 'antigravity-user@example.com.json'), + JSON.stringify({ + type: 'antigravity', + email: 'user@example.com', + project_id: 'project-x', + access_token: 'token', + }) + ); + + const originalFetch = globalThis.fetch; + globalThis.fetch = (async () => + new Response(JSON.stringify({ error: { message: 'forbidden' } }), { + status: 403, + headers: { 'Content-Type': 'application/json' }, + })) as typeof fetch; + + try { + const result = await fetchAccountQuota('agy', 'user@example.com'); + expect(result.success).toBe(false); + expect(result.entitlement).toMatchObject({ + accessState: 'not_entitled', + capacityState: 'unknown', + }); + } finally { + globalThis.fetch = originalFetch; + } + } finally { + if (originalCcsHome === undefined) { + delete process.env.CCS_HOME; + } else { + process.env.CCS_HOME = originalCcsHome; + } + fs.rmSync(tempHome, { recursive: true, force: true }); + } + }); });