diff --git a/src/lib/xai.ts b/src/lib/xai.ts index a0d9102..e2b7ffc 100644 --- a/src/lib/xai.ts +++ b/src/lib/xai.ts @@ -11,7 +11,7 @@ import { sanitizeSingleLineDisplaySnippet } from "./display-sanitize.js"; import { clampPercent } from "./format-utils.js"; import { fetchWithTimeout } from "./http.js"; -import { readAuthFileCached } from "./opencode-auth.js"; +import { readAuthFile, readAuthFileCached } from "./opencode-auth.js"; import type { AuthData, QuotaError } from "./types.js"; export const DEFAULT_XAI_AUTH_CACHE_MAX_AGE_MS = 5_000; @@ -150,10 +150,10 @@ function safeErrorText(message: string, accessToken: string): string { export async function queryXaiQuota( options: { requestTimeoutMs?: number } = {}, ): Promise { - const auth = await readAuthFileCached({ - maxAgeMs: DEFAULT_XAI_AUTH_CACHE_MAX_AGE_MS, - }); - const resolvedAuth = resolveXaiOAuth(auth); + // OpenCode can replace this OAuth entry while servicing a model request. + // Read the file directly so a post-request quota fetch cannot reuse the + // token snapshot from before that refresh. + const resolvedAuth = resolveXaiOAuth(await readAuthFile()); if (resolvedAuth.state !== "configured") return null; if (resolvedAuth.expiresAt !== undefined && resolvedAuth.expiresAt <= Date.now()) { diff --git a/tests/lib.xai.test.ts b/tests/lib.xai.test.ts index bffe0a8..25ac811 100644 --- a/tests/lib.xai.test.ts +++ b/tests/lib.xai.test.ts @@ -4,12 +4,13 @@ import superGrokWeeklyFixture from "./fixtures/xai/supergrok-weekly.json"; import { hasXaiOAuth, periodKindLabel, queryXaiQuota, resolveXaiOAuth } from "../src/lib/xai.js"; vi.mock("../src/lib/opencode-auth.js", () => ({ + readAuthFile: vi.fn(), readAuthFileCached: vi.fn(), })); async function mockConfiguredAuth(overrides: Record = {}): Promise { - const { readAuthFileCached } = await import("../src/lib/opencode-auth.js"); - (readAuthFileCached as any).mockResolvedValueOnce({ + const { readAuthFile } = await import("../src/lib/opencode-auth.js"); + (readAuthFile as any).mockResolvedValueOnce({ xai: { type: "oauth", access: "token-1", @@ -63,8 +64,8 @@ describe("queryXaiQuota", () => { }); it("returns null for missing, wrong-type, and incomplete auth", async () => { - const { readAuthFileCached } = await import("../src/lib/opencode-auth.js"); - (readAuthFileCached as any) + const { readAuthFile } = await import("../src/lib/opencode-auth.js"); + (readAuthFile as any) .mockResolvedValueOnce({}) .mockResolvedValueOnce({ xai: { type: "api", key: "xai-key" } }) .mockResolvedValueOnce({ xai: { type: "oauth", refresh: "refresh-only" } }); @@ -75,8 +76,8 @@ describe("queryXaiQuota", () => { }); it("does not refresh, fetch, or write an expired token", async () => { - const { readAuthFileCached } = await import("../src/lib/opencode-auth.js"); - (readAuthFileCached as any).mockResolvedValueOnce({ + const { readAuthFile, readAuthFileCached } = await import("../src/lib/opencode-auth.js"); + (readAuthFile as any).mockResolvedValueOnce({ xai: { type: "oauth", access: "expired-token", @@ -91,10 +92,38 @@ describe("queryXaiQuota", () => { success: false, error: "xAI OAuth token expired; use xAI in OpenCode to refresh it or reconnect xAI", }); - expect(readAuthFileCached).toHaveBeenCalledOnce(); + expect(readAuthFile).toHaveBeenCalledOnce(); + expect(readAuthFileCached).not.toHaveBeenCalled(); expect(fetchMock).not.toHaveBeenCalled(); }); + it("reads the refreshed OAuth entry instead of a stale cached token", async () => { + const { readAuthFile, readAuthFileCached } = await import("../src/lib/opencode-auth.js"); + (readAuthFileCached as any).mockResolvedValueOnce({ + xai: { type: "oauth", access: "stale-token", expires: Date.now() - 1_000 }, + }); + (readAuthFile as any).mockResolvedValueOnce({ + xai: { type: "oauth", access: "fresh-token", expires: Date.now() + 60_000 }, + }); + const fetchMock = vi.fn( + async () => new Response(JSON.stringify(superGrokWeeklyFixture), { status: 200 }), + ) as any; + vi.stubGlobal("fetch", fetchMock); + + await expect(queryXaiQuota()).resolves.toMatchObject({ + success: true, + window: { percentRemaining: 95, kind: "weekly" }, + }); + expect(readAuthFile).toHaveBeenCalledOnce(); + expect(readAuthFileCached).not.toHaveBeenCalled(); + expect(fetchMock).toHaveBeenCalledWith( + "https://cli-chat-proxy.grok.com/v1/billing?format=credits", + expect.objectContaining({ + headers: expect.objectContaining({ Authorization: "Bearer fresh-token" }), + }), + ); + }); + it("maps the exact PR fixture through one fixed authenticated GET", async () => { await mockConfiguredAuth();