Files
fusion/packages/dashboard/src/usage.test.ts
gsxdsm 806f8ab042 fix(FN-000): restore Claude usage dashboard after Anthropic OAuth changes
Anthropic now requires `anthropic-beta: oauth-2025-04-20` on /api/oauth/usage
for OAuth-scoped tokens; without it the endpoint returns 401 "OAuth
authentication is currently not supported" and the dashboard falls back to
a PTY-based CLI parser that times out at 75s. Mirrors the header the Claude
CLI (2.1.114) sends from `claude /usage`, and also aligns the refresh call
with the CLI's JSON body + `scope` field so token refresh keeps working.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 20:56:13 -07:00

3629 lines
120 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import {
fetchAllProviderUsage,
clearUsageCache,
ProviderUsage,
calculatePace,
_setSleepFn,
_resetSleepFn,
_stripClaudeAnsi,
_parseClaudePercentLine,
_parseClaudeResetLine,
_parseClaudeResetText,
_parseResetTimestamp,
withTimeout,
CLAUDE_FETCH_TIMEOUT_MS,
_clearRefreshedToken,
} from "./usage.js";
// Mock the https module
const mockRequest = vi.fn();
vi.mock("node:https", () => ({
request: (...args: any[]) => mockRequest(...args),
}));
// Mock fs/promises
const mockReadFile = vi.fn();
vi.mock("node:fs/promises", () => ({
readFile: (...args: any[]) => mockReadFile(...args),
default: { readFile: (...args: any[]) => mockReadFile(...args) },
}));
// Mock child_process
const mockExecFileSync = vi.fn();
vi.mock("node:child_process", () => ({
execFileSync: (...args: any[]) => mockExecFileSync(...args),
execFile: (cmd: string, args: string[], options: any, callback: any) => {
const cb = typeof options === "function" ? options : callback;
try {
const stdout = mockExecFileSync(cmd, args, options);
cb(null, stdout, "");
} catch (error) {
cb(error, "", "");
}
},
}));
// Mock node-pty for CLI fallback — default: not available (simulates test env)
vi.mock("node-pty", () => {
throw new Error("node-pty not available in test environment");
});
describe("usage", () => {
beforeEach(() => {
clearUsageCache();
_clearRefreshedToken();
mockRequest.mockClear();
mockReadFile.mockClear();
mockExecFileSync.mockClear();
vi.stubEnv("HOME", "/home/testuser");
});
afterEach(() => {
vi.unstubAllEnvs();
});
describe("fetchAllProviderUsage", () => {
it("returns providers array even when all are not authenticated", async () => {
// All credential files don't exist
mockReadFile.mockImplementation(async () => {
return Promise.reject(new Error("File not found"));
});
const providers = await fetchAllProviderUsage();
expect(providers).toHaveLength(5);
expect(providers.map((p) => p.name)).toContain("Claude");
expect(providers.map((p) => p.name)).toContain("Codex");
expect(providers.map((p) => p.name)).toContain("Gemini");
expect(providers.map((p) => p.name)).toContain("Minimax");
expect(providers.map((p) => p.name)).toContain("Zai");
expect(providers.map((p) => p.name)).not.toContain("Kimi");
// All should be no-auth status
for (const p of providers) {
expect(p.status).toBe("no-auth");
expect(p.error).toBeDefined();
}
});
it("returns cached data within TTL", async () => {
mockReadFile.mockImplementation(async () => {
return Promise.reject(new Error("File not found"));
});
const first = await fetchAllProviderUsage();
const second = await fetchAllProviderUsage();
// Should be the same array reference due to caching
expect(second).toBe(first);
});
it("fetches fresh data after cache expires", async () => {
mockReadFile.mockImplementation(async () => {
return Promise.reject(new Error("File not found"));
});
const first = await fetchAllProviderUsage();
// Manually expire cache
clearUsageCache();
const second = await fetchAllProviderUsage();
// Should be different array reference
expect(second).not.toBe(first);
expect(second).toHaveLength(5);
});
});
describe("Claude provider", () => {
/**
* Helper to set up mocks for Claude tests.
* Claude now reads credentials from files/keychain and calls the API directly.
*/
function setupClaudeMocks(options: {
/** Credential file content (null = file not found) */
credFileContent?: any;
/** Keychain credential content (null = keychain error) */
keychainContent?: any;
}) {
const { credFileContent = null, keychainContent = null } = options;
mockReadFile.mockImplementation((filePath: string) => {
if (filePath.includes("claude") && credFileContent !== null) {
return JSON.stringify(credFileContent);
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation((cmd: string, _args: string[]) => {
// Keychain read via `security` command
if (cmd === "security") {
if (keychainContent !== null) {
return JSON.stringify(keychainContent);
}
throw new Error("Keychain item not found");
}
throw new Error(`Unexpected command: ${cmd}`);
});
}
/**
* Helper to set up mock HTTPS request for Claude usage API.
*/
function setupClaudeApiResponse(mockResponse: any, statusCode = 200) {
const mockReq = {
on: vi.fn(),
write: vi.fn(),
end: vi.fn(),
};
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") {
handler(Buffer.from(JSON.stringify(mockResponse)));
}
if (event === "end") {
handler();
}
}),
};
callback(mockRes);
return mockReq;
});
}
it("detects no auth when credentials file doesn't exist and keychain fails", async () => {
mockReadFile.mockImplementation(async () => {
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude");
expect(claude).toBeDefined();
expect(claude!.status).toBe("no-auth");
expect(claude!.error).toContain("No Claude CLI credentials");
});
it("reads credentials from macOS keychain when file paths fail", async () => {
setupClaudeMocks({
keychainContent: {
claudeAiOauth: {
accessToken: "keychain-token",
scopes: ["user:profile"],
subscriptionType: "pro",
},
},
});
setupClaudeApiResponse({
five_hour: {
utilization: 30.0,
resets_at: new Date(Date.now() + 3 * 60 * 60 * 1000).toISOString(),
},
seven_day: {
utilization: 15.0,
resets_at: new Date(Date.now() + 4 * 24 * 60 * 60 * 1000).toISOString(),
},
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("ok");
expect(claude.plan).toBe("Pro");
expect(claude.windows).toHaveLength(2);
// Verify keychain command was called with correct arguments
expect(mockExecFileSync).toHaveBeenCalledWith(
"security",
["find-generic-password", "-s", "Claude Code-credentials", "-w"],
{ encoding: "utf-8", timeout: 5000 }
);
});
it("parses keychain credentials with rateLimitTier for plan detection", async () => {
setupClaudeMocks({
keychainContent: {
claudeAiOauth: {
accessToken: "keychain-token",
scopes: ["user:profile"],
rateLimitTier: "default_claude_max_20x",
},
},
});
setupClaudeApiResponse({
five_hour: {
utilization: 25.0,
resets_at: new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(),
},
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("ok");
expect(claude.plan).toBe("Max");
});
it("detects missing scope error", async () => {
mockReadFile.mockImplementation((filePath: string) => {
if (filePath.includes("claude")) {
return JSON.stringify({
accessToken: "test-token",
scopes: ["other:scope"], // missing user:profile
});
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude");
expect(claude!.status).toBe("no-auth");
expect(claude!.error).toContain("user:profile scope");
});
it("parses usage data from API response", async () => {
setupClaudeMocks({
credFileContent: {
accessToken: "test-token",
scopes: ["user:profile"],
subscriptionType: "pro",
},
});
setupClaudeApiResponse({
five_hour: {
utilization: 45.5,
resets_at: new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(),
},
seven_day: {
utilization: 23.0,
resets_at: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toISOString(),
},
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("ok");
expect(claude.plan).toBe("Pro");
expect(claude.windows).toHaveLength(2);
const sessionWindow = claude.windows.find((w) => w.label.includes("Session"));
expect(sessionWindow).toBeDefined();
expect(sessionWindow!.percentUsed).toBe(45.5);
expect(sessionWindow!.percentLeft).toBe(54.5);
expect(sessionWindow!.resetText).toContain("resets in");
});
it("parses all four usage windows from API response", async () => {
setupClaudeMocks({
credFileContent: {
accessToken: "test-token",
scopes: ["user:profile"],
subscriptionType: "max",
},
});
setupClaudeApiResponse({
five_hour: {
utilization: 40.0,
resets_at: new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(),
},
seven_day: {
utilization: 20.0,
resets_at: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toISOString(),
},
seven_day_sonnet: {
utilization: 15.0,
resets_at: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toISOString(),
},
seven_day_opus: {
utilization: 5.0,
resets_at: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toISOString(),
},
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("ok");
expect(claude.windows).toHaveLength(4);
expect(claude.windows.map((w) => w.label)).toEqual([
"Session (5h)",
"Weekly",
"Weekly (Sonnet)",
"Weekly (Opus)",
]);
});
it("falls back to CLI parsing on 429 rate limit", async () => {
setupClaudeMocks({
credFileContent: {
accessToken: "test-token",
scopes: ["user:profile"],
},
});
// No-op sleep so retries don't actually wait
_setSleepFn(async () => {});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 429,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from('{"error":"rate_limited"}'));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
// After 429 retries exhausted, falls back to CLI which will fail in test
// (node-pty not available) — so we get the CLI fallback error
expect(claude.status).toBe("error");
// Should have retried 3 times (CLAUDE_MAX_RETRIES)
expect(mockRequest).toHaveBeenCalledTimes(3);
_resetSleepFn();
});
it("handles 401 auth error", async () => {
setupClaudeMocks({
credFileContent: {
accessToken: "expired-token",
scopes: ["user:profile"],
},
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 401,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from('{"error": "unauthorized"}'));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
// Auth failure falls back to CLI (which fails in test env due to mocked node-pty)
expect(claude.status).toBe("error");
});
it("handles 403 auth error", async () => {
setupClaudeMocks({
credFileContent: {
accessToken: "forbidden-token",
scopes: ["user:profile"],
},
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 403,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from('{"error": "forbidden"}'));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
// Auth failure falls back to CLI (which fails in test env due to mocked node-pty)
expect(claude.status).toBe("error");
});
it("sends anthropic-beta=oauth-2025-04-20 header so the OAuth usage endpoint authorizes the request", async () => {
const mockResponse = {
five_hour: { utilization: 10.0 },
};
mockReadFile.mockImplementation((path: string) => {
if (path.includes("claude")) {
return JSON.stringify({
accessToken: "test-token",
scopes: ["user:profile"],
});
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
let capturedHeaders: Record<string, string> = {};
const mockReq = {
on: vi.fn(),
write: vi.fn(),
end: vi.fn(),
};
mockRequest.mockImplementation((options: any, callback: any) => {
capturedHeaders = options.headers || {};
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") {
handler(Buffer.from(JSON.stringify(mockResponse)));
}
if (event === "end") {
handler();
}
}),
};
callback(mockRes);
return mockReq;
});
await fetchAllProviderUsage();
// Anthropic now requires the `anthropic-beta: oauth-2025-04-20` header on
// /api/oauth/usage for OAuth-scoped tokens; without it the endpoint
// replies with 401 "OAuth authentication is currently not supported".
// The value mirrors what the Claude CLI sends from `claude /usage`.
expect(capturedHeaders["anthropic-beta"]).toBe("oauth-2025-04-20");
});
it("retries on 429 and succeeds after transient rate limit", async () => {
const noopSleep = vi.fn().mockResolvedValue(undefined);
_setSleepFn(noopSleep);
const mockResponse = {
five_hour: {
utilization: 20.0,
resets_at: new Date(Date.now() + 3 * 60 * 60 * 1000).toISOString(),
},
};
mockReadFile.mockImplementation((path: string) => {
if (path.includes("claude")) {
return JSON.stringify({
accessToken: "test-token",
scopes: ["user:profile"],
});
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
let callCount = 0;
const mockReq = {
on: vi.fn(),
write: vi.fn(),
end: vi.fn(),
};
mockRequest.mockImplementation((options: any, callback: any) => {
callCount++;
const is429 = callCount <= 2; // First 2 calls return 429, third succeeds
const mockRes = {
statusCode: is429 ? 429 : 200,
headers: is429 ? { "retry-after": "1" } : {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") {
const body = is429
? '{"error":"rate_limited"}'
: JSON.stringify(mockResponse);
handler(Buffer.from(body));
}
if (event === "end") {
handler();
}
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("ok");
expect(claude.windows).toHaveLength(1);
expect(claude.windows[0].percentUsed).toBe(20);
// Verify sleep was called for retries (2 retry sleeps)
expect(noopSleep).toHaveBeenCalledTimes(2);
_resetSleepFn();
});
it("handles malformed JSON response", async () => {
setupClaudeMocks({
credFileContent: {
accessToken: "test-token",
scopes: ["user:profile"],
},
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from("not valid json {{{"));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("error");
expect(claude.error).toBeDefined();
});
it("reports rate limited after all retries exhausted on 429", async () => {
const noopSleep = vi.fn().mockResolvedValue(undefined);
_setSleepFn(noopSleep);
mockReadFile.mockImplementation((path: string) => {
if (path.includes("claude")) {
return JSON.stringify({
accessToken: "test-token",
scopes: ["user:profile"],
});
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockReq = {
on: vi.fn(),
write: vi.fn(),
end: vi.fn(),
};
// Always return 429
mockRequest.mockImplementation((options: any, callback: any) => {
const mockRes = {
statusCode: 429,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") {
handler(Buffer.from('{"error":"rate_limited"}'));
}
if (event === "end") {
handler();
}
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
// After 429 retries exhausted, falls back to CLI which fails in test
// (node-pty not available) — so we get the CLI fallback error
expect(claude.status).toBe("error");
// Verify retries happened (2 sleeps for 3 attempts)
expect(noopSleep).toHaveBeenCalledTimes(2);
_resetSleepFn();
});
it("uses exponential backoff delays when retry-after header is absent", async () => {
const noopSleep = vi.fn().mockResolvedValue(undefined);
_setSleepFn(noopSleep);
mockReadFile.mockImplementation((path: string) => {
if (path.includes("claude")) {
return JSON.stringify({
accessToken: "test-token",
scopes: ["user:profile"],
});
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockReq = {
on: vi.fn(),
write: vi.fn(),
end: vi.fn(),
};
// Always return 429 without retry-after
mockRequest.mockImplementation((options: any, callback: any) => {
const mockRes = {
statusCode: 429,
headers: {}, // No retry-after header
on: vi.fn((event: string, handler: any) => {
if (event === "data") {
handler(Buffer.from('{"error":"rate_limited"}'));
}
if (event === "end") {
handler();
}
}),
};
callback(mockRes);
return mockReq;
});
await fetchAllProviderUsage();
// Exponential backoff: 1000ms * 2^0 = 1000, 1000ms * 2^1 = 2000
expect(noopSleep).toHaveBeenCalledTimes(2);
expect(noopSleep).toHaveBeenNthCalledWith(1, 1000);
expect(noopSleep).toHaveBeenNthCalledWith(2, 2000);
_resetSleepFn();
});
it("respects retry-after header value for delay", async () => {
const noopSleep = vi.fn().mockResolvedValue(undefined);
_setSleepFn(noopSleep);
mockReadFile.mockImplementation((path: string) => {
if (path.includes("claude")) {
return JSON.stringify({
accessToken: "test-token",
scopes: ["user:profile"],
});
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockReq = {
on: vi.fn(),
write: vi.fn(),
end: vi.fn(),
};
// 429 with retry-after: 5 seconds
mockRequest.mockImplementation((options: any, callback: any) => {
const mockRes = {
statusCode: 429,
headers: { "retry-after": "5" },
on: vi.fn((event: string, handler: any) => {
if (event === "data") {
handler(Buffer.from('{"error":"rate_limited"}'));
}
if (event === "end") {
handler();
}
}),
};
callback(mockRes);
return mockReq;
});
await fetchAllProviderUsage();
// Should use retry-after value (5s = 5000ms) for both retries
expect(noopSleep).toHaveBeenCalledTimes(2);
expect(noopSleep).toHaveBeenNthCalledWith(1, 5000);
expect(noopSleep).toHaveBeenNthCalledWith(2, 5000);
_resetSleepFn();
});
it("populates resetAt timestamp for session window from API response", async () => {
const resetTime = new Date(Date.now() + 2 * 60 * 60 * 1000);
setupClaudeMocks({
credFileContent: {
accessToken: "test-token",
scopes: ["user:profile"],
subscriptionType: "pro",
},
});
setupClaudeApiResponse({
five_hour: {
utilization: 45.5,
resets_at: resetTime.toISOString(),
},
seven_day: {
utilization: 23.0,
resets_at: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toISOString(),
},
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("ok");
expect(claude.windows).toHaveLength(2);
const sessionWindow = claude.windows.find((w) => w.label.includes("Session"))!;
expect(sessionWindow.resetAt).toBe(resetTime.toISOString());
expect(sessionWindow.resetText).toContain("resets in");
const weeklyWindow = claude.windows.find((w) => w.label === "Weekly")!;
expect(weeklyWindow.resetAt).toBeDefined();
});
it("uses fallback reset time for session window when API omits resets_at", async () => {
setupClaudeMocks({
credFileContent: {
accessToken: "test-token",
scopes: ["user:profile"],
subscriptionType: "pro",
},
});
setupClaudeApiResponse({
five_hour: {
utilization: 30.0,
// no resets_at field — triggers fallback
},
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("ok");
const sessionWindow = claude.windows.find((w) => w.label.includes("Session"))!;
expect(sessionWindow.resetAt).toBeDefined();
const resetAtDate = new Date(sessionWindow.resetAt!);
const expectedMs = Date.now() + 5 * 60 * 60 * 1000;
expect(Math.abs(resetAtDate.getTime() - expectedMs)).toBeLessThan(1000);
// Fallback: when resets_at is missing, session window gets resetMs = 5h
expect(sessionWindow.resetMs).toBe(5 * 60 * 60 * 1000);
expect(sessionWindow.resetText).toBe("resets in 5h");
});
it("calculates pace for session window with fallback reset time", async () => {
setupClaudeMocks({
credFileContent: {
accessToken: "test-token",
scopes: ["user:profile"],
subscriptionType: "pro",
},
});
setupClaudeApiResponse({
five_hour: {
utilization: 50.0,
// no resets_at — fallback uses full 5h duration
},
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("ok");
const sessionWindow = claude.windows.find((w) => w.label.includes("Session"))!;
expect(sessionWindow.percentUsed).toBe(50);
// With fallback resetMs = 5h and percentUsed = 50%, the window duration
// and reset time are both set, enabling pace indicator on the frontend.
expect(sessionWindow.windowDurationMs).toBe(5 * 60 * 60 * 1000);
expect(sessionWindow.resetMs).toBe(5 * 60 * 60 * 1000);
expect(sessionWindow.resetText).toBe("resets in 5h");
expect(sessionWindow.resetAt).toBeDefined();
const resetAtDate = new Date(sessionWindow.resetAt!);
const expectedMs = Date.now() + 5 * 60 * 60 * 1000;
expect(Math.abs(resetAtDate.getTime() - expectedMs)).toBeLessThan(1000);
});
it("handles empty JSON object from API gracefully", async () => {
setupClaudeMocks({
credFileContent: {
accessToken: "test-token",
scopes: ["user:profile"],
subscriptionType: "pro",
},
});
setupClaudeApiResponse({});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("ok");
expect(claude.plan).toBe("Pro");
expect(claude.windows).toHaveLength(0);
});
it("parses session window when API uses 'session' key instead of 'five_hour'", async () => {
setupClaudeMocks({
credFileContent: {
accessToken: "test-token",
scopes: ["user:profile"],
subscriptionType: "pro",
},
});
// API response uses 'session' key instead of 'five_hour'
setupClaudeApiResponse({
session: {
utilization: 35.0,
resets_at: new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(),
},
seven_day: {
utilization: 20.0,
resets_at: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toISOString(),
},
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("ok");
expect(claude.windows).toHaveLength(2);
// Verify session window is correctly parsed
const sessionWindow = claude.windows.find((w) => w.label.includes("Session"));
expect(sessionWindow).toBeDefined();
expect(sessionWindow!.percentUsed).toBe(35);
expect(sessionWindow!.percentLeft).toBe(65);
expect(sessionWindow!.resetText).toContain("resets in");
expect(sessionWindow!.resetAt).toBeDefined();
});
it("parses utilization from alternative field names", async () => {
setupClaudeMocks({
credFileContent: {
accessToken: "test-token",
scopes: ["user:profile"],
subscriptionType: "pro",
},
});
// Test various field name variations
setupClaudeApiResponse({
five_hour: {
percent_used: 42.0,
resets_at: new Date(Date.now() + 3 * 60 * 60 * 1000).toISOString(),
},
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("ok");
const sessionWindow = claude.windows.find((w) => w.label.includes("Session"))!;
expect(sessionWindow.percentUsed).toBe(42);
});
it("parses reset_at from alternative field names", async () => {
setupClaudeMocks({
credFileContent: {
accessToken: "test-token",
scopes: ["user:profile"],
subscriptionType: "pro",
},
});
// Test various reset time field name variations
setupClaudeApiResponse({
five_hour: {
utilization: 30.0,
reset_at: new Date(Date.now() + 4 * 60 * 60 * 1000).toISOString(),
},
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("ok");
const sessionWindow = claude.windows.find((w) => w.label.includes("Session"))!;
expect(sessionWindow.resetAt).toBeDefined();
expect(sessionWindow.resetText).toContain("resets in");
});
it("preserves plan detection from subscriptionType in credentials", async () => {
setupClaudeMocks({
credFileContent: {
accessToken: "test-token",
scopes: ["user:profile"],
subscriptionType: "team",
},
});
setupClaudeApiResponse({ five_hour: { utilization: 10 } });
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.plan).toBe("Team");
});
it("does not retry on 401 auth errors", async () => {
const noopSleep = vi.fn().mockResolvedValue(undefined);
_setSleepFn(noopSleep);
mockReadFile.mockImplementation((path: string) => {
if (path.includes("claude")) {
return JSON.stringify({
accessToken: "expired-token",
scopes: ["user:profile"],
});
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockReq = {
on: vi.fn(),
write: vi.fn(),
end: vi.fn(),
};
mockRequest.mockImplementation((options: any, callback: any) => {
const mockRes = {
statusCode: 401,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") {
handler(Buffer.from('{"error": "unauthorized"}'));
}
if (event === "end") {
handler();
}
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
// Auth failure falls back to CLI (which fails in test env due to mocked node-pty)
expect(claude.status).toBe("error");
// No retries should happen for auth errors
expect(noopSleep).not.toHaveBeenCalled();
_resetSleepFn();
});
it("preserves plan detection from rateLimitTier for Pro", async () => {
setupClaudeMocks({
keychainContent: {
claudeAiOauth: {
accessToken: "test-token",
scopes: ["user:profile"],
rateLimitTier: "default_claude_pro_5x",
},
},
});
setupClaudeApiResponse({ five_hour: { utilization: 10 } });
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.plan).toBe("Pro");
});
it("does not retry on 403 auth errors", async () => {
const noopSleep = vi.fn().mockResolvedValue(undefined);
_setSleepFn(noopSleep);
mockReadFile.mockImplementation((path: string) => {
if (path.includes("claude")) {
return JSON.stringify({
accessToken: "forbidden-token",
scopes: ["user:profile"],
});
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockReq = {
on: vi.fn(),
write: vi.fn(),
end: vi.fn(),
};
mockRequest.mockImplementation((options: any, callback: any) => {
const mockRes = {
statusCode: 403,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") {
handler(Buffer.from('{"error": "forbidden"}'));
}
if (event === "end") {
handler();
}
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
// Auth failure falls back to CLI (which fails in test env due to mocked node-pty)
expect(claude.status).toBe("error");
// No retries should happen for auth errors
expect(noopSleep).not.toHaveBeenCalled();
_resetSleepFn();
});
describe("token refresh", () => {
it("refreshes expired token before calling usage API", async () => {
const expiredAt = Date.now() - 60_000; // expired 1 minute ago
setupClaudeMocks({
credFileContent: {
claudeAiOauth: {
accessToken: "expired-token",
expiresAt: expiredAt,
refreshToken: "refresh-token-123",
scopes: ["user:profile"],
subscriptionType: "max",
},
},
});
// Track which requests are made
const requestUrls: string[] = [];
const capturedOptions: any[] = [];
const capturedBodies: string[] = [];
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((options: any, callback: any) => {
const url = `https://${options.hostname}${options.path}`;
requestUrls.push(url);
capturedOptions.push(options);
if (url.includes("oauth/token")) {
// Token refresh succeeds
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify({ access_token: "new-fresh-token" })));
if (event === "end") handler();
}),
};
callback(mockRes);
} else {
// Usage API succeeds
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify({
five_hour: { utilization: 50.0, resets_at: new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString() },
})));
if (event === "end") handler();
}),
};
callback(mockRes);
}
return mockReq;
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("ok");
expect(claude.windows).toHaveLength(1);
expect(claude.windows[0].percentUsed).toBe(50);
// Should have called token refresh first, then usage API
expect(requestUrls).toHaveLength(2);
expect(requestUrls[0]).toContain("oauth/token");
expect(requestUrls[1]).toContain("oauth/usage");
});
it("sends refresh request to platform.claude.com with correct content-type and client_id", async () => {
const expiredAt = Date.now() - 60_000;
setupClaudeMocks({
credFileContent: {
claudeAiOauth: {
accessToken: "expired-token",
expiresAt: expiredAt,
refreshToken: "refresh-token-456",
scopes: ["user:profile"],
subscriptionType: "max",
},
},
});
const capturedOptions: any[] = [];
const capturedBodies: string[] = [];
const mockReq = {
on: vi.fn(),
write: vi.fn((data: string) => capturedBodies.push(data)),
end: vi.fn(),
};
mockRequest.mockImplementation((options: any, callback: any) => {
capturedOptions.push({ ...options });
const url = `https://${options.hostname}${options.path}`;
if (url.includes("oauth/token")) {
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify({ access_token: "refreshed-token" })));
if (event === "end") handler();
}),
};
callback(mockRes);
} else {
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify({
five_hour: { utilization: 10.0, resets_at: new Date(Date.now() + 3600000).toISOString() },
})));
if (event === "end") handler();
}),
};
callback(mockRes);
}
return mockReq;
});
await fetchAllProviderUsage();
// Verify the refresh request (first call). Body is JSON with a `scope`
// field — matches what the Claude CLI sends; form-urlencoded bodies or
// missing scope cause Anthropic to reject the refresh.
const refreshOpts = capturedOptions[0];
expect(refreshOpts.hostname).toBe("platform.claude.com");
expect(refreshOpts.path).toBe("/v1/oauth/token");
expect(refreshOpts.headers["content-type"]).toBe("application/json");
expect(capturedBodies.length).toBeGreaterThanOrEqual(1);
const body = JSON.parse(capturedBodies[0]);
expect(body.grant_type).toBe("refresh_token");
expect(body.refresh_token).toBe("refresh-token-456");
expect(body.client_id).toBe("9d1c250a-e61b-44d9-88ed-5944d1962f5e");
expect(body.scope).toBe("user:profile");
});
it("falls back to CLI when refresh fails for expired token", async () => {
const expiredAt = Date.now() - 60_000;
setupClaudeMocks({
credFileContent: {
claudeAiOauth: {
accessToken: "expired-token",
expiresAt: expiredAt,
refreshToken: "bad-refresh-token",
scopes: ["user:profile"],
},
},
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 400,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from('{"error":"invalid_grant"}'));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
// Auth failure falls back to CLI (which fails in test env due to mocked node-pty)
expect(claude.status).toBe("error");
// Should NOT show "Claude token expired" — CLI fallback was attempted
expect(claude.error).not.toContain("Claude token expired");
});
it("falls back to CLI when no refresh token and token is expired", async () => {
const expiredAt = Date.now() - 60_000;
setupClaudeMocks({
credFileContent: {
claudeAiOauth: {
accessToken: "expired-token",
expiresAt: expiredAt,
// No refreshToken
scopes: ["user:profile"],
},
},
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
// Auth failure falls back to CLI (which fails in test env due to mocked node-pty)
expect(claude.status).toBe("error");
// Should NOT show "Claude token expired" — CLI fallback was attempted
expect(claude.error).not.toContain("Claude token expired");
});
it("does not refresh when token is not expired", async () => {
const expiresAt = Date.now() + 3600_000; // expires in 1 hour
setupClaudeMocks({
credFileContent: {
claudeAiOauth: {
accessToken: "valid-token",
expiresAt,
refreshToken: "refresh-token",
scopes: ["user:profile"],
},
},
});
setupClaudeApiResponse({
five_hour: { utilization: 10.0 },
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("ok");
// Only the usage API call should have been made, no refresh
expect(mockRequest).toHaveBeenCalledTimes(1);
});
it("attempts refresh on 401 response as recovery", async () => {
setupClaudeMocks({
credFileContent: {
claudeAiOauth: {
accessToken: "stale-token",
// No expiresAt — so won't pre-refresh
refreshToken: "refresh-token-456",
scopes: ["user:profile"],
},
},
});
let callCount = 0;
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((options: any, callback: any) => {
callCount++;
const url = `https://${options.hostname}${options.path}`;
if (url.includes("oauth/token")) {
// Refresh succeeds
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify({ access_token: "refreshed-token" })));
if (event === "end") handler();
}),
};
callback(mockRes);
} else if (callCount === 1) {
// First usage call returns 401
const mockRes = {
statusCode: 401,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from('{"error":"unauthorized"}'));
if (event === "end") handler();
}),
};
callback(mockRes);
} else {
// Second usage call with refreshed token succeeds
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify({
five_hour: { utilization: 30.0 },
})));
if (event === "end") handler();
}),
};
callback(mockRes);
}
return mockReq;
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("ok");
expect(claude.windows).toHaveLength(1);
expect(claude.windows[0].percentUsed).toBe(30);
});
it("treats token as expired when within 60s buffer of expiresAt", async () => {
const expiresAt = Date.now() + 30_000; // expires in 30s (within buffer)
setupClaudeMocks({
credFileContent: {
claudeAiOauth: {
accessToken: "almost-expired-token",
expiresAt,
refreshToken: "refresh-token",
scopes: ["user:profile"],
},
},
});
const requestUrls: string[] = [];
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((options: any, callback: any) => {
const url = `https://${options.hostname}${options.path}`;
requestUrls.push(url);
if (url.includes("oauth/token")) {
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify({ access_token: "fresh-token" })));
if (event === "end") handler();
}),
};
callback(mockRes);
} else {
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify({ five_hour: { utilization: 5.0 } })));
if (event === "end") handler();
}),
};
callback(mockRes);
}
return mockReq;
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("ok");
// Token should have been refreshed (within 60s buffer)
expect(requestUrls[0]).toContain("oauth/token");
});
});
describe("CLI fallback on auth failures", () => {
it("falls back to CLI when expired token refresh fails", async () => {
const expiredAt = Date.now() - 60_000;
setupClaudeMocks({
credFileContent: {
claudeAiOauth: {
accessToken: "expired-token",
expiresAt: expiredAt,
refreshToken: "bad-refresh-token",
scopes: ["user:profile"],
},
},
});
// Mock refresh endpoint to return failure
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 400,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from('{"error":"invalid_grant"}'));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
// Should fall back to CLI instead of showing "Claude token expired"
expect(claude.status).toBe("error");
expect(claude.error).not.toContain("Claude token expired");
});
it("falls back to CLI when no refresh token is available", async () => {
const expiredAt = Date.now() - 60_000;
setupClaudeMocks({
credFileContent: {
claudeAiOauth: {
accessToken: "expired-token",
expiresAt: expiredAt,
// No refreshToken
scopes: ["user:profile"],
},
},
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
// Should fall back to CLI instead of showing "Claude token expired"
expect(claude.status).toBe("error");
expect(claude.error).not.toContain("Claude token expired");
});
it("falls back to CLI when API returns 401 and refresh fails", async () => {
setupClaudeMocks({
credFileContent: {
claudeAiOauth: {
accessToken: "stale-token",
// No expiresAt — so won't pre-refresh
refreshToken: "bad-refresh-token",
scopes: ["user:profile"],
},
},
});
let callCount = 0;
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((options: any, callback: any) => {
callCount++;
const url = `https://${options.hostname}${options.path}`;
if (url.includes("oauth/token")) {
// Refresh fails
const mockRes = {
statusCode: 400,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from('{"error":"invalid_grant"}'));
if (event === "end") handler();
}),
};
callback(mockRes);
} else {
// Usage API returns 401
const mockRes = {
statusCode: 401,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from('{"error":"unauthorized"}'));
if (event === "end") handler();
}),
};
callback(mockRes);
}
return mockReq;
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
// Should fall back to CLI instead of showing "Claude token expired"
expect(claude.status).toBe("error");
expect(claude.error).not.toContain("Claude token expired");
});
});
});
describe("Codex provider", () => {
it("detects no auth when auth.json doesn't exist", async () => {
mockReadFile.mockImplementation(async () => {
return Promise.reject(new Error("File not found"));
});
const providers = await fetchAllProviderUsage();
const codex = providers.find((p) => p.name === "Codex");
expect(codex).toBeDefined();
expect(codex!.status).toBe("no-auth");
expect(codex!.error).toContain("No Codex credentials");
});
it("parses usage data from API response", async () => {
const mockResponse = {
email: "test@example.com",
plan_type: "pro",
rate_limit: {
primary_window: {
used_percent: 67.5,
limit_window_seconds: 5 * 60 * 60, // 5 hours
reset_after_seconds: 2 * 60 * 60, // 2 hours
},
secondary_window: {
used_percent: 12.0,
limit_window_seconds: 7 * 24 * 60 * 60, // 7 days
reset_after_seconds: 5 * 24 * 60 * 60, // 5 days
},
},
};
mockReadFile.mockImplementation((path: string) => {
if (path.includes("codex")) {
return JSON.stringify({
tokens: {
access_token: "test-token",
id_token: "header.eyJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.signature",
},
});
}
return Promise.reject(new Error("File not found"));
});
const mockReq = {
on: vi.fn(),
write: vi.fn(),
end: vi.fn(),
};
mockRequest.mockImplementation((options: any, callback: any) => {
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") {
handler(Buffer.from(JSON.stringify(mockResponse)));
}
if (event === "end") {
handler();
}
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const codex = providers.find((p) => p.name === "Codex")!;
expect(codex.status).toBe("ok");
expect(codex.email).toBe("test@example.com");
expect(codex.plan).toBe("Pro");
expect(codex.windows).toHaveLength(2);
const sessionWindow = codex.windows.find((w) => w.label.includes("Session"));
expect(sessionWindow).toBeDefined();
expect(sessionWindow!.percentUsed).toBe(67.5);
expect(sessionWindow!.percentLeft).toBe(32.5);
});
it("sets resetAt from reset_at timestamp", async () => {
const resetAtTimestamp = Math.floor(Date.now() / 1000) + 2 * 60 * 60; // 2 hours from now in seconds
const mockResponse = {
rate_limit: {
primary_window: {
used_percent: 50,
reset_at: resetAtTimestamp,
limit_window_seconds: 5 * 60 * 60,
},
},
};
mockReadFile.mockImplementation((path: string) => {
if (path.includes("codex")) {
return JSON.stringify({
tokens: {
access_token: "test-token",
},
});
}
return Promise.reject(new Error("File not found"));
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify(mockResponse)));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const codex = providers.find((p) => p.name === "Codex")!;
expect(codex.status).toBe("ok");
const sessionWindow = codex.windows.find((w) => w.label.includes("Session"))!;
expect(sessionWindow.resetAt).toBe(new Date(resetAtTimestamp * 1000).toISOString());
});
it("sets resetAt from reset_after_seconds fallback", async () => {
const mockResponse = {
rate_limit: {
primary_window: {
used_percent: 50,
reset_after_seconds: 7200, // 2 hours
limit_window_seconds: 5 * 60 * 60,
},
},
};
mockReadFile.mockImplementation((path: string) => {
if (path.includes("codex")) {
return JSON.stringify({
tokens: {
access_token: "test-token",
},
});
}
return Promise.reject(new Error("File not found"));
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify(mockResponse)));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const codex = providers.find((p) => p.name === "Codex")!;
expect(codex.status).toBe("ok");
const sessionWindow = codex.windows.find((w) => w.label.includes("Session"))!;
expect(sessionWindow.resetAt).toBeDefined();
// resetAt should be approximately Date.now() + 7200000
const resetAtDate = new Date(sessionWindow.resetAt!);
const expectedMs = Date.now() + 7200 * 1000;
expect(Math.abs(resetAtDate.getTime() - expectedMs)).toBeLessThan(1000);
});
});
describe("Gemini provider", () => {
it("detects no auth when oauth_creds.json doesn't exist", async () => {
mockReadFile.mockImplementation(async () => {
return Promise.reject(new Error("File not found"));
});
const providers = await fetchAllProviderUsage();
const gemini = providers.find((p) => p.name === "Gemini");
expect(gemini).toBeDefined();
expect(gemini!.status).toBe("no-auth");
expect(gemini!.error).toContain("No Gemini credentials");
});
it("parses usage buckets from API response", async () => {
const mockResponse = {
buckets: [
{
modelId: "gemini-2.0-flash",
remainingFraction: 0.85,
resetTime: new Date(Date.now() + 2 * 60 * 60 * 1000).toISOString(),
},
{
modelId: "gemini-2.0-pro",
remainingFraction: 0.92,
resetTime: new Date(Date.now() + 3 * 60 * 60 * 1000).toISOString(),
},
],
};
mockReadFile.mockImplementation((path: string) => {
if (path.includes("gemini")) {
if (path.includes("oauth_creds")) {
return JSON.stringify({
access_token: "test-token",
id_token: "header.eyJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.signature",
});
}
// settings.json doesn't exist (oauth-personal is default)
return Promise.reject(new Error("File not found"));
}
return Promise.reject(new Error("File not found"));
});
const mockReq = {
on: vi.fn(),
write: vi.fn(),
end: vi.fn(),
};
mockRequest.mockImplementation((options: any, callback: any) => {
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") {
handler(Buffer.from(JSON.stringify(mockResponse)));
}
if (event === "end") {
handler();
}
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const gemini = providers.find((p) => p.name === "Gemini")!;
expect(gemini.status).toBe("ok");
expect(gemini.email).toBe("test@example.com");
expect(gemini.windows).toHaveLength(2);
const flashWindow = gemini.windows.find((w) => w.label.includes("Flash"));
expect(flashWindow).toBeDefined();
expect(flashWindow!.percentUsed).toBe(15); // 100 - 85
expect(flashWindow!.percentLeft).toBe(85);
});
it("sets resetAt from resetTime in bucket data", async () => {
const resetTime = new Date(Date.now() + 2 * 60 * 60 * 1000);
const mockResponse = {
buckets: [
{
modelId: "gemini-2.0-flash",
remainingFraction: 0.85,
resetTime: resetTime.toISOString(),
},
],
};
mockReadFile.mockImplementation((path: string) => {
if (path.includes("gemini")) {
if (path.includes("oauth_creds")) {
return JSON.stringify({
access_token: "test-token",
id_token: "header.eyJlbWFpbCI6InRlc3RAZXhhbXBsZS5jb20ifQ.signature",
});
}
return Promise.reject(new Error("File not found"));
}
return Promise.reject(new Error("File not found"));
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify(mockResponse)));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const gemini = providers.find((p) => p.name === "Gemini")!;
expect(gemini.status).toBe("ok");
const flashWindow = gemini.windows.find((w) => w.label.includes("Flash"))!;
expect(flashWindow.resetAt).toBe(new Date(resetTime).toISOString());
});
it("handles unsupported auth type (api-key)", async () => {
mockReadFile.mockImplementation((path: string) => {
if (path.includes("gemini")) {
if (path.includes("oauth_creds")) {
return JSON.stringify({
access_token: "test-token",
});
}
if (path.includes("settings")) {
return JSON.stringify({
security: {
auth: {
selectedType: "api-key",
},
},
});
}
}
return Promise.reject(new Error("File not found"));
});
const providers = await fetchAllProviderUsage();
const gemini = providers.find((p) => p.name === "Gemini")!;
expect(gemini.status).toBe("error");
expect(gemini.error).toContain("Unsupported auth type");
});
});
describe("Minimax provider", () => {
it("detects no auth when pi auth.json doesn't exist", async () => {
mockReadFile.mockImplementation(async () => {
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const providers = await fetchAllProviderUsage();
const minimax = providers.find((p) => p.name === "Minimax");
expect(minimax).toBeDefined();
expect(minimax!.status).toBe("no-auth");
expect(minimax!.error).toContain("No Minimax credentials");
});
it("detects no auth when minimax entry has no key", async () => {
mockReadFile.mockImplementation((filePath: string) => {
if (filePath.includes(".pi/agent/auth.json")) {
return JSON.stringify({
minimax: { type: "api_key" /* missing key */ },
});
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const providers = await fetchAllProviderUsage();
const minimax = providers.find((p) => p.name === "Minimax");
expect(minimax!.status).toBe("no-auth");
expect(minimax!.error).toContain("No Minimax credentials");
});
it("detects no auth when minimax entry is missing entirely", async () => {
mockReadFile.mockImplementation((filePath: string) => {
if (filePath.includes(".pi/agent/auth.json")) {
return JSON.stringify({ /* no minimax key */ });
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const providers = await fetchAllProviderUsage();
const minimax = providers.find((p) => p.name === "Minimax");
expect(minimax!.status).toBe("no-auth");
expect(minimax!.error).toContain("No Minimax credentials");
});
it("parses usage data from coding_plan/remains API response", async () => {
const now = Date.now();
const mockResponse = {
model_remains: [
{
model_name: "MiniMax-M*",
current_interval_total_count: 4500,
// Note: current_interval_usage_count is actually REMAINING, not used
current_interval_usage_count: 4000,
remains_time: now + 3 * 60 * 60 * 1000 - now, // ms remaining
start_time: now - 2 * 60 * 60 * 1000,
end_time: now + 3 * 60 * 60 * 1000,
},
{
model_name: "speech-hd",
current_interval_total_count: 9000,
current_interval_usage_count: 8000,
remains_time: 76919205,
start_time: now,
end_time: now + 24 * 60 * 60 * 1000,
},
],
};
mockReadFile.mockImplementation((filePath: string) => {
if (filePath.includes(".pi/agent/auth.json")) {
return JSON.stringify({
minimax: { type: "api_key", key: "test-api-key" },
});
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockReq = {
on: vi.fn(),
write: vi.fn(),
end: vi.fn(),
};
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") {
handler(Buffer.from(JSON.stringify(mockResponse)));
}
if (event === "end") {
handler();
}
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const minimax = providers.find((p) => p.name === "Minimax")!;
expect(minimax.status).toBe("ok");
expect(minimax.windows).toHaveLength(2);
const textWindow = minimax.windows.find((w) => w.label === "MiniMax-M*")!;
expect(textWindow).toBeDefined();
// total=4500, remaining=4000, used=500 → 500/4500*100 ≈ 11.1%
expect(textWindow.percentUsed).toBeCloseTo(11.1, 0);
expect(textWindow.percentLeft).toBeGreaterThan(80);
expect(textWindow.resetText).toContain("resets in");
const speechWindow = minimax.windows.find((w) => w.label === "speech-hd")!;
expect(speechWindow).toBeDefined();
});
it("skips models with zero quota", async () => {
const mockResponse = {
model_remains: [
{
model_name: "MiniMax-M*",
current_interval_total_count: 4500,
current_interval_usage_count: 4500,
remains_time: 5000000,
start_time: Date.now() - 1000,
end_time: Date.now() + 5 * 60 * 60 * 1000,
},
{
model_name: "unused-model",
current_interval_total_count: 0,
current_interval_usage_count: 0,
remains_time: 0,
},
],
};
mockReadFile.mockImplementation((filePath: string) => {
if (filePath.includes(".pi/agent/auth.json")) {
return JSON.stringify({
minimax: { type: "api_key", key: "test-api-key" },
});
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify(mockResponse)));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const minimax = providers.find((p) => p.name === "Minimax")!;
expect(minimax.status).toBe("ok");
// Only MiniMax-M* should appear, unused-model has total=0 so skipped
expect(minimax.windows).toHaveLength(1);
expect(minimax.windows[0].label).toBe("MiniMax-M*");
});
it("reads API key from provided AuthStorage before auth files", async () => {
const mockResponse = {
model_remains: [
{
model_name: "MiniMax-M*",
current_interval_total_count: 100,
current_interval_usage_count: 40,
remains_time: 60_000,
start_time: Date.now() - 60_000,
end_time: Date.now() + 60_000,
},
],
};
mockReadFile.mockImplementation(async () => {
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((options: any, callback: any) => {
expect(options.headers.authorization).toBe("Bearer auth-storage-minimax-key");
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify(mockResponse)));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage({
reload: vi.fn(),
hasAuth: vi.fn(() => true),
getApiKey: vi.fn((provider: string) =>
provider === "minimax" ? "auth-storage-minimax-key" : null
),
});
const minimax = providers.find((p) => p.name === "Minimax")!;
expect(minimax.status).toBe("ok");
expect(minimax.windows).toHaveLength(1);
});
it("handles 401 auth error", async () => {
mockReadFile.mockImplementation((filePath: string) => {
if (filePath.includes(".pi/agent/auth.json")) {
return JSON.stringify({
minimax: { type: "api_key", key: "expired-key" },
});
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 401,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from('{"error": "unauthorized"}'));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const minimax = providers.find((p) => p.name === "Minimax")!;
expect(minimax.status).toBe("error");
expect(minimax.error).toContain("Auth expired");
});
it("handles 403 auth error", async () => {
mockReadFile.mockImplementation((filePath: string) => {
if (filePath.includes(".pi/agent/auth.json")) {
return JSON.stringify({
minimax: { type: "api_key", key: "forbidden-key" },
});
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 403,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from('{"error": "forbidden"}'));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const minimax = providers.find((p) => p.name === "Minimax")!;
expect(minimax.status).toBe("error");
expect(minimax.error).toContain("Auth expired");
});
it("sets resetAt from remains_time", async () => {
const remainsTimeMs = 3 * 60 * 60 * 1000; // 3 hours
const mockResponse = {
model_remains: [
{
model_name: "MiniMax-M*",
current_interval_total_count: 4500,
current_interval_usage_count: 4000,
remains_time: remainsTimeMs,
start_time: Date.now() - 2 * 60 * 60 * 1000,
end_time: Date.now() + remainsTimeMs,
},
],
};
mockReadFile.mockImplementation((filePath: string) => {
if (filePath.includes(".pi/agent/auth.json")) {
return JSON.stringify({ minimax: { type: "api_key", key: "test-key" } });
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify(mockResponse)));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const minimax = providers.find((p) => p.name === "Minimax")!;
expect(minimax.status).toBe("ok");
const modelWindow = minimax.windows.find((w) => w.label === "MiniMax-M*")!;
expect(modelWindow.resetAt).toBeDefined();
// resetAt should be approximately Date.now() + remainsTimeMs
const resetAtDate = new Date(modelWindow.resetAt!);
const expectedMs = Date.now() + remainsTimeMs;
expect(Math.abs(resetAtDate.getTime() - expectedMs)).toBeLessThan(1000);
});
});
describe("Zai provider", () => {
it("detects no auth when pi auth.json doesn't exist", async () => {
mockReadFile.mockImplementation(async () => {
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const providers = await fetchAllProviderUsage();
const zai = providers.find((p) => p.name === "Zai");
expect(zai).toBeDefined();
expect(zai!.status).toBe("no-auth");
expect(zai!.error).toContain("No Zai credentials");
});
it("detects no auth when zai entry has no key", async () => {
mockReadFile.mockImplementation((filePath: string) => {
if (filePath.includes(".pi/agent/auth.json")) {
return JSON.stringify({
zai: { type: "api_key" /* missing key */ },
});
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const providers = await fetchAllProviderUsage();
const zai = providers.find((p) => p.name === "Zai");
expect(zai!.status).toBe("no-auth");
expect(zai!.error).toContain("No Zai credentials");
});
it("parses usage data from Z.ai quota API response", async () => {
const mockResponse = {
code: 200,
msg: "Operation successful",
data: {
limits: [
{
type: "TOKENS_LIMIT",
unit: 3,
number: 5,
percentage: 25,
nextResetTime: Date.now() + 3 * 60 * 60 * 1000,
},
{
type: "TIME_LIMIT",
unit: 5,
number: 1,
usage: 4000,
currentValue: 100,
remaining: 3900,
percentage: 2.5,
nextResetTime: Date.now() + 25 * 24 * 60 * 60 * 1000,
},
],
level: "max",
},
success: true,
};
mockReadFile.mockImplementation((filePath: string) => {
if (filePath.includes(".pi/agent/auth.json")) {
return JSON.stringify({
zai: { type: "api_key", key: "test-api-key" },
});
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify(mockResponse)));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const zai = providers.find((p) => p.name === "Zai")!;
expect(zai.status).toBe("ok");
expect(zai.plan).toBe("Max");
expect(zai.windows).toHaveLength(2);
const sessionWindow = zai.windows.find((w) => w.label === "Session (5h)")!;
expect(sessionWindow).toBeDefined();
expect(sessionWindow.percentUsed).toBe(25);
expect(sessionWindow.percentLeft).toBe(75);
expect(sessionWindow.resetText).toContain("resets in");
expect(sessionWindow.windowDurationMs).toBe(5 * 60 * 60 * 1000);
const mcpWindow = zai.windows.find((w) => w.label === "MCP Monthly")!;
expect(mcpWindow).toBeDefined();
expect(mcpWindow.percentUsed).toBe(2.5);
});
it("reads API key from provided AuthStorage before auth files", async () => {
const mockResponse = {
code: 200,
msg: "Operation successful",
data: {
limits: [
{
type: "TOKENS_LIMIT",
percentage: 10,
nextResetTime: Date.now() + 60_000,
},
],
level: "pro",
},
success: true,
};
mockReadFile.mockImplementation(async () => {
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((options: any, callback: any) => {
expect(options.headers.authorization).toBe("auth-storage-zai-key");
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify(mockResponse)));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage({
reload: vi.fn(),
hasAuth: vi.fn(() => true),
get: vi.fn((provider: string) =>
provider === "zai" ? { type: "api_key", key: "auth-storage-zai-key" } : null
),
});
const zai = providers.find((p) => p.name === "Zai")!;
expect(zai.status).toBe("ok");
expect(zai.plan).toBe("Pro");
});
it("falls back to fusion auth files when pi auth files are absent", async () => {
const mockResponse = {
code: 200,
msg: "Operation successful",
data: {
limits: [
{
type: "TOKENS_LIMIT",
percentage: 10,
nextResetTime: Date.now() + 60_000,
},
],
},
success: true,
};
mockReadFile.mockImplementation((filePath: string) => {
if (filePath.includes(".fusion/agent/auth.json")) {
return JSON.stringify({
zai: { type: "api_key", key: "fusion-zai-key" },
});
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((options: any, callback: any) => {
expect(options.headers.authorization).toBe("fusion-zai-key");
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify(mockResponse)));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const zai = providers.find((p) => p.name === "Zai")!;
expect(zai.status).toBe("ok");
});
it("parses only TOKENS_LIMIT when no TIME_LIMIT present", async () => {
const mockResponse = {
code: 200,
msg: "Operation successful",
data: {
limits: [
{
type: "TOKENS_LIMIT",
percentage: 10,
nextResetTime: Date.now() + 4 * 60 * 60 * 1000,
},
],
level: "pro",
},
success: true,
};
mockReadFile.mockImplementation((filePath: string) => {
if (filePath.includes(".pi/agent/auth.json")) {
return JSON.stringify({
zai: { type: "api_key", key: "test-api-key" },
});
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify(mockResponse)));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const zai = providers.find((p) => p.name === "Zai")!;
expect(zai.status).toBe("ok");
expect(zai.plan).toBe("Pro");
expect(zai.windows).toHaveLength(1);
expect(zai.windows[0].label).toBe("Session (5h)");
});
it("handles API error response (success=false)", async () => {
const mockResponse = {
code: 500,
msg: "Internal error",
success: false,
};
mockReadFile.mockImplementation((filePath: string) => {
if (filePath.includes(".pi/agent/auth.json")) {
return JSON.stringify({
zai: { type: "api_key", key: "test-api-key" },
});
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify(mockResponse)));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const zai = providers.find((p) => p.name === "Zai")!;
expect(zai.status).toBe("error");
expect(zai.error).toContain("Internal error");
});
it("handles 401 auth error", async () => {
mockReadFile.mockImplementation((filePath: string) => {
if (filePath.includes(".pi/agent/auth.json")) {
return JSON.stringify({
zai: { type: "api_key", key: "expired-key" },
});
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 401,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from('{"error": "unauthorized"}'));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const zai = providers.find((p) => p.name === "Zai")!;
expect(zai.status).toBe("error");
expect(zai.error).toContain("Auth expired");
});
it("handles 403 auth error", async () => {
mockReadFile.mockImplementation((filePath: string) => {
if (filePath.includes(".pi/agent/auth.json")) {
return JSON.stringify({
zai: { type: "api_key", key: "forbidden-key" },
});
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 403,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from('{"error": "forbidden"}'));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const zai = providers.find((p) => p.name === "Zai")!;
expect(zai.status).toBe("error");
expect(zai.error).toContain("Auth expired");
});
it("sets resetAt from nextResetTime for TOKENS_LIMIT", async () => {
const nextReset = Date.now() + 3 * 60 * 60 * 1000;
const mockResponse = {
code: 200,
msg: "Operation successful",
data: {
limits: [
{
type: "TOKENS_LIMIT",
percentage: 25,
nextResetTime: nextReset,
},
],
level: "max",
},
success: true,
};
mockReadFile.mockImplementation((filePath: string) => {
if (filePath.includes(".pi/agent/auth.json")) {
return JSON.stringify({ zai: { type: "api_key", key: "test-api-key" } });
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify(mockResponse)));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const zai = providers.find((p) => p.name === "Zai")!;
expect(zai.status).toBe("ok");
const sessionWindow = zai.windows.find((w) => w.label === "Session (5h)")!;
expect(sessionWindow.resetAt).toBe(new Date(nextReset).toISOString());
});
it("sets resetAt from nextResetTime for TIME_LIMIT", async () => {
const nextReset = Date.now() + 25 * 24 * 60 * 60 * 1000;
const mockResponse = {
code: 200,
msg: "Operation successful",
data: {
limits: [
{
type: "TOKENS_LIMIT",
percentage: 10,
nextResetTime: Date.now() + 3 * 60 * 60 * 1000,
},
{
type: "TIME_LIMIT",
usage: 4000,
currentValue: 100,
remaining: 3900,
percentage: 2.5,
nextResetTime: nextReset,
},
],
level: "pro",
},
success: true,
};
mockReadFile.mockImplementation((filePath: string) => {
if (filePath.includes(".pi/agent/auth.json")) {
return JSON.stringify({ zai: { type: "api_key", key: "test-api-key" } });
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify(mockResponse)));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const zai = providers.find((p) => p.name === "Zai")!;
expect(zai.status).toBe("ok");
const mcpWindow = zai.windows.find((w) => w.label === "MCP Monthly")!;
expect(mcpWindow.resetAt).toBe(new Date(nextReset).toISOString());
});
});
describe("calculatePace helper", () => {
it("returns ahead status when usage exceeds elapsed time by >5%", () => {
// 70% used, 50% elapsed = 20% ahead (3 days remaining out of 7 = 57% elapsed, 70 - 57 = 13 > 5)
// Actually: 100 - (3/7 * 100) = 57.14% elapsed
// 70 - 57.14 = 12.86% > 5% → ahead
const pace = calculatePace(70, 3 * 24 * 60 * 60 * 1000, 7 * 24 * 60 * 60 * 1000);
expect(pace).toBeDefined();
expect(pace!.status).toBe("ahead");
expect(pace!.percentElapsed).toBe(57);
expect(pace!.message).toContain("over pace");
});
it("returns behind status when usage is under elapsed time by >5%", () => {
// 20% used, 57% elapsed = 37% behind
const pace = calculatePace(20, 3 * 24 * 60 * 60 * 1000, 7 * 24 * 60 * 60 * 1000);
expect(pace).toBeDefined();
expect(pace!.status).toBe("behind");
expect(pace!.percentElapsed).toBe(57);
expect(pace!.message).toContain("under pace");
});
it("returns on-track status when within 5% of elapsed time", () => {
// 52% used, 57% elapsed = 5% difference (within threshold)
const pace = calculatePace(52, 3.5 * 24 * 60 * 60 * 1000, 7 * 24 * 60 * 60 * 1000);
expect(pace).toBeDefined();
expect(pace!.status).toBe("on-track");
expect(pace!.message).toBe("On pace with time elapsed");
});
it("returns undefined when resetMs is undefined", () => {
const pace = calculatePace(50, undefined, 7 * 24 * 60 * 60 * 1000);
expect(pace).toBeUndefined();
});
it("returns undefined when windowDurationMs is undefined", () => {
const pace = calculatePace(50, 3 * 24 * 60 * 60 * 1000, undefined);
expect(pace).toBeUndefined();
});
it("returns undefined when resetMs is 0 or negative", () => {
expect(calculatePace(50, 0, 7 * 24 * 60 * 60 * 1000)).toBeUndefined();
expect(calculatePace(50, -1000, 7 * 24 * 60 * 60 * 1000)).toBeUndefined();
});
it("returns undefined when windowDurationMs is 0 or negative", () => {
expect(calculatePace(50, 3 * 24 * 60 * 60 * 1000, 0)).toBeUndefined();
expect(calculatePace(50, 3 * 24 * 60 * 60 * 1000, -1000)).toBeUndefined();
});
it("clamps percentUsed to 0-100 range", () => {
// Test with negative percentUsed
let pace = calculatePace(-10, 3 * 24 * 60 * 60 * 1000, 7 * 24 * 60 * 60 * 1000);
expect(pace).toBeDefined();
expect(pace!.status).toBe("behind");
// Test with percentUsed > 100
pace = calculatePace(150, 3 * 24 * 60 * 60 * 1000, 7 * 24 * 60 * 60 * 1000);
expect(pace).toBeDefined();
expect(pace!.status).toBe("ahead");
});
});
describe("error handling", () => {
it("handles Claude API errors gracefully", async () => {
mockReadFile.mockImplementation((filePath: string) => {
if (filePath.includes("claude")) {
return JSON.stringify({
accessToken: "test-token",
scopes: ["user:profile"],
});
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 500,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from('{"error": "internal server error"}'));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("error");
expect(claude.error).toContain("HTTP 500");
});
it("handles Claude network error", async () => {
mockReadFile.mockImplementation((filePath: string) => {
if (filePath.includes("claude")) {
return JSON.stringify({
accessToken: "test-token",
scopes: ["user:profile"],
});
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
mockRequest.mockImplementation((_options: any, _callback: any) => {
const mockReq = {
on: vi.fn((event: string, handler: any) => {
if (event === "error") {
// Simulate network error
setTimeout(() => handler(new Error("network error")), 0);
}
}),
write: vi.fn(),
end: vi.fn(),
};
return mockReq;
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("error");
expect(claude.error).toContain("network error");
});
});
describe("formatDuration helper", () => {
it("formats duration correctly via resetText", async () => {
mockReadFile.mockImplementation((path: string) => {
if (path.includes("codex")) {
return JSON.stringify({
tokens: {
access_token: "test-token",
},
});
}
return Promise.reject(new Error("File not found"));
});
const mockResponse = {
rate_limit: {
primary_window: {
used_percent: 50,
reset_after_seconds: 3661, // 1h 1m 1s
},
},
};
const mockReq = {
on: vi.fn(),
write: vi.fn(),
end: vi.fn(),
};
mockRequest.mockImplementation((options: any, callback: any) => {
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") {
handler(Buffer.from(JSON.stringify(mockResponse)));
}
if (event === "end") {
handler();
}
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const codex = providers.find((p) => p.name === "Codex")!;
expect(codex.windows[0].resetText).toContain("1h 1m");
});
});
describe("pace integration with provider windows", () => {
it("attaches pace to Minimax model window with valid timing data", async () => {
const now = Date.now();
const fiveHours = 5 * 60 * 60 * 1000;
const twoHoursFromNow = 2 * 60 * 60 * 1000;
const mockResponse = {
model_remains: [
{
model_name: "MiniMax-M*",
current_interval_total_count: 4500,
current_interval_usage_count: 4000,
remains_time: twoHoursFromNow,
start_time: now - 3 * 60 * 60 * 1000,
end_time: now + twoHoursFromNow,
},
],
};
mockReadFile.mockImplementation((filePath: string) => {
if (filePath.includes(".pi/agent/auth.json")) {
return JSON.stringify({ minimax: { type: "api_key", key: "test-key" } });
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify(mockResponse)));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const minimax = providers.find((p) => p.name === "Minimax")!;
expect(minimax.status).toBe("ok");
expect(minimax.windows).toHaveLength(1);
const modelWindow = minimax.windows[0];
expect(modelWindow.label).toBe("MiniMax-M*");
expect(modelWindow.pace).toBeDefined();
expect(modelWindow.pace!.percentElapsed).toBeGreaterThan(0);
});
it("does not attach pace when resetMs is 0 (window already reset)", async () => {
const now = Date.now();
const mockResponse = {
model_remains: [
{
model_name: "MiniMax-M*",
current_interval_total_count: 4500,
current_interval_usage_count: 4500,
remains_time: 0,
start_time: now - 5 * 60 * 60 * 1000,
end_time: now,
},
],
};
mockReadFile.mockImplementation((filePath: string) => {
if (filePath.includes(".pi/agent/auth.json")) {
return JSON.stringify({ minimax: { type: "api_key", key: "test-key" } });
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 200,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from(JSON.stringify(mockResponse)));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const minimax = providers.find((p) => p.name === "Minimax")!;
expect(minimax.status).toBe("ok");
// remains_time is 0, so resetMs is undefined (no active reset timer)
expect(minimax.windows[0].resetMs).toBeUndefined();
expect(minimax.windows[0].pace).toBeUndefined();
});
});
describe("Claude CLI fallback parsing", () => {
describe("_stripClaudeAnsi", () => {
it("strips basic ANSI color codes", () => {
const input = "\x1B[32m████████\x1B[0m 27% used";
expect(_stripClaudeAnsi(input)).toBe("████████ 27% used");
});
it("converts cursor forward to spaces", () => {
const input = "Current\x1B[1Csession";
expect(_stripClaudeAnsi(input)).toBe("Current session");
});
it("handles multi-character cursor forward", () => {
const input = "Hello\x1B[3Cworld";
expect(_stripClaudeAnsi(input)).toBe("Hello world");
});
it("strips DEC private mode sequences", () => {
const input = "\x1B[?2026lClaude Code\x1B[?2026h more text";
expect(_stripClaudeAnsi(input)).toBe("Claude Code more text");
});
it("strips OSC title sequences", () => {
const input = "\x1B]0;Claude Code\x07Usage data";
expect(_stripClaudeAnsi(input)).toBe("Usage data");
});
it("handles real Claude TUI output with cursor movement", () => {
const input =
"Current\x1B[1Cweek\x1B[1C(all\x1B[1Cmodels)\n" +
"\x1B[32m█████████████████████████▌\x1B[0m\x1B[1C51%\x1B[1Cused\n" +
"Resets\x1B[1CFeb\x1B[1C19\x1B[1Cat\x1B[1C3pm\x1B[1C(America/Los_Angeles)";
const result = _stripClaudeAnsi(input);
expect(result).toContain("Current week (all models)");
expect(result).toContain("51% used");
expect(result).toContain("Resets Feb 19 at 3pm (America/Los_Angeles)");
});
it("handles backspace characters", () => {
const input = "abc\x08d";
expect(_stripClaudeAnsi(input)).toBe("abd");
});
it("preserves newlines and tabs", () => {
const input = "Line 1\nLine 2\tTabbed";
expect(_stripClaudeAnsi(input)).toBe("Line 1\nLine 2\tTabbed");
});
});
describe("_parseClaudePercentLine", () => {
it("parses 'X% used'", () => {
expect(_parseClaudePercentLine("████████ 27% used")).toBe(27);
});
it("parses 'X% left' and converts to used", () => {
expect(_parseClaudePercentLine("████████ 65% left")).toBe(35);
});
it("parses 'X% remaining' and converts to used", () => {
expect(_parseClaudePercentLine("████ 80% remaining")).toBe(20);
});
it("parses 100% used", () => {
expect(_parseClaudePercentLine("████████████████████ 100% used")).toBe(100);
});
it("parses 0% left as 100% used", () => {
expect(_parseClaudePercentLine("0% left")).toBe(100);
});
it("returns null for non-matching lines", () => {
expect(_parseClaudePercentLine("Current session")).toBeNull();
expect(_parseClaudePercentLine("Resets in 2h")).toBeNull();
});
});
describe("_parseClaudeResetLine", () => {
it("extracts 'Resets in 2h 15m'", () => {
expect(_parseClaudeResetLine("Resets in 2h 15m")).toBe("Resets in 2h 15m");
});
it("extracts 'Resets 11am'", () => {
expect(_parseClaudeResetLine("Resets 11am")).toBe("Resets 11am");
});
it("extracts from line with prefix garbage", () => {
expect(_parseClaudeResetLine("some garbage Resets 3pm")).toBe("Resets 3pm");
});
it("strips timezone suffix", () => {
expect(_parseClaudeResetLine("Resets Feb 19 at 3pm (America/Los_Angeles)")).toBe(
"Resets Feb 19 at 3pm"
);
});
it("strips percentage info if on same line", () => {
expect(_parseClaudeResetLine("46%used Resets 5:59pm")).toBe("Resets 5:59pm");
});
it("returns null for non-reset lines", () => {
expect(_parseClaudeResetLine("Current session")).toBeNull();
expect(_parseClaudeResetLine("27% used")).toBeNull();
});
});
describe("_parseClaudeResetText", () => {
beforeEach(() => {
vi.useFakeTimers();
vi.setSystemTime(new Date("2025-01-15T10:00:00Z"));
});
afterEach(() => {
vi.useRealTimers();
});
it("parses duration format with hours and minutes", () => {
const result = _parseClaudeResetText("Resets in 2h 15m");
expect(result).toBe(new Date("2025-01-15T12:15:00Z").toISOString());
});
it("parses duration format with only minutes", () => {
const result = _parseClaudeResetText("Resets in 30m");
expect(result).toBe(new Date("2025-01-15T10:30:00Z").toISOString());
});
it("parses simple AM time", () => {
const result = _parseClaudeResetText("Resets 11am");
expect(result).toBeTruthy();
expect(new Date(result!).getHours()).toBe(11);
});
it("parses simple PM time", () => {
const result = _parseClaudeResetText("Resets 3pm");
expect(result).toBeTruthy();
expect(new Date(result!).getHours()).toBe(15);
});
it("parses date format with month day at time", () => {
const result = _parseClaudeResetText("Resets Feb 19 at 3pm");
expect(result).toBeTruthy();
const d = new Date(result!);
expect(d.getMonth()).toBe(1); // Feb
expect(d.getDate()).toBe(19);
expect(d.getHours()).toBe(15);
});
it("parses date format with 'at' immediately before time (no space after 'at')", () => {
// CLI output with cursor-forward sequences can produce "at3pm" instead of "at 3pm"
const result = _parseClaudeResetText("Resets Feb 19 at3pm");
expect(result).toBeTruthy();
const d = new Date(result!);
expect(d.getMonth()).toBe(1); // Feb
expect(d.getDate()).toBe(19);
expect(d.getHours()).toBe(15);
});
it("parses date format with 'at' and minutes (no space after 'at')", () => {
const result = _parseClaudeResetText("Resets Feb 19 at3:30pm");
expect(result).toBeTruthy();
const d = new Date(result!);
expect(d.getMonth()).toBe(1); // Feb
expect(d.getDate()).toBe(19);
expect(d.getHours()).toBe(15);
expect(d.getMinutes()).toBe(30);
});
it("parses date format with comma", () => {
const result = _parseClaudeResetText("Resets Jan 15, 3:30pm");
expect(result).toBeTruthy();
const d = new Date(result!);
expect(d.getMonth()).toBe(0);
expect(d.getDate()).toBe(15);
expect(d.getHours()).toBe(15);
expect(d.getMinutes()).toBe(30);
});
it("handles 12am correctly", () => {
const result = _parseClaudeResetText("Resets 12am");
expect(result).toBeTruthy();
expect(new Date(result!).getHours()).toBe(0);
});
it("handles 12pm correctly", () => {
const result = _parseClaudeResetText("Resets 12pm");
expect(result).toBeTruthy();
expect(new Date(result!).getHours()).toBe(12);
});
it("returns null for unparseable text", () => {
expect(_parseClaudeResetText("unknown format")).toBeNull();
});
});
});
describe("withTimeout", () => {
afterEach(() => {
vi.useRealTimers();
});
it("resolves with provider result when fetch completes within timeout", async () => {
const provider: ProviderUsage = {
name: "TestProvider",
icon: "🧪",
status: "ok",
windows: [],
};
const result = await withTimeout(Promise.resolve(provider), "TestProvider", 5000);
expect(result).toEqual(provider);
expect(result.status).toBe("ok");
});
it("returns error provider when fetch exceeds timeout", async () => {
vi.useFakeTimers();
const slowPromise = new Promise<ProviderUsage>((resolve) => {
setTimeout(() => resolve({ name: "Slow", icon: "🐌", status: "ok", windows: [] }), 10000);
});
const resultPromise = withTimeout(slowPromise, "Slow", 50);
await vi.advanceTimersByTimeAsync(50);
const result = await resultPromise;
expect(result.status).toBe("error");
expect(result.error).toBe("Timed out after 0s");
expect(result.name).toBe("Slow");
});
it("includes timeout duration in error message for different durations", async () => {
vi.useFakeTimers();
// 100ms => "0s"
const result100Promise = withTimeout(
new Promise<ProviderUsage>(() => {}),
"Test",
100,
);
await vi.advanceTimersByTimeAsync(100);
const result100 = await result100Promise;
expect(result100.error).toBe("Timed out after 0s");
// 10_000ms is too long to actually wait, but we can verify the format
// by using a 1050ms timeout (rounds to 1s)
const result1sPromise = withTimeout(
new Promise<ProviderUsage>(() => {}),
"Test",
1050,
);
await vi.advanceTimersByTimeAsync(1050);
const result1s = await result1sPromise;
expect(result1s.error).toBe("Timed out after 1s");
});
it("catches rejected promises and returns error provider", async () => {
const failingPromise = Promise.reject(new Error("Network failure"));
const result = await withTimeout(failingPromise, "Failing", 5000);
expect(result.status).toBe("error");
expect(result.error).toBe("Network failure");
});
});
describe("Claude timeout constant", () => {
it("CLAUDE_FETCH_TIMEOUT_MS is 75 seconds", () => {
expect(CLAUDE_FETCH_TIMEOUT_MS).toBe(75_000);
});
it("CLAUDE_FETCH_TIMEOUT_MS is larger than default provider timeout", () => {
// The default PROVIDER_FETCH_TIMEOUT_MS is 10_000. CLAUDE_FETCH_TIMEOUT_MS should be much larger.
expect(CLAUDE_FETCH_TIMEOUT_MS).toBeGreaterThan(10_000);
});
});
describe("Claude API error diagnostics", () => {
it("includes response body snippet in HTTP 500 error", async () => {
mockReadFile.mockImplementation((filePath: string) => {
if (filePath.includes("claude")) {
return JSON.stringify({
accessToken: "test-token",
scopes: ["user:profile"],
});
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation(() => {
throw new Error("Keychain item not found");
});
const mockReq = { on: vi.fn(), write: vi.fn(), end: vi.fn() };
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode: 500,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") handler(Buffer.from('{"error": "internal server error", "details": "something went wrong"}'));
if (event === "end") handler();
}),
};
callback(mockRes);
return mockReq;
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("error");
expect(claude.error).toContain("HTTP 500");
expect(claude.error).toContain("internal server error");
});
});
describe("_parseResetTimestamp helper", () => {
it("parses ISO string format", () => {
const futureDate = new Date(Date.now() + 2 * 60 * 60 * 1000); // 2 hours from now
const result = _parseResetTimestamp(futureDate.toISOString());
expect(result).not.toBeNull();
expect(result!.msLeft).toBeGreaterThan(0);
expect(result!.msLeft).toBeLessThanOrEqual(2 * 60 * 60 * 1000);
});
it("parses Unix timestamp in seconds (10 digits)", () => {
const futureSeconds = Math.floor((Date.now() + 3 * 60 * 60 * 1000) / 1000); // 3 hours from now
const result = _parseResetTimestamp(futureSeconds);
expect(result).not.toBeNull();
expect(result!.msLeft).toBeGreaterThan(0);
expect(result!.msLeft).toBeLessThanOrEqual(3 * 60 * 60 * 1000);
});
it("parses Unix timestamp in milliseconds (13 digits)", () => {
const futureMs = Date.now() + 4 * 60 * 60 * 1000; // 4 hours from now
const result = _parseResetTimestamp(futureMs);
expect(result).not.toBeNull();
expect(result!.msLeft).toBeGreaterThan(0);
expect(result!.msLeft).toBeLessThanOrEqual(4 * 60 * 60 * 1000);
});
it("parses numeric string in seconds", () => {
const futureSeconds = Math.floor((Date.now() + 5 * 60 * 60 * 1000) / 1000); // 5 hours from now
const result = _parseResetTimestamp(String(futureSeconds));
expect(result).not.toBeNull();
expect(result!.msLeft).toBeGreaterThan(0);
expect(result!.msLeft).toBeLessThanOrEqual(5 * 60 * 60 * 1000);
});
it("parses numeric string in milliseconds", () => {
const futureMs = Date.now() + 6 * 60 * 60 * 1000; // 6 hours from now
const result = _parseResetTimestamp(String(futureMs));
expect(result).not.toBeNull();
expect(result!.msLeft).toBeGreaterThan(0);
expect(result!.msLeft).toBeLessThanOrEqual(6 * 60 * 60 * 1000);
});
it("returns null for null value", () => {
expect(_parseResetTimestamp(null)).toBeNull();
});
it("returns null for undefined value", () => {
expect(_parseResetTimestamp(undefined)).toBeNull();
});
it("returns null for empty string", () => {
expect(_parseResetTimestamp("")).toBeNull();
});
it("returns null for invalid date string", () => {
expect(_parseResetTimestamp("invalid-date")).toBeNull();
expect(_parseResetTimestamp("not a timestamp")).toBeNull();
});
it("returns null for past timestamp", () => {
const pastMs = Date.now() - 60 * 60 * 1000; // 1 hour ago
expect(_parseResetTimestamp(pastMs)).toBeNull();
expect(_parseResetTimestamp(pastMs / 1000)).toBeNull(); // seconds format
});
it("returns null for zero timestamp", () => {
expect(_parseResetTimestamp(0)).toBeNull();
});
});
describe("Claude session reset fallback scenarios", () => {
function setupClaudeMocks(options: {
credFileContent?: any;
keychainContent?: any;
}) {
const { credFileContent = null, keychainContent = null } = options;
mockReadFile.mockImplementation((filePath: string) => {
if (filePath.includes("claude") && credFileContent !== null) {
return JSON.stringify(credFileContent);
}
return Promise.reject(new Error("File not found"));
});
mockExecFileSync.mockImplementation((cmd: string, _args: string[]) => {
if (cmd === "security") {
if (keychainContent !== null) {
return JSON.stringify(keychainContent);
}
throw new Error("Keychain item not found");
}
throw new Error(`Unexpected command: ${cmd}`);
});
}
function setupClaudeApiResponse(mockResponse: any, statusCode = 200) {
const mockReq = {
on: vi.fn(),
write: vi.fn(),
end: vi.fn(),
};
mockRequest.mockImplementation((_options: any, callback: any) => {
const mockRes = {
statusCode,
headers: {},
on: vi.fn((event: string, handler: any) => {
if (event === "data") {
handler(Buffer.from(JSON.stringify(mockResponse)));
}
if (event === "end") {
handler();
}
}),
};
callback(mockRes);
return mockReq;
});
}
it("applies 5h fallback when resets_at is an invalid string", async () => {
setupClaudeMocks({
credFileContent: {
accessToken: "test-token",
scopes: ["user:profile"],
subscriptionType: "pro",
},
});
// API returns invalid reset timestamp
setupClaudeApiResponse({
five_hour: {
utilization: 30.0,
resets_at: "invalid-date-string",
},
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("ok");
const sessionWindow = claude.windows.find((w) => w.label.includes("Session"))!;
expect(sessionWindow).toBeDefined();
// Should use 5h fallback
expect(sessionWindow.resetMs).toBe(5 * 60 * 60 * 1000);
expect(sessionWindow.resetText).toBe("resets in 5h");
expect(sessionWindow.resetAt).toBeDefined();
});
it("applies 5h fallback when resets_at is a past timestamp", async () => {
setupClaudeMocks({
credFileContent: {
accessToken: "test-token",
scopes: ["user:profile"],
subscriptionType: "pro",
},
});
// API returns a past reset timestamp (1 hour ago)
const pastReset = new Date(Date.now() - 60 * 60 * 1000).toISOString();
setupClaudeApiResponse({
five_hour: {
utilization: 30.0,
resets_at: pastReset,
},
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("ok");
const sessionWindow = claude.windows.find((w) => w.label.includes("Session"))!;
expect(sessionWindow).toBeDefined();
// Should use 5h fallback since the reset time is in the past
expect(sessionWindow.resetMs).toBe(5 * 60 * 60 * 1000);
expect(sessionWindow.resetText).toBe("resets in 5h");
expect(sessionWindow.resetAt).toBeDefined();
});
it("applies 5h fallback when resets_at is empty string", async () => {
setupClaudeMocks({
credFileContent: {
accessToken: "test-token",
scopes: ["user:profile"],
subscriptionType: "pro",
},
});
// API returns empty string for reset timestamp
setupClaudeApiResponse({
five_hour: {
utilization: 30.0,
resets_at: "",
},
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("ok");
const sessionWindow = claude.windows.find((w) => w.label.includes("Session"))!;
expect(sessionWindow).toBeDefined();
// Should use 5h fallback
expect(sessionWindow.resetMs).toBe(5 * 60 * 60 * 1000);
expect(sessionWindow.resetText).toBe("resets in 5h");
});
it("applies 5h fallback when resets_at is null", async () => {
setupClaudeMocks({
credFileContent: {
accessToken: "test-token",
scopes: ["user:profile"],
subscriptionType: "pro",
},
});
// API returns null for reset timestamp
setupClaudeApiResponse({
five_hour: {
utilization: 30.0,
resets_at: null,
},
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("ok");
const sessionWindow = claude.windows.find((w) => w.label.includes("Session"))!;
expect(sessionWindow).toBeDefined();
// Should use 5h fallback
expect(sessionWindow.resetMs).toBe(5 * 60 * 60 * 1000);
expect(sessionWindow.resetText).toBe("resets in 5h");
});
it("parses resets_at as Unix seconds correctly", async () => {
setupClaudeMocks({
credFileContent: {
accessToken: "test-token",
scopes: ["user:profile"],
subscriptionType: "pro",
},
});
// API returns Unix timestamp in seconds (10 digits)
const futureSeconds = Math.floor((Date.now() + 2 * 60 * 60 * 1000) / 1000);
setupClaudeApiResponse({
five_hour: {
utilization: 30.0,
resets_at: futureSeconds, // Unix seconds
},
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("ok");
const sessionWindow = claude.windows.find((w) => w.label.includes("Session"))!;
expect(sessionWindow.resetText).toContain("resets in");
expect(sessionWindow.resetMs).toBeGreaterThan(0);
expect(sessionWindow.resetAt).toBeDefined();
});
it("parses resets_at as Unix milliseconds correctly", async () => {
setupClaudeMocks({
credFileContent: {
accessToken: "test-token",
scopes: ["user:profile"],
subscriptionType: "pro",
},
});
// API returns Unix timestamp in milliseconds (13 digits)
const futureMs = Date.now() + 3 * 60 * 60 * 1000;
setupClaudeApiResponse({
five_hour: {
utilization: 30.0,
resets_at: futureMs, // Unix milliseconds
},
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("ok");
const sessionWindow = claude.windows.find((w) => w.label.includes("Session"))!;
expect(sessionWindow.resetText).toContain("resets in");
expect(sessionWindow.resetMs).toBeGreaterThan(0);
expect(sessionWindow.resetAt).toBeDefined();
});
it("does NOT apply 5h fallback to weekly windows with missing reset", async () => {
setupClaudeMocks({
credFileContent: {
accessToken: "test-token",
scopes: ["user:profile"],
subscriptionType: "pro",
},
});
// Weekly window with missing reset - should NOT apply 5h fallback
setupClaudeApiResponse({
five_hour: {
utilization: 30.0,
},
seven_day: {
utilization: 15.0,
// No resets_at for weekly
},
});
const providers = await fetchAllProviderUsage();
const claude = providers.find((p) => p.name === "Claude")!;
expect(claude.status).toBe("ok");
// Session should have fallback
const sessionWindow = claude.windows.find((w) => w.label.includes("Session"))!;
expect(sessionWindow.resetText).toBe("resets in 5h");
expect(sessionWindow.resetMs).toBe(5 * 60 * 60 * 1000);
// Weekly should NOT have fallback (only session window gets fallback)
const weeklyWindow = claude.windows.find((w) => w.label === "Weekly")!;
expect(weeklyWindow.resetText).toBeNull();
expect(weeklyWindow.resetMs).toBeUndefined();
expect(weeklyWindow.resetAt).toBeUndefined();
});
});
});