FN-7798: filter unconfigured/no-entitlement providers from usage view

Only show usage meters for AI providers the user has actually configured, instead of surfacing entries for providers with no meterable data.

- fetchGitHubCopilotUsage now demotes GitHub's 404 "No Copilot subscription found" response (both the Fusion-credential HTTP path and the gh-CLI fallback path) to a `no-auth` status instead of `error`, so it is treated as no meterable entitlement.
- fetchAllProviderUsage's existing `status !== "no-auth"` filter now also excludes these no-entitlement Copilot results, so they no longer appear in the usage list.
- Configured-but-failing providers (expired auth returning 401/403, transient HTTP 5xx, or other errors) keep `status: "error"` and remain visible with their diagnostic message.
- Added regression tests covering: Fusion-credential 404 omitted, Fusion-credential 500 surfaced as error, gh-CLI 404 omitted, gh-CLI 401 surfaced as "GitHub auth expired" error.
- Added a changeset documenting the usage-view behavior change as a patch/fix.

Files changed:
 .changeset/fn-7798-usage-configured-providers.md |  7 +++
 packages/dashboard/src/__tests__/usage.test.ts   | 71 ++++++++++++++++++++++--
 packages/dashboard/src/usage.ts                  | 19 ++++++-
 3 files changed, 90 insertions(+), 7 deletions(-)

Fusion-Task-Id: FN-7798

Fusion-Task-Lineage: 18835b48-68f3-48aa-a03c-cc85772778a9

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-10 19:39:11 -07:00
parent 725ce45c5d
commit 7846c9613e
3 changed files with 90 additions and 7 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Usage view now shows meters only for AI providers you have configured.
category: fix
dev: fetchAllProviderUsage() in packages/dashboard/src/usage.ts filters providers with no resolved credentials and no meterable entitlement (e.g. GitHub 404 "No Copilot subscription found" reclassified error→no-auth); configured-but-failing providers (auth expired / HTTP 5xx / timeout) remain visible.

View File

@@ -256,6 +256,28 @@ describe("usage", () => {
expect(copilot).toBeUndefined(); expect(copilot).toBeUndefined();
}); });
it("omits Fusion-sourced Copilot credentials when GitHub reports no subscription", async () => {
coreInteropMocks.readStoredCredentialsFromAuthFile.mockReturnValue({
"github-copilot": { type: "oauth", access: "fusion-gho", refresh: "r", expires: Date.now() + 60_000 },
});
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 404,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from('{"message":"No Copilot subscription found"}'));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const copilot = providers.find((p) => p.name === "GitHub Copilot");
expect(copilot).toBeUndefined();
});
it("surfaces Fusion re-login guidance when Fusion-sourced token gets 401", async () => { it("surfaces Fusion re-login guidance when Fusion-sourced token gets 401", async () => {
coreInteropMocks.readStoredCredentialsFromAuthFile.mockReturnValue({ coreInteropMocks.readStoredCredentialsFromAuthFile.mockReturnValue({
"github-copilot": { type: "oauth", access: "fusion-gho", refresh: "r", expires: Date.now() + 60_000 }, "github-copilot": { type: "oauth", access: "fusion-gho", refresh: "r", expires: Date.now() + 60_000 },
@@ -279,6 +301,29 @@ describe("usage", () => {
expect(copilot?.error).toContain("re-login from Fusion Settings"); expect(copilot?.error).toContain("re-login from Fusion Settings");
}); });
it("surfaces Fusion HTTP failures when a configured Copilot provider fails transiently", async () => {
coreInteropMocks.readStoredCredentialsFromAuthFile.mockReturnValue({
"github-copilot": { type: "oauth", access: "fusion-gho", refresh: "r", expires: Date.now() + 60_000 },
});
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 500,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from('{"message":"server unavailable"}'));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const copilot = providers.find((p) => p.name === "GitHub Copilot");
expect(copilot?.status).toBe("error");
expect(copilot?.error).toContain("HTTP 500");
});
it("falls back to gh CLI when no Fusion credential is present", async () => { it("falls back to gh CLI when no Fusion credential is present", async () => {
coreInteropMocks.readStoredCredentialsFromAuthFile.mockReturnValue({}); coreInteropMocks.readStoredCredentialsFromAuthFile.mockReturnValue({});
mockExecFileSync.mockImplementation((cmd: string, args: string[]) => { mockExecFileSync.mockImplementation((cmd: string, args: string[]) => {
@@ -299,8 +344,9 @@ describe("usage", () => {
expect(copilot!.windows.some((window) => window.label === "Chat (Monthly)")).toBe(true); expect(copilot!.windows.some((window) => window.label === "Chat (Monthly)")).toBe(true);
}); });
it("returns error when Copilot subscription not found (404)", async () => { it("omits gh CLI Copilot when GitHub reports no subscription", async () => {
mockReadFile.mockRejectedValue(new Error("File not found")); mockReadFile.mockRejectedValue(new Error("File not found"));
coreInteropMocks.readStoredCredentialsFromAuthFile.mockReturnValue({});
mockExecFileSync.mockImplementation((cmd: string, args: string[]) => { mockExecFileSync.mockImplementation((cmd: string, args: string[]) => {
if (cmd === "gh" && args[0] === "auth") { if (cmd === "gh" && args[0] === "auth") {
return ""; return "";
@@ -313,9 +359,26 @@ describe("usage", () => {
const providers = await fetchAllProviderUsage(); const providers = await fetchAllProviderUsage();
const copilot = providers.find((p) => p.name === "GitHub Copilot"); const copilot = providers.find((p) => p.name === "GitHub Copilot");
expect(copilot).toBeDefined(); expect(copilot).toBeUndefined();
expect(copilot!.status).toBe("error"); });
expect(copilot!.error).toContain("No Copilot subscription");
it("surfaces gh CLI auth-expired errors as configured but failing", async () => {
mockReadFile.mockRejectedValue(new Error("File not found"));
coreInteropMocks.readStoredCredentialsFromAuthFile.mockReturnValue({});
mockExecFileSync.mockImplementation((cmd: string, args: string[]) => {
if (cmd === "gh" && args[0] === "auth") {
return "";
}
if (cmd === "gh" && args[0] === "api") {
throw new Error("HTTP 401: Bad credentials");
}
throw new Error("File not found");
});
const providers = await fetchAllProviderUsage();
const copilot = providers.find((p) => p.name === "GitHub Copilot");
expect(copilot?.status).toBe("error");
expect(copilot?.error).toContain("GitHub auth expired");
}); });
}); });

View File

@@ -1808,12 +1808,18 @@ async function fetchGitHubCopilotUsage(): Promise<ProviderUsage> {
return usage; return usage;
} }
usage.status = "error";
if (res.status === 404) { if (res.status === 404) {
/*
FNXC:UsageProviders 2026-07-10-00:00:
Usage surfaces must only show providers the user configured with meterable data. A GitHub credential without Copilot entitlement is a no-entitlement path, so demote it to `no-auth` for the aggregate filter while keeping diagnostics on the provider result.
*/
usage.status = "no-auth";
usage.error = "No Copilot subscription found"; usage.error = "No Copilot subscription found";
} else if (res.status === 401 || res.status === 403) { } else if (res.status === 401 || res.status === 403) {
usage.status = "error";
usage.error = "Auth expired — re-login from Fusion Settings → Authentication"; usage.error = "Auth expired — re-login from Fusion Settings → Authentication";
} else { } else {
usage.status = "error";
usage.error = `HTTP ${res.status}: ${res.body.slice(0, 200)}`; usage.error = `HTTP ${res.status}: ${res.body.slice(0, 200)}`;
} }
return usage; return usage;
@@ -1841,7 +1847,11 @@ async function fetchGitHubCopilotUsage(): Promise<ProviderUsage> {
} catch (e: unknown) { } catch (e: unknown) {
const errMsg = e instanceof Error ? e.message : "Failed to fetch"; const errMsg = e instanceof Error ? e.message : "Failed to fetch";
if (errMsg.includes("404") || errMsg.includes("Not Found")) { if (errMsg.includes("404") || errMsg.includes("Not Found")) {
usage.status = "error"; /*
FNXC:UsageProviders 2026-07-10-00:00:
An authenticated `gh` CLI can exist for ordinary git without Fusion Copilot setup or a Copilot subscription. Treat the 404 no-subscription response as not configured/no meterable entitlement so it is omitted, while 401/403 and transient failures remain visible errors.
*/
usage.status = "no-auth";
usage.error = "No Copilot subscription found"; usage.error = "No Copilot subscription found";
} else if (errMsg.includes("401") || errMsg.includes("403")) { } else if (errMsg.includes("401") || errMsg.includes("403")) {
usage.status = "error"; usage.status = "error";
@@ -1936,7 +1946,10 @@ export async function fetchAllProviderUsage(authStorage?: AuthStorageLike): Prom
} }
} }
// Only return providers that have valid auth configured. /*
FNXC:UsageProviders 2026-07-10-00:00:
This is the single enforcement point for the usage-list invariant: show only providers that are both configured and expose meterable data. Fetchers demote missing credentials and no-entitlement/nothing-to-meter outcomes to `no-auth`; configured providers with actionable failures stay `error` and remain visible.
*/
const authenticatedProviders = providers.filter((provider) => provider.status !== "no-auth"); const authenticatedProviders = providers.filter((provider) => provider.status !== "no-auth");
// Update cache // Update cache