import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import os from "node:os"; import path from "node:path"; import type { QuotaWindow } from "@paperclipai/adapter-utils"; // Pure utility functions — import directly from adapter source import { toPercent, fetchWithTimeout, fetchClaudeQuota, parseClaudeCliUsageText, readClaudeToken, claudeConfigDir, } from "@paperclipai/adapter-claude-local/server"; import { secondsToWindowLabel, readCodexAuthInfo, readCodexToken, fetchCodexQuota, mapCodexRpcQuota, codexHomeDir, } from "@paperclipai/adapter-codex-local/server"; // --------------------------------------------------------------------------- // toPercent // --------------------------------------------------------------------------- describe("toPercent", () => { it("returns null for null input", () => { expect(toPercent(null)).toBe(null); }); it("returns null for undefined input", () => { expect(toPercent(undefined)).toBe(null); }); it("converts 0 to 0", () => { expect(toPercent(0)).toBe(0); }); it("converts 0.5 to 50", () => { expect(toPercent(0.5)).toBe(50); }); it("converts 1.0 to 100", () => { expect(toPercent(1.0)).toBe(100); }); it("clamps overshoot to 100", () => { // floating-point utilization can slightly exceed 1.0 expect(toPercent(1.001)).toBe(100); expect(toPercent(1.01)).toBe(100); }); it("rounds to nearest integer", () => { expect(toPercent(0.333)).toBe(33); expect(toPercent(0.666)).toBe(67); }); }); // --------------------------------------------------------------------------- // secondsToWindowLabel // --------------------------------------------------------------------------- describe("secondsToWindowLabel", () => { it("returns fallback for null seconds", () => { expect(secondsToWindowLabel(null, "Primary")).toBe("Primary"); }); it("returns fallback for undefined seconds", () => { expect(secondsToWindowLabel(undefined, "Secondary")).toBe("Secondary"); }); it("labels windows under 6 hours as '5h'", () => { expect(secondsToWindowLabel(3600, "fallback")).toBe("5h"); // 1h expect(secondsToWindowLabel(18000, "fallback")).toBe("5h"); // 5h exactly }); it("labels windows up to 24 hours as '24h'", () => { expect(secondsToWindowLabel(21600, "fallback")).toBe("24h"); // 6h (≥6h boundary) expect(secondsToWindowLabel(86400, "fallback")).toBe("24h"); // 24h exactly }); it("labels windows up to 7 days as '7d'", () => { expect(secondsToWindowLabel(86401, "fallback")).toBe("7d"); // just over 24h expect(secondsToWindowLabel(604800, "fallback")).toBe("7d"); // 7d exactly }); it("labels windows beyond 7 days with actual day count", () => { expect(secondsToWindowLabel(1209600, "fallback")).toBe("14d"); // 14d expect(secondsToWindowLabel(2592000, "fallback")).toBe("30d"); // 30d }); }); // --------------------------------------------------------------------------- // WHAM used_percent normalization (codex / openai) // --------------------------------------------------------------------------- describe("WHAM used_percent normalization via fetchCodexQuota", () => { beforeEach(() => { vi.stubGlobal("fetch", vi.fn()); }); afterEach(() => { vi.unstubAllGlobals(); }); function mockFetch(body: unknown) { (fetch as ReturnType).mockResolvedValue({ ok: true, json: async () => body, } as Response); } it("treats values >= 1 as already-percentage (50 → 50%)", async () => { mockFetch({ rate_limit: { primary_window: { used_percent: 50, limit_window_seconds: 18000, reset_at: null, }, }, }); const windows = await fetchCodexQuota("token", null); expect(windows[0]!.usedPercent).toBe(50); }); it("treats values < 1 as fraction and multiplies by 100 (0.5 → 50%)", async () => { mockFetch({ rate_limit: { primary_window: { used_percent: 0.5, limit_window_seconds: 18000, reset_at: null, }, }, }); const windows = await fetchCodexQuota("token", null); expect(windows[0]!.usedPercent).toBe(50); }); it("treats value exactly 1.0 as 1% (not 100%) — the < 1 heuristic boundary", async () => { // 1.0 is NOT < 1, so it is treated as already-percentage → 1% mockFetch({ rate_limit: { primary_window: { used_percent: 1.0, limit_window_seconds: 18000, reset_at: null, }, }, }); const windows = await fetchCodexQuota("token", null); expect(windows[0]!.usedPercent).toBe(1); }); it("treats value 0 as 0%", async () => { mockFetch({ rate_limit: { primary_window: { used_percent: 0, limit_window_seconds: 18000, reset_at: null, }, }, }); const windows = await fetchCodexQuota("token", null); expect(windows[0]!.usedPercent).toBe(0); }); it("clamps 100% to 100 (no overshoot)", async () => { mockFetch({ rate_limit: { primary_window: { used_percent: 105, limit_window_seconds: 18000, reset_at: null, }, }, }); const windows = await fetchCodexQuota("token", null); expect(windows[0]!.usedPercent).toBe(100); }); it("sets usedPercent to null when used_percent is absent", async () => { mockFetch({ rate_limit: { primary_window: { limit_window_seconds: 18000, reset_at: null, }, }, }); const windows = await fetchCodexQuota("token", null); expect(windows[0]!.usedPercent).toBe(null); }); }); // --------------------------------------------------------------------------- // readClaudeToken — filesystem paths // --------------------------------------------------------------------------- describe("readClaudeToken", () => { const savedEnv = process.env.CLAUDE_CONFIG_DIR; afterEach(() => { if (savedEnv === undefined) { delete process.env.CLAUDE_CONFIG_DIR; } else { process.env.CLAUDE_CONFIG_DIR = savedEnv; } vi.restoreAllMocks(); }); it("returns null when credentials.json does not exist", async () => { // Point to a directory that does not have credentials.json process.env.CLAUDE_CONFIG_DIR = "/tmp/__no_such_paperclip_dir__"; const token = await readClaudeToken(); expect(token).toBe(null); }); it("returns null for malformed JSON", async () => { const tmpDir = path.join(os.tmpdir(), `paperclip-test-claude-${Date.now()}`); await import("node:fs/promises").then((fs) => fs.mkdir(tmpDir, { recursive: true }).then(() => fs.writeFile(path.join(tmpDir, "credentials.json"), "not-json"), ), ); process.env.CLAUDE_CONFIG_DIR = tmpDir; const token = await readClaudeToken(); expect(token).toBe(null); await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true })); }); it("returns null when claudeAiOauth key is missing", async () => { const tmpDir = path.join(os.tmpdir(), `paperclip-test-claude-${Date.now()}`); await import("node:fs/promises").then((fs) => fs.mkdir(tmpDir, { recursive: true }).then(() => fs.writeFile(path.join(tmpDir, "credentials.json"), JSON.stringify({ other: "data" })), ), ); process.env.CLAUDE_CONFIG_DIR = tmpDir; const token = await readClaudeToken(); expect(token).toBe(null); await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true })); }); it("returns null when accessToken is an empty string", async () => { const tmpDir = path.join(os.tmpdir(), `paperclip-test-claude-${Date.now()}`); const creds = { claudeAiOauth: { accessToken: "" } }; await import("node:fs/promises").then((fs) => fs.mkdir(tmpDir, { recursive: true }).then(() => fs.writeFile(path.join(tmpDir, "credentials.json"), JSON.stringify(creds)), ), ); process.env.CLAUDE_CONFIG_DIR = tmpDir; const token = await readClaudeToken(); expect(token).toBe(null); await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true })); }); it("returns the token when credentials file is well-formed", async () => { const tmpDir = path.join(os.tmpdir(), `paperclip-test-claude-${Date.now()}`); const creds = { claudeAiOauth: { accessToken: "my-test-token" } }; await import("node:fs/promises").then((fs) => fs.mkdir(tmpDir, { recursive: true }).then(() => fs.writeFile(path.join(tmpDir, "credentials.json"), JSON.stringify(creds)), ), ); process.env.CLAUDE_CONFIG_DIR = tmpDir; const token = await readClaudeToken(); expect(token).toBe("my-test-token"); await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true })); }); it("reads the token from .credentials.json when that is the available Claude auth file", async () => { const tmpDir = path.join(os.tmpdir(), `paperclip-test-claude-${Date.now()}`); const creds = { claudeAiOauth: { accessToken: "dotfile-token" } }; await import("node:fs/promises").then((fs) => fs.mkdir(tmpDir, { recursive: true }).then(() => fs.writeFile(path.join(tmpDir, ".credentials.json"), JSON.stringify(creds)), ), ); process.env.CLAUDE_CONFIG_DIR = tmpDir; const token = await readClaudeToken(); expect(token).toBe("dotfile-token"); await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true })); }); }); describe("parseClaudeCliUsageText", () => { it("parses the Claude usage panel layout into quota windows", () => { const raw = ` Settings: Status Config Usage Current session 2% used Resets 5pm (America/Chicago) Current week (all models) 47% used Resets Mar 18 at 7:59am (America/Chicago) Current week (Sonnet only) 0% used Resets Mar 18 at 8:59am (America/Chicago) Extra usage Extra usage not enabled • /extra-usage to enable `; expect(parseClaudeCliUsageText(raw)).toEqual([ { label: "Current session", usedPercent: 2, resetsAt: null, valueLabel: null, detail: "Resets 5pm (America/Chicago)", }, { label: "Current week (all models)", usedPercent: 47, resetsAt: null, valueLabel: null, detail: "Resets Mar 18 at 7:59am (America/Chicago)", }, { label: "Current week (Sonnet only)", usedPercent: 0, resetsAt: null, valueLabel: null, detail: "Resets Mar 18 at 8:59am (America/Chicago)", }, { label: "Extra usage", usedPercent: null, resetsAt: null, valueLabel: null, detail: "Extra usage not enabled • /extra-usage to enable", }, ]); }); it("throws a useful error when the Claude CLI panel reports a usage load failure", () => { expect(() => parseClaudeCliUsageText("Failed to load usage data")).toThrow( "Claude CLI could not load usage data. Open the CLI and retry `/usage`.", ); }); }); // --------------------------------------------------------------------------- // readCodexAuthInfo / readCodexToken — filesystem paths // --------------------------------------------------------------------------- describe("readCodexAuthInfo", () => { const savedEnv = process.env.CODEX_HOME; afterEach(() => { if (savedEnv === undefined) { delete process.env.CODEX_HOME; } else { process.env.CODEX_HOME = savedEnv; } }); it("returns null when auth.json does not exist", async () => { process.env.CODEX_HOME = "/tmp/__no_such_paperclip_codex_dir__"; const result = await readCodexAuthInfo(); expect(result).toBe(null); }); it("returns null for malformed JSON", async () => { const tmpDir = path.join(os.tmpdir(), `paperclip-test-codex-${Date.now()}`); await import("node:fs/promises").then((fs) => fs.mkdir(tmpDir, { recursive: true }).then(() => fs.writeFile(path.join(tmpDir, "auth.json"), "{bad json"), ), ); process.env.CODEX_HOME = tmpDir; const result = await readCodexAuthInfo(); expect(result).toBe(null); await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true })); }); it("returns null when accessToken is absent", async () => { const tmpDir = path.join(os.tmpdir(), `paperclip-test-codex-${Date.now()}`); await import("node:fs/promises").then((fs) => fs.mkdir(tmpDir, { recursive: true }).then(() => fs.writeFile(path.join(tmpDir, "auth.json"), JSON.stringify({ accountId: "acc-1" })), ), ); process.env.CODEX_HOME = tmpDir; const result = await readCodexAuthInfo(); expect(result).toBe(null); await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true })); }); it("reads the legacy flat auth shape", async () => { const tmpDir = path.join(os.tmpdir(), `paperclip-test-codex-${Date.now()}`); const auth = { accessToken: "codex-token", accountId: "acc-123" }; await import("node:fs/promises").then((fs) => fs.mkdir(tmpDir, { recursive: true }).then(() => fs.writeFile(path.join(tmpDir, "auth.json"), JSON.stringify(auth)), ), ); process.env.CODEX_HOME = tmpDir; const result = await readCodexAuthInfo(); expect(result).toMatchObject({ accessToken: "codex-token", accountId: "acc-123", email: null, planType: null, }); await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true })); }); it("reads the modern nested auth shape", async () => { const tmpDir = path.join(os.tmpdir(), `paperclip-test-codex-${Date.now()}`); const jwtPayload = Buffer.from( JSON.stringify({ email: "codex@example.com", "https://api.openai.com/auth": { chatgpt_plan_type: "pro", chatgpt_user_email: "codex@example.com", }, }), ).toString("base64url"); const auth = { tokens: { access_token: `header.${jwtPayload}.sig`, account_id: "acc-modern", refresh_token: "refresh-me", id_token: `header.${jwtPayload}.sig`, }, last_refresh: "2026-03-14T12:00:00Z", }; await import("node:fs/promises").then((fs) => fs.mkdir(tmpDir, { recursive: true }).then(() => fs.writeFile(path.join(tmpDir, "auth.json"), JSON.stringify(auth)), ), ); process.env.CODEX_HOME = tmpDir; const result = await readCodexAuthInfo(); expect(result).toMatchObject({ accessToken: `header.${jwtPayload}.sig`, accountId: "acc-modern", refreshToken: "refresh-me", email: "codex@example.com", planType: "pro", lastRefresh: "2026-03-14T12:00:00Z", }); await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true })); }); }); describe("readCodexToken", () => { const savedEnv = process.env.CODEX_HOME; afterEach(() => { if (savedEnv === undefined) { delete process.env.CODEX_HOME; } else { process.env.CODEX_HOME = savedEnv; } }); it("returns token and accountId from the nested auth shape", async () => { const tmpDir = path.join(os.tmpdir(), `paperclip-test-codex-${Date.now()}`); await import("node:fs/promises").then((fs) => fs.mkdir(tmpDir, { recursive: true }).then(() => fs.writeFile(path.join(tmpDir, "auth.json"), JSON.stringify({ tokens: { access_token: "nested-token", account_id: "acc-nested", }, })), ), ); process.env.CODEX_HOME = tmpDir; const result = await readCodexToken(); expect(result).toEqual({ token: "nested-token", accountId: "acc-nested" }); await import("node:fs/promises").then((fs) => fs.rm(tmpDir, { recursive: true })); }); }); // --------------------------------------------------------------------------- // fetchClaudeQuota — response parsing // --------------------------------------------------------------------------- describe("fetchClaudeQuota", () => { beforeEach(() => { vi.stubGlobal("fetch", vi.fn()); }); afterEach(() => { vi.unstubAllGlobals(); }); function mockFetch(body: unknown, ok = true, status = 200) { (fetch as ReturnType).mockResolvedValue({ ok, status, json: async () => body, } as Response); } it("throws when the API returns a non-200 status", async () => { mockFetch({}, false, 401); await expect(fetchClaudeQuota("token")).rejects.toThrow("anthropic usage api returned 401"); }); it("returns an empty array when all window fields are absent", async () => { mockFetch({}); const windows = await fetchClaudeQuota("token"); expect(windows).toEqual([]); }); it("parses five_hour window", async () => { mockFetch({ five_hour: { utilization: 0.4, resets_at: "2026-01-01T00:00:00Z" } }); const windows = await fetchClaudeQuota("token"); expect(windows).toHaveLength(1); expect(windows[0]).toMatchObject({ label: "Current session", usedPercent: 40, resetsAt: "2026-01-01T00:00:00Z", }); }); it("parses seven_day window", async () => { mockFetch({ seven_day: { utilization: 0.75, resets_at: null } }); const windows = await fetchClaudeQuota("token"); expect(windows).toHaveLength(1); expect(windows[0]).toMatchObject({ label: "Current week (all models)", usedPercent: 75, resetsAt: null, }); }); it("parses seven_day_sonnet and seven_day_opus windows", async () => { mockFetch({ seven_day_sonnet: { utilization: 0.2, resets_at: null }, seven_day_opus: { utilization: 0.9, resets_at: null }, }); const windows = await fetchClaudeQuota("token"); expect(windows).toHaveLength(2); expect(windows[0]!.label).toBe("Current week (Sonnet only)"); expect(windows[1]!.label).toBe("Current week (Opus only)"); }); it("sets usedPercent to null when utilization is absent", async () => { mockFetch({ five_hour: { resets_at: null } }); const windows = await fetchClaudeQuota("token"); expect(windows[0]!.usedPercent).toBe(null); }); it("includes all four windows when all are present", async () => { mockFetch({ five_hour: { utilization: 0.1, resets_at: null }, seven_day: { utilization: 0.2, resets_at: null }, seven_day_sonnet: { utilization: 0.3, resets_at: null }, seven_day_opus: { utilization: 0.4, resets_at: null }, }); const windows = await fetchClaudeQuota("token"); expect(windows).toHaveLength(4); const labels = windows.map((w: QuotaWindow) => w.label); expect(labels).toEqual([ "Current session", "Current week (all models)", "Current week (Sonnet only)", "Current week (Opus only)", ]); }); it("parses extra usage when the OAuth response includes it", async () => { mockFetch({ extra_usage: { is_enabled: false, utilization: null, }, }); const windows = await fetchClaudeQuota("token"); expect(windows).toEqual([ { label: "Extra usage", usedPercent: null, resetsAt: null, valueLabel: "Not enabled", detail: "Extra usage not enabled", }, ]); }); }); // --------------------------------------------------------------------------- // fetchCodexQuota — response parsing (credits, windows) // --------------------------------------------------------------------------- describe("fetchCodexQuota", () => { beforeEach(() => { vi.stubGlobal("fetch", vi.fn()); }); afterEach(() => { vi.unstubAllGlobals(); }); function mockFetch(body: unknown, ok = true, status = 200) { (fetch as ReturnType).mockResolvedValue({ ok, status, json: async () => body, } as Response); } it("throws when the WHAM API returns a non-200 status", async () => { mockFetch({}, false, 403); await expect(fetchCodexQuota("token", null)).rejects.toThrow("chatgpt wham api returned 403"); }); it("passes ChatGPT-Account-Id header when accountId is provided", async () => { mockFetch({}); await fetchCodexQuota("token", "acc-xyz"); const callInit = (fetch as ReturnType).mock.calls[0][1] as RequestInit; expect((callInit.headers as Record)["ChatGPT-Account-Id"]).toBe("acc-xyz"); }); it("omits ChatGPT-Account-Id header when accountId is null", async () => { mockFetch({}); await fetchCodexQuota("token", null); const callInit = (fetch as ReturnType).mock.calls[0][1] as RequestInit; expect((callInit.headers as Record)["ChatGPT-Account-Id"]).toBeUndefined(); }); it("returns empty array when response body is empty", async () => { mockFetch({}); const windows = await fetchCodexQuota("token", null); expect(windows).toEqual([]); }); it("normalizes numeric reset timestamps from WHAM", async () => { mockFetch({ rate_limit: { primary_window: { used_percent: 30, limit_window_seconds: 86400, reset_at: 1_767_312_000 }, }, }); const windows = await fetchCodexQuota("token", null); expect(windows).toHaveLength(1); expect(windows[0]).toMatchObject({ label: "5h limit", usedPercent: 30, resetsAt: "2026-01-02T00:00:00.000Z" }); }); it("parses secondary_window alongside primary_window", async () => { mockFetch({ rate_limit: { primary_window: { used_percent: 10, limit_window_seconds: 18000 }, secondary_window: { used_percent: 60, limit_window_seconds: 604800 }, }, }); const windows = await fetchCodexQuota("token", null); expect(windows).toHaveLength(2); expect(windows[0]!.label).toBe("5h limit"); expect(windows[1]!.label).toBe("Weekly limit"); }); it("includes Credits window when credits present and not unlimited", async () => { mockFetch({ credits: { balance: 420, unlimited: false }, }); const windows = await fetchCodexQuota("token", null); expect(windows).toHaveLength(1); expect(windows[0]).toMatchObject({ label: "Credits", valueLabel: "$4.20 remaining", usedPercent: null }); }); it("omits Credits window when unlimited is true", async () => { mockFetch({ credits: { balance: 9999, unlimited: true }, }); const windows = await fetchCodexQuota("token", null); expect(windows).toEqual([]); }); it("shows 'N/A' valueLabel when credits balance is null", async () => { mockFetch({ credits: { balance: null, unlimited: false }, }); const windows = await fetchCodexQuota("token", null); expect(windows[0]!.valueLabel).toBe("N/A"); }); }); describe("mapCodexRpcQuota", () => { it("maps account and model-specific Codex limits into quota windows", () => { const snapshot = mapCodexRpcQuota( { rateLimits: { limitId: "codex", primary: { usedPercent: 1, windowDurationMins: 300, resetsAt: 1_763_500_000 }, secondary: { usedPercent: 27, windowDurationMins: 10_080 }, planType: "pro", }, rateLimitsByLimitId: { codex_bengalfox: { limitId: "codex_bengalfox", limitName: "GPT-5.3-Codex-Spark", primary: { usedPercent: 8, windowDurationMins: 300 }, secondary: { usedPercent: 20, windowDurationMins: 10_080 }, }, }, }, { account: { email: "codex@example.com", planType: "pro", }, }, ); expect(snapshot.email).toBe("codex@example.com"); expect(snapshot.planType).toBe("pro"); expect(snapshot.windows).toEqual([ { label: "5h limit", usedPercent: 1, resetsAt: "2025-11-18T21:06:40.000Z", valueLabel: null, detail: null, }, { label: "Weekly limit", usedPercent: 27, resetsAt: null, valueLabel: null, detail: null, }, { label: "GPT-5.3-Codex-Spark · 5h limit", usedPercent: 8, resetsAt: null, valueLabel: null, detail: null, }, { label: "GPT-5.3-Codex-Spark · Weekly limit", usedPercent: 20, resetsAt: null, valueLabel: null, detail: null, }, ]); }); it("includes a credits row when the root Codex limit reports finite credits", () => { const snapshot = mapCodexRpcQuota({ rateLimits: { limitId: "codex", credits: { unlimited: false, balance: "12.34", }, }, }); expect(snapshot.windows).toEqual([ { label: "Credits", usedPercent: null, resetsAt: null, valueLabel: "$12.34 remaining", detail: null, }, ]); }); }); // --------------------------------------------------------------------------- // fetchWithTimeout — abort on timeout // --------------------------------------------------------------------------- describe("fetchWithTimeout", () => { afterEach(() => { vi.unstubAllGlobals(); vi.useRealTimers(); }); it("resolves normally when fetch completes before timeout", async () => { const mockResponse = { ok: true, status: 200, json: async () => ({}) } as Response; vi.stubGlobal("fetch", vi.fn().mockResolvedValue(mockResponse)); const result = await fetchWithTimeout("https://example.com", {}, 5000); expect(result.ok).toBe(true); }); it("rejects with abort error when fetch takes too long", async () => { vi.useFakeTimers(); vi.stubGlobal( "fetch", vi.fn().mockImplementation( (_url: string, init: RequestInit) => new Promise((_resolve, reject) => { init.signal?.addEventListener("abort", () => { reject(new DOMException("The operation was aborted.", "AbortError")); }); }), ), ); const promise = fetchWithTimeout("https://example.com", {}, 1000); vi.advanceTimersByTime(1001); await expect(promise).rejects.toThrow("aborted"); }); });