Files
Mohamed Boudra 05e94ab52e Keep usage logins visible and explain unavailable quota (#5876)
* fix(usage): keep existing logins visible and explain unavailable quota

Credential discovery must preserve accounts whose login exists even when it has expired. Keep vendor failures on the discovered account and isolate discovery failures without inventing provider cards.

* test(usage): align credential route tests with account discovery
2026-10-02 00:30:11 +01:00

180 lines
5.8 KiB
TypeScript

import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { discover, fetchUsage } from "./usage.js";
import type { UsageReport } from "@getpaseo/plugin/server/usage";
function mockFetch(handlers: Map<string, () => Response>): typeof fetch {
return vi.fn(async (url: RequestInfo | URL) => {
const key = url.toString();
const handler = handlers.get(key);
if (!handler) throw new Error(`Unmocked fetch: ${key}`);
return handler();
}) as unknown as typeof fetch;
}
function jsonResponse(body: unknown, status = 200): Response {
return new Response(JSON.stringify(body), {
status,
headers: { "Content-Type": "application/json" },
});
}
describe("zai usage source", () => {
let homeDir: string;
let fetchApi: typeof fetch;
let originalEnv: Record<string, string | undefined>;
beforeEach(() => {
homeDir = mkdtempSync(join(tmpdir(), "usage-home-"));
originalEnv = { ...process.env };
process.env["HOME"] = homeDir;
process.env["USERPROFILE"] = homeDir;
for (const key of [
"APPDATA",
"COPILOT_TOKEN",
"GITHUB_TOKEN",
"GITHUB_PAT",
"CURSOR_ACCESS_TOKEN",
"CURSOR_TOKEN",
"ZAI_API_KEY",
"GLM_API_KEY",
"GROK_API_KEY",
"GROK_TOKEN",
"KIMI_TOKEN",
"KIMI_API_KEY",
"KIMI_CODE_HOME",
"MINIMAX_API_KEY",
"MINIMAX_BASE_URL",
])
delete process.env[key];
fetchApi = mockFetch(new Map());
});
afterEach(() => {
rmSync(homeDir, { recursive: true, force: true });
for (const key of Object.keys(process.env)) if (!(key in originalEnv)) delete process.env[key];
for (const key in originalEnv) process.env[key] = originalEnv[key];
});
function service(
_options: {
platform?: typeof process.platform;
keychain?: () => Promise<unknown | null>;
cursorHomeDir?: string;
kimiHomeDir?: string;
} = {},
) {
return {
listUsage: async () => {
const report = await fetchFirst((url, init) => fetchApi(url, init));
return {
providers: [
{
providerId: "zai",
...report,
error: report.status === "error" ? report.error : null,
planLabel: report.status === "available" ? (report.planLabel ?? null) : null,
},
],
};
},
};
}
function findProvider(
result: { providers: Array<{ providerId: string } & UsageReport> },
id: string,
) {
const report = result.providers.find((item) => item.providerId === id);
if (!report) throw new Error(`Missing usage source ${id}`);
return report;
}
it("fetches Z.ai usage from ZAI_API_KEY", async () => {
process.env["ZAI_API_KEY"] = "zai_test_token";
fetchApi = mockFetch(
new Map([
[
"https://api.z.ai/api/biz/subscription/list",
() =>
jsonResponse({
data: [
{
productName: "GLM Coding Max",
status: "VALID",
purchaseTime: "2026-01-12 16:55:13",
valid: "2026-02-12 16:55:13-2026-03-12 16:55:13",
},
],
}),
],
]),
);
const zai = findProvider(await service().listUsage(), "zai");
expect(zai).toMatchObject({
status: "available",
planLabel: "GLM Coding Max",
details: expect.arrayContaining([{ id: "status", label: "Status", value: "VALID" }]),
});
});
});
it("discovery returns a locator when fetch finds zai credentials", async () => {
const previous = process.env["ZAI_API_KEY"];
try {
process.env["ZAI_API_KEY"] = "fixture-token";
let requested = false;
await fetchFirst(async () => {
requested = true;
return new Response(null, { status: 401 });
});
expect(requested).toBe(true);
expect(await discover()).toEqual([
{ key: "default", input: { store: "env", locator: "ZAI_API_KEY" } },
]);
} finally {
if (previous === undefined) delete process.env["ZAI_API_KEY"];
else process.env["ZAI_API_KEY"] = previous;
}
});
describe("account discovery", () => {
it.each(["empty home", "unrelated files"])("returns no accounts for %s", async (scenario) => {
const { mkdtemp, writeFile, rm } = await import("node:fs/promises");
const directory = await mkdtemp(join(tmpdir(), "usage-empty-"));
const original = { ...process.env };
try {
for (const key of Object.keys(process.env)) delete process.env[key];
process.env.HOME = directory;
process.env.USERPROFILE = directory;
if (scenario === "unrelated files") await writeFile(join(directory, "unrelated.json"), "{}");
expect(await discover()).toEqual([]);
} finally {
for (const key of Object.keys(process.env)) delete process.env[key];
Object.assign(process.env, original);
await rm(directory, { recursive: true, force: true });
}
});
});
async function fetchFirst(fetchApi: typeof fetch) {
const accounts = await discover();
const account = accounts[0];
if (!account) throw new Error("No configured account");
return fetchUsage(account.input as Parameters<typeof fetchUsage>[0], fetchApi);
}
it.each([401, 403])("reports an existing login rejected with HTTP %i", async (status) => {
const previous = process.env["ZAI_API_KEY"];
try {
process.env["ZAI_API_KEY"] = "fixture-rejected-login";
const report = await fetchUsage(
{ store: "env", locator: "ZAI_API_KEY" },
async () => new Response(null, { status }),
);
expect(report).toEqual({ status: "unavailable", problem: { kind: "rejected", status } });
} finally {
if (previous === undefined) delete process.env["ZAI_API_KEY"];
else process.env["ZAI_API_KEY"] = previous;
}
});