import fs from "node:fs"; import path from "node:path"; import { describe, expect, it, vi } from "vitest"; import { withTempHome } from "../../test/helpers/temp-home.js"; import { ensureAuthProfileStore, listProfilesForProvider } from "../agents/auth-profiles.js"; import { formatUsageReportLines, formatUsageSummaryLine, loadProviderUsageSummary, type UsageSummary, } from "./provider-usage.js"; describe("provider usage formatting", () => { it("returns null when no usage is available", () => { const summary: UsageSummary = { updatedAt: 0, providers: [] }; expect(formatUsageSummaryLine(summary)).toBeNull(); }); it("picks the most-used window for summary line", () => { const summary: UsageSummary = { updatedAt: 0, providers: [ { provider: "anthropic", displayName: "Claude", windows: [ { label: "5h", usedPercent: 10 }, { label: "Week", usedPercent: 60 }, ], }, ], }; const line = formatUsageSummaryLine(summary, { now: 0 }); expect(line).toContain("Claude"); expect(line).toContain("40% left"); expect(line).toContain("(Week"); }); it("prints provider errors in report output", () => { const summary: UsageSummary = { updatedAt: 0, providers: [ { provider: "openai-codex", displayName: "Codex", windows: [], error: "Token expired", }, ], }; const lines = formatUsageReportLines(summary); expect(lines.join("\n")).toContain("Codex: Token expired"); }); it("includes reset countdowns in report lines", () => { const now = Date.UTC(2026, 0, 7, 0, 0, 0); const summary: UsageSummary = { updatedAt: now, providers: [ { provider: "anthropic", displayName: "Claude", windows: [{ label: "5h", usedPercent: 20, resetAt: now + 60_000 }], }, ], }; const lines = formatUsageReportLines(summary, { now }); expect(lines.join("\n")).toContain("resets 1m"); }); }); describe("provider usage loading", () => { it("loads usage snapshots with injected auth", async () => { const makeResponse = (status: number, body: unknown): Response => { const payload = typeof body === "string" ? body : JSON.stringify(body); const headers = typeof body === "string" ? undefined : { "Content-Type": "application/json" }; return new Response(payload, { status, headers }); }; const mockFetch = vi.fn, ReturnType>(async (input) => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (url.includes("api.anthropic.com")) { return makeResponse(200, { five_hour: { utilization: 20, resets_at: "2026-01-07T01:00:00Z" }, }); } if (url.includes("api.z.ai")) { return makeResponse(200, { success: true, code: 200, data: { planName: "Pro", limits: [ { type: "TOKENS_LIMIT", percentage: 25, unit: 3, number: 6, nextResetTime: "2026-01-07T06:00:00Z", }, ], }, }); } if (url.includes("api.minimaxi.com/v1/api/openplatform/coding_plan/remains")) { return makeResponse(200, { base_resp: { status_code: 0, status_msg: "ok" }, data: { total: 200, remain: 50, reset_at: "2026-01-07T05:00:00Z", plan_name: "Coding Plan", }, }); } return makeResponse(404, "not found"); }); const summary = await loadProviderUsageSummary({ now: Date.UTC(2026, 0, 7, 0, 0, 0), auth: [ { provider: "anthropic", token: "token-1" }, { provider: "minimax", token: "token-1b" }, { provider: "zai", token: "token-2" }, ], fetch: mockFetch, }); expect(summary.providers).toHaveLength(3); const claude = summary.providers.find((p) => p.provider === "anthropic"); const minimax = summary.providers.find((p) => p.provider === "minimax"); const zai = summary.providers.find((p) => p.provider === "zai"); expect(claude?.windows[0]?.label).toBe("5h"); expect(minimax?.windows[0]?.usedPercent).toBe(75); expect(zai?.plan).toBe("Pro"); expect(mockFetch).toHaveBeenCalled(); }); it("handles nested MiniMax usage payloads", async () => { const makeResponse = (status: number, body: unknown): Response => { const payload = typeof body === "string" ? body : JSON.stringify(body); const headers = typeof body === "string" ? undefined : { "Content-Type": "application/json" }; return new Response(payload, { status, headers }); }; const mockFetch = vi.fn, ReturnType>(async (input) => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (url.includes("api.minimaxi.com/v1/api/openplatform/coding_plan/remains")) { return makeResponse(200, { base_resp: { status_code: 0, status_msg: "ok" }, data: { plan_name: "Coding Plan", usage: { prompt_limit: 200, prompt_remain: 50, next_reset_time: "2026-01-07T05:00:00Z", }, }, }); } return makeResponse(404, "not found"); }); const summary = await loadProviderUsageSummary({ now: Date.UTC(2026, 0, 7, 0, 0, 0), auth: [{ provider: "minimax", token: "token-1b" }], fetch: mockFetch, }); const minimax = summary.providers.find((p) => p.provider === "minimax"); expect(minimax?.windows[0]?.usedPercent).toBe(75); expect(minimax?.plan).toBe("Coding Plan"); expect(mockFetch).toHaveBeenCalled(); }); it("prefers MiniMax count-based usage when percent looks inverted", async () => { const makeResponse = (status: number, body: unknown): Response => { const payload = typeof body === "string" ? body : JSON.stringify(body); const headers = typeof body === "string" ? undefined : { "Content-Type": "application/json" }; return new Response(payload, { status, headers }); }; const mockFetch = vi.fn, ReturnType>(async (input) => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (url.includes("api.minimaxi.com/v1/api/openplatform/coding_plan/remains")) { return makeResponse(200, { base_resp: { status_code: 0, status_msg: "ok" }, data: { prompt_limit: 200, prompt_remain: 150, usage_percent: 75, next_reset_time: "2026-01-07T05:00:00Z", }, }); } return makeResponse(404, "not found"); }); const summary = await loadProviderUsageSummary({ now: Date.UTC(2026, 0, 7, 0, 0, 0), auth: [{ provider: "minimax", token: "token-1b" }], fetch: mockFetch, }); const minimax = summary.providers.find((p) => p.provider === "minimax"); expect(minimax?.windows[0]?.usedPercent).toBe(25); expect(mockFetch).toHaveBeenCalled(); }); it("handles MiniMax model_remains usage payloads", async () => { const makeResponse = (status: number, body: unknown): Response => { const payload = typeof body === "string" ? body : JSON.stringify(body); const headers = typeof body === "string" ? undefined : { "Content-Type": "application/json" }; return new Response(payload, { status, headers }); }; const mockFetch = vi.fn, ReturnType>(async (input) => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (url.includes("api.minimaxi.com/v1/api/openplatform/coding_plan/remains")) { return makeResponse(200, { base_resp: { status_code: 0, status_msg: "ok" }, model_remains: [ { start_time: 1736217600, end_time: 1736235600, remains_time: 600, current_interval_total_count: 120, current_interval_usage_count: 30, model_name: "MiniMax-M2.1", }, ], }); } return makeResponse(404, "not found"); }); const summary = await loadProviderUsageSummary({ now: Date.UTC(2026, 0, 7, 0, 0, 0), auth: [{ provider: "minimax", token: "token-1b" }], fetch: mockFetch, }); const minimax = summary.providers.find((p) => p.provider === "minimax"); expect(minimax?.windows[0]?.usedPercent).toBe(25); expect(mockFetch).toHaveBeenCalled(); }); it("discovers Claude usage from token auth profiles", async () => { await withTempHome( async (tempHome) => { const agentDir = path.join( process.env.CLAWDBOT_STATE_DIR ?? path.join(tempHome, ".clawdbot"), "agents", "main", "agent", ); fs.mkdirSync(agentDir, { recursive: true, mode: 0o700 }); fs.writeFileSync( path.join(agentDir, "auth-profiles.json"), `${JSON.stringify( { version: 1, order: { anthropic: ["anthropic:default"] }, profiles: { "anthropic:default": { type: "token", provider: "anthropic", token: "token-1", expires: Date.UTC(2100, 0, 1, 0, 0, 0), }, }, }, null, 2, )}\n`, "utf8", ); const store = ensureAuthProfileStore(agentDir, { allowKeychainPrompt: false, }); expect(listProfilesForProvider(store, "anthropic")).toContain("anthropic:default"); const makeResponse = (status: number, body: unknown): Response => { const payload = typeof body === "string" ? body : JSON.stringify(body); const headers = typeof body === "string" ? undefined : { "Content-Type": "application/json" }; return new Response(payload, { status, headers }); }; const mockFetch = vi.fn, ReturnType>( async (input, init) => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (url.includes("api.anthropic.com/api/oauth/usage")) { const headers = (init?.headers ?? {}) as Record; expect(headers.Authorization).toBe("Bearer token-1"); return makeResponse(200, { five_hour: { utilization: 20, resets_at: "2026-01-07T01:00:00Z", }, }); } return makeResponse(404, "not found"); }, ); const summary = await loadProviderUsageSummary({ now: Date.UTC(2026, 0, 7, 0, 0, 0), providers: ["anthropic"], agentDir, fetch: mockFetch, }); expect(summary.providers).toHaveLength(1); const claude = summary.providers[0]; expect(claude?.provider).toBe("anthropic"); expect(claude?.windows[0]?.label).toBe("5h"); expect(mockFetch).toHaveBeenCalled(); }, { env: { CLAWDBOT_STATE_DIR: (home) => path.join(home, ".clawdbot"), }, prefix: "clawdbot-provider-usage-", }, ); }); it("prefers claude-cli token for Anthropic usage snapshots", async () => { await withTempHome( async () => { const stateDir = process.env.CLAWDBOT_STATE_DIR; if (!stateDir) throw new Error("Missing CLAWDBOT_STATE_DIR"); const agentDir = path.join(stateDir, "agents", "main", "agent"); fs.mkdirSync(agentDir, { recursive: true, mode: 0o700 }); fs.writeFileSync( path.join(agentDir, "auth-profiles.json"), `${JSON.stringify( { version: 1, profiles: { "anthropic:default": { type: "token", provider: "anthropic", token: "token-default", expires: Date.UTC(2100, 0, 1, 0, 0, 0), }, "anthropic:claude-cli": { type: "token", provider: "anthropic", token: "token-cli", expires: Date.UTC(2100, 0, 1, 0, 0, 0), }, }, }, null, 2, )}\n`, "utf8", ); const makeResponse = (status: number, body: unknown): Response => { const payload = typeof body === "string" ? body : JSON.stringify(body); const headers = typeof body === "string" ? undefined : { "Content-Type": "application/json" }; return new Response(payload, { status, headers }); }; const mockFetch = vi.fn, ReturnType>( async (input, init) => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (url.includes("api.anthropic.com/api/oauth/usage")) { const headers = (init?.headers ?? {}) as Record; expect(headers.Authorization).toBe("Bearer token-cli"); return makeResponse(200, { five_hour: { utilization: 20, resets_at: "2026-01-07T01:00:00Z" }, }); } return makeResponse(404, "not found"); }, ); const summary = await loadProviderUsageSummary({ now: Date.UTC(2026, 0, 7, 0, 0, 0), providers: ["anthropic"], agentDir, fetch: mockFetch, }); expect(summary.providers).toHaveLength(1); expect(summary.providers[0]?.provider).toBe("anthropic"); expect(summary.providers[0]?.windows[0]?.label).toBe("5h"); expect(mockFetch).toHaveBeenCalled(); }, { prefix: "clawdbot-provider-usage-" }, ); }); it("falls back to claude.ai web usage when OAuth scope is missing", async () => { const cookieSnapshot = process.env.CLAUDE_AI_SESSION_KEY; process.env.CLAUDE_AI_SESSION_KEY = "sk-ant-web-1"; try { const makeResponse = (status: number, body: unknown): Response => { const payload = typeof body === "string" ? body : JSON.stringify(body); const headers = typeof body === "string" ? undefined : { "Content-Type": "application/json" }; return new Response(payload, { status, headers }); }; const mockFetch = vi.fn, ReturnType>(async (input) => { const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; if (url.includes("api.anthropic.com/api/oauth/usage")) { return makeResponse(403, { type: "error", error: { type: "permission_error", message: "OAuth token does not meet scope requirement user:profile", }, }); } if (url.includes("claude.ai/api/organizations/org-1/usage")) { return makeResponse(200, { five_hour: { utilization: 20, resets_at: "2026-01-07T01:00:00Z" }, seven_day: { utilization: 40, resets_at: "2026-01-08T01:00:00Z" }, seven_day_opus: { utilization: 5 }, }); } if (url.includes("claude.ai/api/organizations")) { return makeResponse(200, [{ uuid: "org-1", name: "Test" }]); } return makeResponse(404, "not found"); }); const summary = await loadProviderUsageSummary({ now: Date.UTC(2026, 0, 7, 0, 0, 0), auth: [{ provider: "anthropic", token: "sk-ant-oauth-1" }], fetch: mockFetch, }); expect(summary.providers).toHaveLength(1); const claude = summary.providers[0]; expect(claude?.provider).toBe("anthropic"); expect(claude?.windows.some((w) => w.label === "5h")).toBe(true); expect(claude?.windows.some((w) => w.label === "Week")).toBe(true); } finally { if (cookieSnapshot === undefined) delete process.env.CLAUDE_AI_SESSION_KEY; else process.env.CLAUDE_AI_SESSION_KEY = cookieSnapshot; } }); });