From b65bc0bbb410f62a0d3129ccfb614c9944786a8d Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Fri, 31 Jul 2026 19:18:49 -0700 Subject: [PATCH] FN-8650: preserve Grok CLI authentication status Keep unavailable Grok CLI billing data distinct from authentication expiry. - Preserve billing outcomes for usage windows, missing data, HTTP responses, and transport failures. - Mark CLI authentication expired only after observed 401 or 403 responses. - Cover unmeterable authenticated accounts in usage and indicator tests. - Add a patch changeset for the corrected indicator behavior. Files changed: .changeset/fn-8650-grok-cli-auth-expired.md | 7 ++ .../components/__tests__/UsageIndicator.test.tsx | 16 +++++ packages/dashboard/src/__tests__/usage.test.ts | 79 ++++++++++++++++++++-- packages/dashboard/src/usage.ts | 57 ++++++++++++---- 4 files changed, 139 insertions(+), 20 deletions(-) Fusion-Task-Id: FN-8650 Fusion-Task-Lineage: f18cceb6-1092-4073-b540-9c7a5434fbcd Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-8650-grok-cli-auth-expired.md | 7 ++ .../__tests__/UsageIndicator.test.tsx | 16 ++++ .../dashboard/src/__tests__/usage.test.ts | 79 +++++++++++++++++-- packages/dashboard/src/usage.ts | 57 +++++++++---- 4 files changed, 139 insertions(+), 20 deletions(-) create mode 100644 .changeset/fn-8650-grok-cli-auth-expired.md diff --git a/.changeset/fn-8650-grok-cli-auth-expired.md b/.changeset/fn-8650-grok-cli-auth-expired.md new file mode 100644 index 0000000000..430defdd31 --- /dev/null +++ b/.changeset/fn-8650-grok-cli-auth-expired.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Stop reporting a working Grok CLI login as expired auth in the Usage indicator. +category: fix +dev: Grok billing fetches now preserve auth, no-data, HTTP, and transport outcomes. diff --git a/packages/dashboard/app/components/__tests__/UsageIndicator.test.tsx b/packages/dashboard/app/components/__tests__/UsageIndicator.test.tsx index 3f30547c63..e9867ef106 100644 --- a/packages/dashboard/app/components/__tests__/UsageIndicator.test.tsx +++ b/packages/dashboard/app/components/__tests__/UsageIndicator.test.tsx @@ -185,6 +185,22 @@ describe("UsageIndicator", () => { expect(screen.getByText("Hourly")).toBeInTheDocument(); }); + it("renders an authenticated provider with no windows without an error badge or meter", () => { + mockUseUsageData.mockReturnValue(createUsageDataState({ + providers: [{ name: "Grok", icon: "✖️", status: "ok", windows: [] }], + lastUpdated: new Date(), + refresh: mockRefresh, + })); + + render(); + + const card = document.querySelector('[data-provider="Grok"]')!; + expect(card).toHaveAttribute("data-status", "ok"); + expect(card.querySelector(".usage-status-badge--error")).toBeNull(); + expect(card.querySelector(".usage-provider-windows")).toBeNull(); + expect(screen.getByText("No usage data available")).toBeInTheDocument(); + }); + it("renders drag handle in the right-side actions cluster for each provider card", () => { mockUseUsageData.mockReturnValue(createUsageDataState({ providers: mockProviders, diff --git a/packages/dashboard/src/__tests__/usage.test.ts b/packages/dashboard/src/__tests__/usage.test.ts index 846449c368..d480e2eb1e 100644 --- a/packages/dashboard/src/__tests__/usage.test.ts +++ b/packages/dashboard/src/__tests__/usage.test.ts @@ -3819,8 +3819,7 @@ describe("usage", () => { expect(mockRequest).toHaveBeenCalledTimes(1); }); - it("does not fabricate a weekly credit window when the CLI billing percentage is omitted", async () => { - vi.stubEnv("GROK_API_KEY", "env-grok-key"); + it("keeps a healthy unmeterable CLI login authenticated when the billing percentage is omitted", async () => { mockReadFile.mockImplementation(async (filePath: string) => { if (String(filePath).includes(".grok/auth.json")) return GROK_CLI_AUTH_JSON; return Promise.reject(new Error("File not found")); @@ -3846,7 +3845,8 @@ describe("usage", () => { expect(grok.status).toBe("ok"); expect(grok.windows).toEqual([]); expect(grok.windows.some((window) => window.percentUsed === 100)).toBe(false); - expect(mockRequest).toHaveBeenCalledTimes(2); + expect(grok.error ?? "").not.toMatch(/auth expired/i); + expect(mockRequest).toHaveBeenCalledTimes(1); }); it("renders a zero-percent weekly CLI credit window", async () => { @@ -3886,9 +3886,28 @@ describe("usage", () => { const providers = await fetchAllProviderUsage(); const grok = providers.find((provider) => provider.name === "Grok")!; - expect(grok.status).toBe("error"); + expect(grok.status).toBe("ok"); expect(grok.windows).toEqual([]); - expect(grok.error).toContain("grok login"); + expect(grok.error ?? "").not.toMatch(/auth expired/i); + }); + + it.each([ + ["missing config", {}], + ["invalid config", { config: null }], + ["empty body", ""], + ])("keeps a CLI login authenticated for %s billing data", async (_description, body) => { + mockReadFile.mockImplementation(async (filePath: string) => { + if (String(filePath).includes(".grok/auth.json")) return GROK_CLI_AUTH_JSON; + return Promise.reject(new Error("File not found")); + }); + mockGrokBillingResponse(200, body); + + const providers = await fetchAllProviderUsage(); + const grok = providers.find((provider) => provider.name === "Grok")!; + + expect(grok.status).toBe("ok"); + expect(grok.windows).toEqual([]); + expect(grok.error ?? "").not.toMatch(/auth expired/i); }); it("retains the Credits label for a non-weekly numeric CLI percentage", async () => { @@ -3914,6 +3933,56 @@ describe("usage", () => { }); }); + it.each([401, 403])("shows CLI auth expiry only for observed billing HTTP %s without an API key", async (statusCode) => { + mockReadFile.mockImplementation(async (filePath: string) => { + if (String(filePath).includes(".grok/auth.json")) return GROK_CLI_AUTH_JSON; + return Promise.reject(new Error("File not found")); + }); + mockGrokBillingResponse(statusCode, { error: "unauthorized" }); + + const providers = await fetchAllProviderUsage(); + const grok = providers.find((provider) => provider.name === "Grok")!; + + expect(grok.status).toBe("error"); + expect(grok.error).toMatch(/auth expired/i); + }); + + it.each([429, 500])("reports billing HTTP %s without claiming CLI auth expired", async (statusCode) => { + mockReadFile.mockImplementation(async (filePath: string) => { + if (String(filePath).includes(".grok/auth.json")) return GROK_CLI_AUTH_JSON; + return Promise.reject(new Error("File not found")); + }); + mockGrokBillingResponse(statusCode, { error: "unavailable" }); + + const providers = await fetchAllProviderUsage(); + const grok = providers.find((provider) => provider.name === "Grok")!; + + expect(grok.status).toBe("error"); + expect(grok.error).toContain(`HTTP ${statusCode}`); + expect(grok.error ?? "").not.toMatch(/auth expired/i); + }); + + it("reports a CLI billing transport failure without claiming auth expired", async () => { + mockReadFile.mockImplementation(async (filePath: string) => { + if (String(filePath).includes(".grok/auth.json")) return GROK_CLI_AUTH_JSON; + return Promise.reject(new Error("File not found")); + }); + mockRequest.mockImplementation(() => ({ + on: vi.fn((event: string, handler: (error: Error) => void) => { + if (event === "error") handler(new Error("network unavailable")); + }), + write: vi.fn(), + end: vi.fn(), + })); + + const providers = await fetchAllProviderUsage(); + const grok = providers.find((provider) => provider.name === "Grok")!; + + expect(grok.status).toBe("error"); + expect(grok.error).toContain("network unavailable"); + expect(grok.error ?? "").not.toMatch(/auth expired/i); + }); + it("falls back to the xAI API-key validity card when CLI billing fails", async () => { vi.stubEnv("GROK_API_KEY", "env-grok-key"); mockReadFile.mockImplementation(async (filePath: string) => { diff --git a/packages/dashboard/src/usage.ts b/packages/dashboard/src/usage.ts index 456f774945..9ba864f263 100644 --- a/packages/dashboard/src/usage.ts +++ b/packages/dashboard/src/usage.ts @@ -1694,12 +1694,22 @@ async function readGrokCliOidcToken(): Promise { return null; } +type GrokCliBillingUsageOutcome = + | { outcome: "window" } + | { outcome: "unauthorized" } + | { outcome: "no-data" } + | { outcome: "http-error"; status: number } + | { outcome: "transport-error"; message: string }; + /** * Fetch Grok subscription credit usage via the grok CLI's billing endpoint. - * Returns null when the request fails in any way so the caller can fall back - * to the xAI API-key auth-validity card. + * Preserves the observed reason no usage window was emitted so callers never + * infer authentication failure from unavailable meter data. */ -async function fetchGrokCliBillingUsage(token: string, usage: ProviderUsage): Promise { +async function fetchGrokCliBillingUsage( + token: string, + usage: ProviderUsage, +): Promise { try { const res = await httpsRequest("https://cli-chat-proxy.grok.com/v1/billing?format=credits", { method: "GET", @@ -1708,21 +1718,22 @@ async function fetchGrokCliBillingUsage(token: string, usage: ProviderUsage): Pr "content-type": "application/json", }, }); - if (res.status !== 200) return false; + if (res.status === 401 || res.status === 403) return { outcome: "unauthorized" }; + if (res.status !== 200) return { outcome: "http-error", status: res.status }; // eslint-disable-next-line @typescript-eslint/no-explicit-any -- untyped API response const data: any = res.body.trim().length > 0 ? JSON.parse(res.body) : {}; const config = data?.config; - if (!config || typeof config !== "object") return false; + if (!config || typeof config !== "object") return { outcome: "no-data" }; const parsedReset = _parseResetTimestamp(config.billingPeriodEnd ?? config.currentPeriod?.end); const isWeekly = config.currentPeriod?.type === "USAGE_PERIOD_TYPE_WEEKLY"; /* FNXC:UsageProviders 2026-07-31-20:31: - A real account reported zero Grok credit usage while its billing response omitted `creditUsagePercent`, disproving the former omitted-field-to-100% inference. Emit a credits window only for a finite API-supplied percentage; field absence must fall through to the API-key validity or CLI-auth error card rather than fabricate consumption. + A real account reported zero Grok credit usage while its billing response omitted `creditUsagePercent`, disproving the former omitted-field-to-100% inference. Emit a credits window only for a finite API-supplied percentage; field absence must remain an authenticated but unmeterable state rather than fabricate consumption or infer expired CLI auth. */ const pctUsed = config.creditUsagePercent; - if (typeof pctUsed !== "number" || !Number.isFinite(pctUsed)) return false; + if (typeof pctUsed !== "number" || !Number.isFinite(pctUsed)) return { outcome: "no-data" }; usage.windows.push({ label: isWeekly ? "Weekly (credits)" : "Credits", @@ -1734,9 +1745,12 @@ async function fetchGrokCliBillingUsage(token: string, usage: ProviderUsage): Pr windowDurationMs: isWeekly ? 7 * 24 * 60 * 60 * 1000 : undefined, }); usage.status = "ok"; - return true; - } catch { - return false; + return { outcome: "window" }; + } catch (error: unknown) { + return { + outcome: "transport-error", + message: error instanceof Error ? error.message : "Failed to fetch", + }; } } @@ -1765,17 +1779,30 @@ async function fetchGrokUsage(authStorage?: AuthStorageLike): Promise