Files
paseo/plugins/zai-usage-source/server/usage.test.ts
T
peanut_hzm 7ba608fcb2 fix(usage): follow the CLIs' current auth locations for MiniMax and Z.ai
The usage sources look for credentials where the CLIs used to keep them, so
their plan quota never appears in Settings → Usage.

MiniMax Code (`mcode`) moved its OAuth login to
`~/.minimax/auth/<buildEnv>/<region>/<clientId>/auth.json`, with the token
inside a `records` map and the region in a sibling `auth-state.json`. The
source still looked in `~/.mmx/` for the older `access_token` shape. It also
called `/v1/token_plan/remains`, which the agent backend answers with
`status_code 1004` ("Please carry the API secret key in the 'Authorization'
field"); the coding-plan endpoint the CLI itself uses is
`/v1/api/openplatform/coding_plan/remains`.

Z.ai's `glm-acp-agent` reads `Z_AI_API_KEY` and persists it to
`~/.config/glm-acp-agent/credentials.json` as `z_ai_api_key`. The source
looked for `ZAI_API_KEY` and `GLM_API_KEY` only, and never read the
credentials file.

Both keep their existing lookups working, so a configured environment
variable or the older `~/.mmx/` layout still resolves.
2026-10-31 00:41:24 +08:00

234 lines
8.3 KiB
TypeScript

import { mkdirSync, mkdtempSync, rmSync, writeFileSync } 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",
"Z_AI_API_KEY",
"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("fetches Z.ai usage from Z_AI_API_KEY, the name the glm-acp-agent CLI uses", async () => {
process.env["Z_AI_API_KEY"] = "zai_cli_token";
let authorization: string | null = null;
fetchApi = (async (_url: RequestInfo | URL, init?: RequestInit) => {
authorization = (init?.headers as Record<string, string> | undefined)?.Authorization ?? null;
return jsonResponse({ data: [{ productName: "GLM Coding Max", status: "VALID" }] });
}) as unknown as typeof fetch;
const zai = findProvider(await service().listUsage(), "zai");
expect(authorization).toBe("Bearer zai_cli_token");
expect(zai).toMatchObject({ status: "available", planLabel: "GLM Coding Max" });
});
it("reads Z.ai usage from the credentials file the glm-acp-agent CLI writes", async () => {
const configDir = join(homeDir, ".config", "glm-acp-agent");
mkdirSync(configDir, { recursive: true });
writeFileSync(
join(configDir, "credentials.json"),
JSON.stringify({ z_ai_api_key: "zai_file_token" }),
);
let authorization: string | null = null;
fetchApi = (async (_url: RequestInfo | URL, init?: RequestInit) => {
authorization = (init?.headers as Record<string, string> | undefined)?.Authorization ?? null;
return jsonResponse({ data: [{ productName: "GLM Coding Max", status: "VALID" }] });
}) as unknown as typeof fetch;
const zai = findProvider(await service().listUsage(), "zai");
expect(authorization).toBe("Bearer zai_file_token");
expect(zai).toMatchObject({ status: "available" });
});
it("honours XDG_CONFIG_HOME for the credentials file", async () => {
const configDir = join(homeDir, "xdg", "glm-acp-agent");
mkdirSync(configDir, { recursive: true });
writeFileSync(
join(configDir, "credentials.json"),
JSON.stringify({ z_ai_api_key: "zai_xdg_token" }),
);
process.env["XDG_CONFIG_HOME"] = join(homeDir, "xdg");
let authorization: string | null = null;
fetchApi = (async (_url: RequestInfo | URL, init?: RequestInit) => {
authorization = (init?.headers as Record<string, string> | undefined)?.Authorization ?? null;
return jsonResponse({ data: [{ productName: "GLM Coding Max", status: "VALID" }] });
}) as unknown as typeof fetch;
const zai = findProvider(await service().listUsage(), "zai");
expect(authorization).toBe("Bearer zai_xdg_token");
expect(zai).toMatchObject({ status: "available" });
});
});
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;
}
});