import { mkdirSync, readFileSync, writeFileSync, 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 kimiCredentialPath(dir: string): string { return join(dir, "credentials", "kimi-code.json"); } function writeKimiCredentials(dir: string, accessToken: string, overrides: object = {}): void { mkdirSync(join(dir, "credentials"), { recursive: true }); writeFileSync( kimiCredentialPath(dir), JSON.stringify({ access_token: accessToken, refresh_token: "rt_kimi", expires_at: 1_798_812_800, expires_in: 900, scope: "kimi-code", token_type: "Bearer", ...overrides, }), ); } function mockFetch(handlers: Map 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("kimi usage source", () => { let homeDir: string; let fetchApi: typeof fetch; let originalEnv: Record; 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; cursorHomeDir?: string; kimiHomeDir?: string; } = {}, ) { return { listUsage: async () => { const report = await fetchFirst((url, init) => fetchApi(url, init)); return { providers: [ { providerId: "kimi", ...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 Kimi usage from KIMI_TOKEN", async () => { process.env["KIMI_TOKEN"] = "kimi_test_token"; fetchApi = mockFetch( new Map([ [ "https://api.kimi.com/coding/v1/usages", () => jsonResponse({ usage: { limit: "100", remaining: "74", resetTime: "2026-02-11T17:32:50Z", }, }), ], ]), ); const kimi = findProvider(await service().listUsage(), "kimi"); expect(kimi).toMatchObject({ status: "available", windows: [ expect.objectContaining({ id: "coding_usage", usedPct: 26, remainingPct: 74, resetsAt: "2026-02-11T17:32:50Z", }), ], }); }); it("fetches Kimi usage from the CLI credential home", async () => { writeKimiCredentials(join(homeDir, ".kimi-code"), "kimi_cli_token"); let requestedUrl: string | null = null; let authorization: string | null = null; fetchApi = (async (url: RequestInfo | URL, init?: RequestInit) => { requestedUrl = url.toString(); authorization = (init?.headers as Record | undefined)?.Authorization ?? null; return jsonResponse({ usage: { limit: "200", remaining: "150", resetTime: "2026-06-23T05:12:17Z", }, }); }) as unknown as typeof fetch; const kimi = findProvider(await service({ kimiHomeDir: homeDir }).listUsage(), "kimi"); expect(requestedUrl).toBe("https://api.kimi.com/coding/v1/usages"); expect(authorization).toBe("Bearer kimi_cli_token"); expect(kimi).toMatchObject({ status: "available", windows: [ expect.objectContaining({ id: "coding_usage", usedPct: 25, remainingPct: 75, resetsAt: "2026-06-23T05:12:17Z", }), ], }); }); it("reads Kimi credentials whose optional fields are null", async () => { writeKimiCredentials(join(homeDir, ".kimi-code"), "kimi_cli_token", { expires_at: null, expires_in: null, scope: null, token_type: null, }); fetchApi = mockFetch( new Map([ [ "https://api.kimi.com/coding/v1/usages", () => jsonResponse({ usage: { limit: "100", remaining: "60" } }), ], ]), ); const kimi = findProvider(await service({ kimiHomeDir: homeDir }).listUsage(), "kimi"); expect(kimi.status).toBe("available"); }); it("returns unavailable on 401 without refreshing or rewriting the credential file", async () => { writeKimiCredentials(join(homeDir, ".kimi-code"), "at_kimi_expired"); const credPath = kimiCredentialPath(join(homeDir, ".kimi-code")); const before = readFileSync(credPath, "utf8"); let usageCalls = 0; fetchApi = vi.fn(async (url: RequestInfo | URL) => { const endpoint = url.toString(); if (endpoint === "https://api.kimi.com/coding/v1/usages") { usageCalls += 1; return new Response(null, { status: 401 }); } // The read-only fetcher must never hit the OAuth token endpoint. throw new Error(`Unmocked: ${endpoint}`); }) as never; const result = await service({ kimiHomeDir: homeDir }).listUsage(); expect(findProvider(result, "kimi").status).toBe("unavailable"); expect(usageCalls).toBe(1); // The credentials file must be left untouched for the Kimi CLI to own. expect(readFileSync(credPath, "utf8")).toBe(before); }); it("does not refresh Kimi tokens read from the environment", async () => { process.env["KIMI_TOKEN"] = "kimi_test_token"; const usageFetch = vi.fn(async () => new Response(null, { status: 401 })); fetchApi = usageFetch as never; const result = await service().listUsage(); expect(findProvider(result, "kimi").status).toBe("unavailable"); expect(usageFetch).toHaveBeenCalledTimes(1); }); it("does not refresh Kimi tokens on a 403", async () => { writeKimiCredentials(join(homeDir, ".kimi-code"), "at_kimi_forbidden"); const usageFetch = vi.fn(async () => new Response(null, { status: 403 })); fetchApi = usageFetch as never; const result = await service({ kimiHomeDir: homeDir }).listUsage(); expect(findProvider(result, "kimi").status).toBe("unavailable"); expect(usageFetch).toHaveBeenCalledTimes(1); }); }); describe("Kimi usage source usage windows", () => { afterEach(() => { delete process.env["KIMI_TOKEN"]; vi.restoreAllMocks(); }); it("normalizes weekly and enforced rolling usage windows", async () => { process.env["KIMI_TOKEN"] = "kimi_test_token"; const fetchApi = vi.fn(async () => jsonResponse({ limited: true, usage: { limit: "100", used: "61", remaining: "39", resetTime: "2026-08-05T00:01:45Z", }, limits: [ { window: { duration: 300, timeUnit: "TIME_UNIT_MINUTE", }, detail: { limit: "100", used: "100", resetTime: "2026-07-31T17:01:45Z", }, }, ], }), ); const usage = await fetchFirst(fetchApi); expect(usage).toMatchObject({ status: "available", windows: [ { id: "coding_usage", label: "Usage limit", usedPct: 61, remainingPct: 39, resetsAt: "2026-08-05T00:01:45Z", tone: "ok", }, { id: "coding_limit_300_time_unit_minute", label: "5-hour limit", usedPct: 100, remainingPct: 0, resetsAt: "2026-07-31T17:01:45Z", tone: "danger", }, ], }); }); it("keeps valid windows when another limits entry is malformed", async () => { process.env["KIMI_TOKEN"] = "kimi_test_token"; const fetchApi = vi.fn(async () => jsonResponse({ usage: { limit: "100", remaining: "75", resetTime: "2026-08-05T00:01:45Z", }, limits: [ { window: { duration: "invalid" }, detail: {} }, { window: { duration: 300, timeUnit: "TIME_UNIT_MINUTE" }, detail: { limit: "100", remaining: "50" }, }, ], }), ); const usage = await fetchFirst(fetchApi); expect(usage.windows).toHaveLength(2); expect(usage.windows[1]).toMatchObject({ label: "5-hour limit", usedPct: 50, remainingPct: 50, }); }); it("accepts direct limit fields, alternate reset keys, and provider labels", async () => { process.env["KIMI_TOKEN"] = "kimi_test_token"; const fetchApi = vi.fn(async () => jsonResponse({ usage: null, limits: [ { name: "Burst quota", limit: "80", remaining: "20", reset_at: "2026-08-01T00:00:00Z", }, ], }), ); const usage = await fetchFirst(fetchApi); expect(usage.windows).toEqual([ expect.objectContaining({ id: "coding_limit_burst_quota", label: "Burst quota", usedPct: 75, remainingPct: 25, resetsAt: "2026-08-01T00:00:00Z", }), ]); }); it("keeps window ids unique when Kimi returns duplicate limit descriptors", async () => { process.env["KIMI_TOKEN"] = "kimi_test_token"; const duplicate = { window: { duration: 300, timeUnit: "TIME_UNIT_MINUTE" }, detail: { limit: "100", used: "10" }, }; const fetchApi = vi.fn(async () => jsonResponse({ limits: [duplicate, duplicate] })); const usage = await fetchFirst(fetchApi); expect(usage.windows.map((window) => window.id)).toEqual([ "coding_limit_300_time_unit_minute", "coding_limit_300_time_unit_minute_2", ]); }); }); it("discovery returns a locator when fetch finds kimi credentials", async () => { const previous = process.env["KIMI_TOKEN"]; try { process.env["KIMI_TOKEN"] = "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: "KIMI_TOKEN" } }, ]); } finally { if (previous === undefined) delete process.env["KIMI_TOKEN"]; else process.env["KIMI_TOKEN"] = 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[0], fetchApi); } it.each([401, 403])("reports an existing login rejected with HTTP %i", async (status) => { const previous = process.env["KIMI_TOKEN"]; try { process.env["KIMI_TOKEN"] = "fixture-rejected-login"; const report = await fetchUsage( { store: "env", locator: "KIMI_TOKEN" }, async () => new Response(null, { status }), ); expect(report).toEqual({ status: "unavailable", problem: { kind: "rejected", status } }); } finally { if (previous === undefined) delete process.env["KIMI_TOKEN"]; else process.env["KIMI_TOKEN"] = previous; } }); it("does not invent a weekly duration for unnamed top-level usage", async () => { const directory = mkdtempSync(join(tmpdir(), "kimi-period-")); try { const locator = join(directory, "auth.json"); writeFileSync(locator, JSON.stringify({ access_token: "fixture" })); const report = await fetchUsage({ store: "file", locator }, async () => Response.json({ usage: { limit: "100", remaining: "74" } }), ); expect(report).toMatchObject({ status: "available", windows: [{ id: "coding_usage", label: "Usage limit" }], }); } finally { rmSync(directory, { recursive: true, force: true }); } }); it("does not infer a duration from an unfamiliar time-unit name containing HOUR", async () => { const directory = mkdtempSync(join(tmpdir(), "kimi-unit-")); try { const locator = join(directory, "auth.json"); writeFileSync(locator, JSON.stringify({ access_token: "fixture" })); const report = await fetchUsage({ store: "file", locator }, async () => Response.json({ limits: [ { window: { duration: 2, timeUnit: "TIME_UNIT_NOT_HOUR" }, detail: { limit: "100", used: "11" }, }, ], }), ); expect(report).toMatchObject({ status: "available", windows: [{ label: "Limit 1" }] }); } finally { rmSync(directory, { recursive: true, force: true }); } });