diff --git a/packages/coding-agent/test/account-inventory-usage.test.ts b/packages/coding-agent/test/account-inventory-usage.test.ts index b6aba5c1e6..b88b2d4ad2 100644 --- a/packages/coding-agent/test/account-inventory-usage.test.ts +++ b/packages/coding-agent/test/account-inventory-usage.test.ts @@ -1,10 +1,11 @@ -import { describe, expect, it } from "bun:test"; +import { afterEach, describe, expect, it, vi } from "bun:test"; import type { AuthStorage, CachedUsageReport, CredentialHealthResult, CredentialInventoryRecord, } from "@gajae-code/ai/core"; +import * as aiCore from "@gajae-code/ai/core"; import { buildAccountInventorySnapshot, checkAccountInventory } from "../src/session/account-inventory"; const NOW = 1_700_000_000_000; @@ -49,10 +50,36 @@ function makeAuthStorage(overrides: Partial = {}): AuthStorage { hasConfigApiKey: () => false, getEffectiveCredentialType: () => "oauth", getGeneration: () => 1, + checkCredentials: async () => [], + // An exported provider key in the operator's shell makes the inventory add a + // synthetic env row, which exercises these hooks. Stubbing them keeps the + // test hermetic instead of passing only in a key-free environment. + peekCachedCredentialHealthForSource: () => ({ status: "unknown", reason: null }), + recordCredentialHealthForSource: () => undefined, + peekApiKey: async () => undefined, + checkApiKeyCredential: async () => ({ provider: "openai-codex", type: "api_key", ok: null }), ...overrides, } as unknown as AuthStorage; } +/** Synthetic credential constant. Never a real credential. */ +const CODEX_ENV_KEY = "test-env-row-token"; + +/** + * Make env-key resolution deterministic for one test: getEnvApiKey is spied at + * the @gajae-code/ai/core boundary and restored after each test, so the suite + * never reads the inherited credential snapshot, agent/user `.env` files, or + * shell startup files — regardless of what the host machine carries — and never + * mutates process.env. + */ +function stubEnvKey(provider: string, key: string | undefined): void { + vi.spyOn(aiCore, "getEnvApiKey").mockImplementation(resolved => (resolved === provider ? key : undefined)); +} + +afterEach(() => { + vi.restoreAllMocks(); +}); + const modelRegistry = { getAvailable: () => [{ provider: "openai-codex" }], getProviderBaseUrl: () => BASE_URL, @@ -60,6 +87,9 @@ const modelRegistry = { describe("account inventory usage", () => { it("uses the provider base URL to retrieve cached usage for a stored credential", () => { + // A resolvable provider env key must not disturb the stored credential's + // cached usage lookup even though it adds a synthetic row. + stubEnvKey("openai-codex", CODEX_ENV_KEY); let receivedBaseUrl: string | undefined; const cached: CachedUsageReport = { report: usageReport(), @@ -76,12 +106,14 @@ describe("account inventory usage", () => { }); const snapshot = buildAccountInventorySnapshot({ authStorage, modelRegistry, nowMs: NOW }); + const stored = snapshot.rows.find(row => row.source === "stored" && row.provider === "openai-codex"); expect(receivedBaseUrl).toBe(BASE_URL); - expect(snapshot.rows[0]?.usage?.report.limits[0]?.label).toBe("7 days"); + expect(stored?.usage?.report.limits[0]?.label).toBe("7 days"); }); it("attaches a fresh check report directly when the persistent cache cannot be read back", async () => { + stubEnvKey("openai-codex", CODEX_ENV_KEY); const result: CredentialHealthResult = { id: 1, provider: "openai-codex", @@ -95,9 +127,27 @@ describe("account inventory usage", () => { }); const snapshot = await checkAccountInventory({ authStorage, modelRegistry, nowMs: NOW }); + const stored = snapshot.rows.find(row => row.source === "stored" && row.provider === "openai-codex"); + + expect(stored?.health.status).toBe("ok"); + expect(stored?.capabilities.hasCachedUsage).toBe(true); + expect(stored?.usage?.report.limits[0]?.amount.used).toBe(24); + }); + + it("keeps the suite green on a credential-free host by stubbing the synthetic env row", () => { + // CI runners resolve no provider credentials, so the synthetic-row path + // would otherwise never execute there. The spy guarantees the row exists + // (and exercises the source-health hooks) on any host. + stubEnvKey("openai-codex", CODEX_ENV_KEY); + const snapshot = buildAccountInventorySnapshot({ + authStorage: makeAuthStorage(), + modelRegistry, + nowMs: NOW, + }); + const env = snapshot.rows.find(row => row.source === "env" && row.provider === "openai-codex"); - expect(snapshot.rows[0]?.health.status).toBe("ok"); - expect(snapshot.rows[0]?.capabilities.hasCachedUsage).toBe(true); - expect(snapshot.rows[0]?.usage?.report.limits[0]?.amount.used).toBe(24); + expect(env).toBeDefined(); + // Row payloads never carry the key bytes. + expect(JSON.stringify(snapshot.rows).includes(CODEX_ENV_KEY)).toBe(false); }); });