mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-07 00:44:49 +00:00
Derive missing remaining percentages so CLI spending output matches the Usage card. Preserve explicitly reported percentages and cover missing currency and absent rolling windows. Co-authored-by: Matt Cowger <matt@cowger.us> Co-authored-by: JasprW <wang@jaspr.me>
1195 lines
40 KiB
TypeScript
1195 lines
40 KiB
TypeScript
import { isDeepStrictEqual } from "node:util";
|
|
import { readFileSync, writeFileSync, mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { fetchUsage, discover } from "./usage.js";
|
|
import { inputSchema, type UsageInput } from "../shared/input.js";
|
|
import { hashAccountKey, type UsageReport } from "@getpaseo/plugin/server/usage";
|
|
|
|
function credentialInput(directory: string): UsageInput {
|
|
return { route: { store: "claude", path: join(directory, ".credentials.json") } };
|
|
}
|
|
|
|
function writeClaudeCredentials(
|
|
dir: string,
|
|
accessToken: string,
|
|
refreshToken = "rt_test",
|
|
subscriptionType = "pro",
|
|
rateLimitTier = "default_1x",
|
|
): void {
|
|
writeFileSync(
|
|
join(dir, ".credentials.json"),
|
|
JSON.stringify({
|
|
claudeAiOauth: { accessToken, refreshToken, subscriptionType, rateLimitTier },
|
|
}),
|
|
);
|
|
}
|
|
|
|
function makeClaudeResponse(
|
|
overrides: Partial<{
|
|
five_hour: { utilization: number | string; resets_at: string };
|
|
seven_day: { utilization: number | string; resets_at: string };
|
|
seven_day_opus: { utilization: number | string; resets_at: string };
|
|
}> = {},
|
|
) {
|
|
return {
|
|
five_hour: { utilization: 11, resets_at: "2026-06-01T21:00:00Z" },
|
|
seven_day: { utilization: 1, resets_at: "2026-06-04T00:00:00Z" },
|
|
seven_day_opus: { utilization: 0.5, resets_at: "2026-06-04T00:00:00Z" },
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function mockFetch(handlers: Map<string, () => Response>): typeof fetch {
|
|
return vi.fn(async (url: RequestInfo | URL) => {
|
|
const key = url.toString();
|
|
const handler = handlers.get(key);
|
|
if (!handler) throw new Error(`Unmocked fetch: ${key}`);
|
|
return handler();
|
|
}) as unknown as typeof fetch;
|
|
}
|
|
|
|
function jsonResponse(body: unknown, status = 200, headers: Record<string, string> = {}): Response {
|
|
return new Response(JSON.stringify(body), {
|
|
status,
|
|
headers: { "Content-Type": "application/json", ...headers },
|
|
});
|
|
}
|
|
|
|
describe("claude usage source", () => {
|
|
let claudeHome: string;
|
|
let homeDir: string;
|
|
let fetchApi: typeof fetch;
|
|
let originalEnv: Record<string, string | undefined>;
|
|
beforeEach(() => {
|
|
claudeHome = mkdtempSync(join(tmpdir(), "usage-claude-"));
|
|
homeDir = mkdtempSync(join(tmpdir(), "usage-home-"));
|
|
originalEnv = { ...process.env };
|
|
process.env["HOME"] = homeDir;
|
|
process.env["USERPROFILE"] = homeDir;
|
|
process.env["CLAUDE_CONFIG_DIR"] = claudeHome;
|
|
for (const key of [
|
|
"APPDATA",
|
|
"COPILOT_TOKEN",
|
|
"GITHUB_TOKEN",
|
|
"GITHUB_PAT",
|
|
"CURSOR_ACCESS_TOKEN",
|
|
"CURSOR_TOKEN",
|
|
"ZAI_API_KEY",
|
|
"GLM_API_KEY",
|
|
"GROK_API_KEY",
|
|
"GROK_TOKEN",
|
|
"KIMI_TOKEN",
|
|
"KIMI_API_KEY",
|
|
"KIMI_CODE_HOME",
|
|
"MINIMAX_API_KEY",
|
|
"MINIMAX_BASE_URL",
|
|
])
|
|
delete process.env[key];
|
|
fetchApi = mockFetch(new Map());
|
|
});
|
|
afterEach(() => {
|
|
rmSync(claudeHome, { recursive: true, force: true });
|
|
rmSync(homeDir, { recursive: true, force: true });
|
|
for (const key of Object.keys(process.env)) if (!(key in originalEnv)) delete process.env[key];
|
|
for (const key in originalEnv) process.env[key] = originalEnv[key];
|
|
});
|
|
function service(
|
|
_options: {
|
|
platform?: typeof process.platform;
|
|
keychain?: () => Promise<unknown | null>;
|
|
cursorHomeDir?: string;
|
|
kimiHomeDir?: string;
|
|
} = {},
|
|
) {
|
|
return {
|
|
listUsage: async () => {
|
|
const report = await fetchUsage(credentialInput(claudeHome), (url, init) =>
|
|
fetchApi(url, init),
|
|
);
|
|
return {
|
|
providers: [
|
|
{
|
|
providerId: "claude",
|
|
...report,
|
|
error: report.error ?? null,
|
|
planLabel: report.planLabel ?? null,
|
|
},
|
|
],
|
|
};
|
|
},
|
|
};
|
|
}
|
|
function findProvider(
|
|
result: { providers: Array<{ providerId: string } & UsageReport> },
|
|
id: string,
|
|
) {
|
|
const report = result.providers.find((item) => item.providerId === id);
|
|
if (!report) throw new Error(`Missing usage source ${id}`);
|
|
return report;
|
|
}
|
|
it("fetches Claude usage, coerces API numbers, and attaches HTTP timeout signals", async () => {
|
|
writeClaudeCredentials(claudeHome, "at_valid");
|
|
fetchApi = mockFetch(
|
|
new Map([
|
|
[
|
|
"https://api.anthropic.com/api/oauth/usage",
|
|
() =>
|
|
jsonResponse(
|
|
makeClaudeResponse({
|
|
five_hour: { utilization: "11", resets_at: "2026-06-01T21:00:00Z" },
|
|
}),
|
|
),
|
|
],
|
|
]),
|
|
);
|
|
|
|
const result = await service().listUsage();
|
|
const claude = findProvider(result, "claude");
|
|
|
|
expect(claude).toMatchObject({
|
|
status: "available",
|
|
planLabel: "Pro 1x",
|
|
windows: expect.arrayContaining([
|
|
expect.objectContaining({ id: "five_hour", usedPct: 11 }),
|
|
expect.objectContaining({ id: "weekly", usedPct: 1 }),
|
|
expect.objectContaining({ id: "weekly_model_opus", usedPct: 0.5 }),
|
|
]),
|
|
});
|
|
expect(fetchApi).toHaveBeenCalledWith(
|
|
"https://api.anthropic.com/api/oauth/usage",
|
|
expect.objectContaining({ signal: expect.any(AbortSignal) }),
|
|
);
|
|
});
|
|
it.each([false, true])("reports USD extra usage with rolling windows %j", async (hasWindows) => {
|
|
writeClaudeCredentials(claudeHome, "at_valid");
|
|
fetchApi = mockFetch(
|
|
new Map([
|
|
[
|
|
"https://api.anthropic.com/api/oauth/usage",
|
|
() =>
|
|
jsonResponse({
|
|
...(hasWindows ? makeClaudeResponse() : {}),
|
|
extra_usage: {
|
|
is_enabled: true,
|
|
monthly_limit: 5000,
|
|
used_credits: 1250,
|
|
currency: "USD",
|
|
},
|
|
}),
|
|
],
|
|
]),
|
|
);
|
|
|
|
const claude = findProvider(await service().listUsage(), "claude");
|
|
|
|
expect(claude.balances).toEqual([
|
|
expect.objectContaining({
|
|
id: "extra_usage_monthly",
|
|
used: 12.5,
|
|
remaining: 37.5,
|
|
limit: 50,
|
|
unit: "usd",
|
|
}),
|
|
]);
|
|
expect(claude.details).toContainEqual(
|
|
expect.objectContaining({ id: "extra_usage", value: "Enabled" }),
|
|
);
|
|
});
|
|
|
|
it.each(["EUR", null, undefined])(
|
|
"does not report extra usage balances for currency %j",
|
|
async (currency) => {
|
|
writeClaudeCredentials(claudeHome, "at_valid");
|
|
fetchApi = mockFetch(
|
|
new Map([
|
|
[
|
|
"https://api.anthropic.com/api/oauth/usage",
|
|
() =>
|
|
jsonResponse({
|
|
...makeClaudeResponse(),
|
|
extra_usage: { monthly_limit: 5000, used_credits: 1250, currency },
|
|
}),
|
|
],
|
|
]),
|
|
);
|
|
|
|
const claude = findProvider(await service().listUsage(), "claude");
|
|
|
|
expect(claude.balances).toEqual([]);
|
|
},
|
|
);
|
|
|
|
it("accepts a null Claude resets_at when a window has no scheduled reset", async () => {
|
|
writeClaudeCredentials(claudeHome, "at_valid");
|
|
fetchApi = mockFetch(
|
|
new Map([
|
|
[
|
|
"https://api.anthropic.com/api/oauth/usage",
|
|
() =>
|
|
jsonResponse({
|
|
five_hour: { utilization: 0, resets_at: null },
|
|
seven_day: { utilization: 1, resets_at: "2026-06-04T00:00:00Z" },
|
|
}),
|
|
],
|
|
]),
|
|
);
|
|
|
|
const result = await service().listUsage();
|
|
const claude = findProvider(result, "claude");
|
|
|
|
expect(claude).toMatchObject({
|
|
status: "available",
|
|
windows: expect.arrayContaining([
|
|
expect.objectContaining({ id: "five_hour", usedPct: 0, resetsAt: null }),
|
|
expect.objectContaining({ id: "weekly", usedPct: 1 }),
|
|
]),
|
|
});
|
|
});
|
|
|
|
it.each(["empty home", "unrelated files"])(
|
|
"discovers no Claude logins in %s",
|
|
async (scenario) => {
|
|
if (scenario === "unrelated files") writeFileSync(join(homeDir, "unrelated.json"), "{}");
|
|
expect(
|
|
await discover({ kind: "global" }, { home: homeDir, env: {}, platform: "linux" }),
|
|
).toEqual([]);
|
|
},
|
|
);
|
|
|
|
it("returns unavailable on 401 without refreshing or rewriting credentials", async () => {
|
|
writeClaudeCredentials(claudeHome, "at_expired", "rt_valid");
|
|
const credPath = join(claudeHome, ".credentials.json");
|
|
const before = readFileSync(credPath, "utf8");
|
|
let usageCalls = 0;
|
|
fetchApi = vi.fn(async (url: RequestInfo | URL) => {
|
|
const endpoint = url.toString();
|
|
if (endpoint === "https://api.anthropic.com/api/oauth/usage") {
|
|
usageCalls += 1;
|
|
return new Response(null, { status: 401 });
|
|
}
|
|
// The read-only fetcher must never hit the OAuth token endpoint.
|
|
throw new Error(`Unmocked: ${endpoint}`);
|
|
}) as never;
|
|
|
|
const result = await service().listUsage();
|
|
|
|
expect(findProvider(result, "claude").status).toBe("unavailable");
|
|
expect(usageCalls).toBe(1);
|
|
// The credentials file must be left untouched for the Claude CLI to own.
|
|
expect(readFileSync(credPath, "utf8")).toBe(before);
|
|
});
|
|
});
|
|
|
|
describe("usage bars escalate as they fill", () => {
|
|
let claudeHome: string;
|
|
|
|
beforeEach(() => {
|
|
claudeHome = mkdtempSync(join(tmpdir(), "paseo-tone-claude-"));
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(claudeHome, { recursive: true, force: true });
|
|
});
|
|
|
|
function claudeAt(utilization: number) {
|
|
writeClaudeCredentials(claudeHome, "at_valid");
|
|
process.env["CLAUDE_CONFIG_DIR"] = claudeHome;
|
|
return fetchUsage(
|
|
credentialInput(claudeHome),
|
|
mockFetch(
|
|
new Map([
|
|
[
|
|
"https://api.anthropic.com/api/oauth/usage",
|
|
() => jsonResponse({ seven_day: { utilization, resets_at: "2026-06-04T00:00:00Z" } }),
|
|
],
|
|
]),
|
|
),
|
|
);
|
|
}
|
|
|
|
it.each([
|
|
[10, "ok"],
|
|
[75, "warning"],
|
|
[99, "danger"],
|
|
])("a Claude window at %s%% is %s", async (utilization, tone) => {
|
|
const usage = await claudeAt(utilization);
|
|
expect(usage.windows).toEqual([expect.objectContaining({ id: "weekly", tone })]);
|
|
});
|
|
});
|
|
|
|
describe("Claude usage source scoped weekly limits", () => {
|
|
beforeEach(() => {
|
|
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
});
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
let claudeHome: string;
|
|
|
|
beforeEach(() => {
|
|
claudeHome = mkdtempSync(join(tmpdir(), "paseo-claude-limits-"));
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(claudeHome, { recursive: true, force: true });
|
|
});
|
|
|
|
function fableLimit(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
kind: "weekly_scoped",
|
|
group: "weekly",
|
|
percent: 0,
|
|
severity: "normal",
|
|
resets_at: "2026-06-04T00:00:00Z",
|
|
is_active: false,
|
|
scope: { model: { id: null, display_name: "Fable" }, surface: null },
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function claudeProvider(body: unknown) {
|
|
writeClaudeCredentials(claudeHome, "at_valid");
|
|
process.env["CLAUDE_CONFIG_DIR"] = claudeHome;
|
|
const fetchApi = mockFetch(
|
|
new Map([["https://api.anthropic.com/api/oauth/usage", () => jsonResponse(body)]]),
|
|
);
|
|
return { provider: { fetchUsage: () => fetchUsage(credentialInput(claudeHome), fetchApi) } };
|
|
}
|
|
|
|
it("renders a scoped weekly limit as its own window", async () => {
|
|
const { provider } = claudeProvider({
|
|
five_hour: { utilization: 6, resets_at: "2026-06-01T21:00:00Z" },
|
|
seven_day: { utilization: 23, resets_at: "2026-06-04T00:00:00Z" },
|
|
limits: [fableLimit()],
|
|
});
|
|
|
|
const usage = await provider.fetchUsage();
|
|
|
|
expect(usage.windows).toContainEqual(
|
|
expect.objectContaining({ id: "weekly_model_fable", label: "Weekly · Fable" }),
|
|
);
|
|
});
|
|
|
|
it("summarizes the session and weekly windows by default, not scoped ones", async () => {
|
|
const { provider } = claudeProvider({
|
|
five_hour: { utilization: 6, resets_at: "2026-06-01T21:00:00Z" },
|
|
seven_day: { utilization: 23, resets_at: "2026-06-04T00:00:00Z" },
|
|
limits: [fableLimit()],
|
|
});
|
|
|
|
const usage = await provider.fetchUsage();
|
|
|
|
expect(usage.windows.map((window) => [window.id, window.summary ?? false])).toEqual([
|
|
["five_hour", true],
|
|
["weekly", true],
|
|
["weekly_model_fable", false],
|
|
]);
|
|
});
|
|
|
|
it("gives every window a short label for tight spaces", async () => {
|
|
const { provider } = claudeProvider({
|
|
five_hour: { utilization: 6, resets_at: "2026-06-01T21:00:00Z" },
|
|
seven_day: { utilization: 23, resets_at: "2026-06-04T00:00:00Z" },
|
|
limits: [fableLimit()],
|
|
});
|
|
|
|
const usage = await provider.fetchUsage();
|
|
|
|
expect(usage.windows.map((window) => [window.id, window.shortLabel])).toEqual([
|
|
["five_hour", "5h"],
|
|
["weekly", "wk"],
|
|
["weekly_model_fable", "wk Fable"],
|
|
]);
|
|
});
|
|
|
|
it("renders a scoped window that is at zero and inactive", async () => {
|
|
const { provider } = claudeProvider({
|
|
seven_day: { utilization: 23, resets_at: "2026-06-04T00:00:00Z" },
|
|
limits: [fableLimit({ percent: 0, is_active: false })],
|
|
});
|
|
|
|
const usage = await provider.fetchUsage();
|
|
|
|
expect(usage.windows).toContainEqual(
|
|
expect.objectContaining({ id: "weekly_model_fable", usedPct: 0, remainingPct: 100 }),
|
|
);
|
|
});
|
|
|
|
it("ignores session and all-models entries so they do not duplicate the top-level windows", async () => {
|
|
const { provider } = claudeProvider({
|
|
five_hour: { utilization: 6, resets_at: "2026-06-01T21:00:00Z" },
|
|
seven_day: { utilization: 23, resets_at: "2026-06-04T00:00:00Z" },
|
|
limits: [
|
|
{ kind: "session", percent: 6, resets_at: "2026-06-01T21:00:00Z", scope: null },
|
|
{ kind: "weekly_all", percent: 23, resets_at: "2026-06-04T00:00:00Z", scope: null },
|
|
fableLimit(),
|
|
],
|
|
});
|
|
|
|
const usage = await provider.fetchUsage();
|
|
|
|
expect(usage.windows.map((window) => window.id)).toEqual([
|
|
"five_hour",
|
|
"weekly",
|
|
"weekly_model_fable",
|
|
]);
|
|
});
|
|
|
|
it("labels a surface-scoped limit from its surface name", async () => {
|
|
const { provider } = claudeProvider({
|
|
seven_day: { utilization: 23, resets_at: "2026-06-04T00:00:00Z" },
|
|
limits: [
|
|
fableLimit({ scope: { model: null, surface: { id: "code", display_name: "Code" } } }),
|
|
],
|
|
});
|
|
|
|
const usage = await provider.fetchUsage();
|
|
|
|
expect(usage.windows).toContainEqual(
|
|
expect.objectContaining({ id: "weekly_surface_code", label: "Weekly · Code" }),
|
|
);
|
|
});
|
|
|
|
it("skips a scoped limit with no resolvable label rather than rendering an unlabelled bar", async () => {
|
|
const { provider } = claudeProvider({
|
|
seven_day: { utilization: 23, resets_at: "2026-06-04T00:00:00Z" },
|
|
limits: [fableLimit({ scope: { model: { id: null, display_name: null }, surface: null } })],
|
|
});
|
|
|
|
const usage = await provider.fetchUsage();
|
|
|
|
expect(usage.windows.map((window) => window.id)).toEqual(["weekly"]);
|
|
expect(console.warn).toHaveBeenCalled();
|
|
});
|
|
|
|
// Regression: an additive section must never take down data that already parsed.
|
|
it("keeps the top-level windows when a limits entry is malformed", async () => {
|
|
const { provider } = claudeProvider({
|
|
five_hour: { utilization: 6, resets_at: "2026-06-01T21:00:00Z" },
|
|
seven_day: { utilization: 23, resets_at: "2026-06-04T00:00:00Z" },
|
|
limits: [{ percent: "not-a-kind" }, fableLimit()],
|
|
});
|
|
|
|
const usage = await provider.fetchUsage();
|
|
|
|
expect(usage.status).toBe("available");
|
|
expect(usage.windows.map((window) => window.id)).toEqual([
|
|
"five_hour",
|
|
"weekly",
|
|
"weekly_model_fable",
|
|
]);
|
|
expect(console.warn).toHaveBeenCalled();
|
|
});
|
|
|
|
it("warns when a successful response describes no windows at all", async () => {
|
|
const { provider } = claudeProvider({ limits: [] });
|
|
|
|
const usage = await provider.fetchUsage();
|
|
|
|
expect(usage.windows).toEqual([]);
|
|
expect(console.warn).toHaveBeenCalledWith(
|
|
"Claude usage response parsed but produced no windows",
|
|
);
|
|
});
|
|
});
|
|
/**
|
|
* The reconciliation matrix.
|
|
*
|
|
* Four review rounds on PR #2303 each found a different hole in how the two
|
|
* representations of one scoped limit get combined, because each fix was tested against
|
|
* the case that was reported rather than the space of cases. This walks the space:
|
|
* every combination of which representation carries the limit, whether the `limits[]`
|
|
* entry supplies values, and whether the two descriptions denote the same limit at all.
|
|
*/
|
|
|
|
describe("Claude usage source scoped limit reconciliation", () => {
|
|
let claudeHome: string;
|
|
|
|
beforeEach(() => {
|
|
claudeHome = mkdtempSync(join(tmpdir(), "paseo-claude-matrix-"));
|
|
});
|
|
|
|
afterEach(() => {
|
|
rmSync(claudeHome, { recursive: true, force: true });
|
|
});
|
|
|
|
const RESETS = "2026-06-04T00:00:00Z";
|
|
|
|
function scoped(scope: unknown, percent: number | null = 30, resetsAt: string | null = RESETS) {
|
|
return { kind: "weekly_scoped", percent, resets_at: resetsAt, scope };
|
|
}
|
|
|
|
const model = (name: string | null, id: string | null = null) => ({
|
|
model: { id, display_name: name },
|
|
surface: null,
|
|
});
|
|
const surface = (name: string | null, id: string | null = null) => ({
|
|
model: null,
|
|
surface: { id, display_name: name },
|
|
});
|
|
|
|
async function windowsFor(body: Record<string, unknown>) {
|
|
writeClaudeCredentials(claudeHome, "at_valid");
|
|
process.env["CLAUDE_CONFIG_DIR"] = claudeHome;
|
|
const usage = await fetchUsage(
|
|
credentialInput(claudeHome),
|
|
mockFetch(new Map([["https://api.anthropic.com/api/oauth/usage", () => jsonResponse(body)]])),
|
|
);
|
|
return usage.windows;
|
|
}
|
|
|
|
it("which representation carries the limit: legacy only", async () => {
|
|
const windows = await windowsFor({
|
|
seven_day_omelette: { utilization: 12, resets_at: RESETS },
|
|
});
|
|
expect(windows).toEqual([
|
|
expect.objectContaining({
|
|
id: "weekly_model_omelette",
|
|
label: "Weekly · Omelette",
|
|
usedPct: 12,
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it("which representation carries the limit: limits[] only", async () => {
|
|
const windows = await windowsFor({ limits: [scoped(model("Fable"), 2)] });
|
|
expect(windows).toEqual([
|
|
expect.objectContaining({
|
|
id: "weekly_model_fable",
|
|
label: "Weekly · Fable",
|
|
usedPct: 2,
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it("which representation carries the limit: both, same limit — one bar, scoped identity", async () => {
|
|
const windows = await windowsFor({
|
|
seven_day_omelette: { utilization: 12, resets_at: RESETS },
|
|
limits: [scoped(model("Omelette"), 30)],
|
|
});
|
|
expect(windows).toEqual([
|
|
expect.objectContaining({ id: "weekly_model_omelette", usedPct: 30 }),
|
|
]);
|
|
});
|
|
|
|
it("which representation carries the limit: both, different limits — two bars", async () => {
|
|
const windows = await windowsFor({
|
|
seven_day_opus: { utilization: 8, resets_at: RESETS },
|
|
limits: [scoped(model("Fable"), 2)],
|
|
});
|
|
expect(windows.map((w) => w.id)).toEqual(["weekly_model_opus", "weekly_model_fable"]);
|
|
});
|
|
|
|
it("which representation carries the limit: neither", async () => {
|
|
const windows = await windowsFor({ seven_day: { utilization: 23, resets_at: RESETS } });
|
|
expect(windows.map((w) => w.id)).toEqual(["weekly"]);
|
|
});
|
|
|
|
const legacy = { seven_day_omelette: { utilization: 12, resets_at: RESETS } };
|
|
|
|
it("value fallback when the scoped entry is sparse: scoped values win when present", async () => {
|
|
const windows = await windowsFor({
|
|
...legacy,
|
|
limits: [scoped(model("Omelette"), 30, "2026-06-09T00:00:00Z")],
|
|
});
|
|
expect(windows[0]).toMatchObject({ usedPct: 30, resetsAt: "2026-06-09T00:00:00Z" });
|
|
});
|
|
|
|
it("value fallback when the scoped entry is sparse: percentage falls back per field", async () => {
|
|
const windows = await windowsFor({
|
|
...legacy,
|
|
limits: [scoped(model("Omelette"), null, "2026-06-09T00:00:00Z")],
|
|
});
|
|
expect(windows[0]).toMatchObject({ usedPct: 12, resetsAt: "2026-06-09T00:00:00Z" });
|
|
});
|
|
|
|
it("value fallback when the scoped entry is sparse: reset time falls back per field", async () => {
|
|
const windows = await windowsFor({
|
|
...legacy,
|
|
limits: [scoped(model("Omelette"), 30, null)],
|
|
});
|
|
expect(windows[0]).toMatchObject({ usedPct: 30, resetsAt: RESETS });
|
|
});
|
|
|
|
it("value fallback when the scoped entry is sparse: both fall back when the scoped entry only names the limit", async () => {
|
|
const windows = await windowsFor({
|
|
...legacy,
|
|
limits: [scoped(model("Omelette"), null, null)],
|
|
});
|
|
expect(windows[0]).toMatchObject({ usedPct: 12, resetsAt: RESETS });
|
|
});
|
|
|
|
it("value fallback when the scoped entry is sparse: stays empty when neither side has a value", async () => {
|
|
const windows = await windowsFor({ limits: [scoped(model("Fable"), null, null)] });
|
|
expect(windows[0]).toMatchObject({ id: "weekly_model_fable", usedPct: null });
|
|
});
|
|
|
|
it("identity: a surface never matches a legacy model window of the same name", async () => {
|
|
const windows = await windowsFor({
|
|
seven_day_omelette: { utilization: 12, resets_at: RESETS },
|
|
limits: [scoped(surface("Omelette"), 30)],
|
|
});
|
|
expect(windows.map((w) => w.id)).toEqual(["weekly_model_omelette", "weekly_surface_omelette"]);
|
|
expect(windows[0]).toMatchObject({ usedPct: 12 });
|
|
expect(windows[1]).toMatchObject({ usedPct: 30 });
|
|
});
|
|
|
|
it("identity: a model and a surface of the same name stay apart", async () => {
|
|
const windows = await windowsFor({
|
|
limits: [scoped(model("Code"), 4), scoped(surface("Code"), 9)],
|
|
});
|
|
expect(windows.map((w) => w.id)).toEqual(["weekly_model_code", "weekly_surface_code"]);
|
|
});
|
|
|
|
it("identity: ids decide when both sides have one", async () => {
|
|
const windows = await windowsFor({
|
|
limits: [
|
|
scoped(model("Fable-Pro", "fable-pro"), 4),
|
|
scoped(model("Fable_Pro", "fable_pro"), 9),
|
|
],
|
|
});
|
|
expect(windows.map((w) => w.id)).toEqual(["weekly_model_fable-pro", "weekly_model_fable_pro"]);
|
|
});
|
|
|
|
it("identity: names decide when ids are absent, so indistinguishable entries merge", async () => {
|
|
const windows = await windowsFor({
|
|
limits: [scoped(model("Fable Pro"), 4), scoped(model("Fable-Pro"), 9)],
|
|
});
|
|
expect(windows).toEqual([
|
|
expect.objectContaining({ id: "weekly_model_fable_pro", usedPct: 9 }),
|
|
]);
|
|
});
|
|
|
|
it("identity: a renamed scope keeps its id when the API supplies one", async () => {
|
|
const before = await windowsFor({ limits: [scoped(model("Fable", "fable"), 2)] });
|
|
const after = await windowsFor({ limits: [scoped(model("Fable 5", "fable"), 2)] });
|
|
expect(before[0]?.id).toBe("weekly_model_fable");
|
|
expect(after[0]?.id).toBe("weekly_model_fable");
|
|
expect(after[0]?.label).toBe("Weekly · Fable 5");
|
|
});
|
|
|
|
it("identity: a limit keeps one id whichever representation carries it", async () => {
|
|
const viaLegacy = await windowsFor({
|
|
seven_day_omelette: { utilization: 12, resets_at: RESETS },
|
|
});
|
|
const viaLimits = await windowsFor({ limits: [scoped(model("Omelette"), 12)] });
|
|
expect(viaLegacy[0]?.id).toBe(viaLimits[0]?.id);
|
|
});
|
|
|
|
it("ordering and unscoped windows: puts session and weekly ahead of the scoped bars", async () => {
|
|
const windows = await windowsFor({
|
|
five_hour: { utilization: 6, resets_at: RESETS },
|
|
seven_day: { utilization: 23, resets_at: RESETS },
|
|
seven_day_opus: { utilization: 8, resets_at: RESETS },
|
|
limits: [
|
|
{ kind: "session", percent: 6, resets_at: RESETS, scope: null },
|
|
{ kind: "weekly_all", percent: 23, resets_at: RESETS, scope: null },
|
|
scoped(model("Fable"), 2),
|
|
],
|
|
});
|
|
expect(windows.map((w) => w.id)).toEqual([
|
|
"five_hour",
|
|
"weekly",
|
|
"weekly_model_opus",
|
|
"weekly_model_fable",
|
|
]);
|
|
});
|
|
});
|
|
|
|
it("discovery uses the credential token profile instead of stale Claude config", async () => {
|
|
const { mkdtemp, writeFile, rm } = await import("node:fs/promises");
|
|
const directory = await mkdtemp(join(tmpdir(), "claude-identity-"));
|
|
try {
|
|
await writeFile(
|
|
join(directory, ".claude.json"),
|
|
JSON.stringify({
|
|
oauthAccount: {
|
|
accountUuid: "account-uuid",
|
|
organizationUuid: "org-uuid",
|
|
emailAddress: "test@example.com",
|
|
},
|
|
}),
|
|
);
|
|
writeClaudeCredentials(directory, "fixture-local-account");
|
|
expect(
|
|
await accountFor(
|
|
credentialInput(directory),
|
|
async () =>
|
|
jsonResponse({
|
|
account: { uuid: "token-account", email: "token@example.test" },
|
|
organization: { uuid: "token-org" },
|
|
}),
|
|
Date.now,
|
|
{ home: directory, claudeHome: directory },
|
|
),
|
|
).toEqual({
|
|
key: "token-account.token-org",
|
|
label: "token@example.test",
|
|
});
|
|
} finally {
|
|
await rm(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("Claude credentials without account metadata use a cached OAuth profile identity", async () => {
|
|
let calls = 0;
|
|
const fetchProfile: typeof fetch = async () => {
|
|
calls++;
|
|
return new Response(
|
|
JSON.stringify({
|
|
account: { uuid: "account-uuid", email: "test@example.com" },
|
|
organization: { uuid: "org-uuid" },
|
|
}),
|
|
{ status: 200 },
|
|
);
|
|
};
|
|
const lookup = tokenLookup("fixture-claude-token");
|
|
expect(
|
|
await accountFor(credentialInput(lookup.claudeHome), fetchProfile, Date.now, lookup),
|
|
).toEqual({
|
|
key: "account-uuid.org-uuid",
|
|
label: "test@example.com",
|
|
});
|
|
expect(
|
|
await accountFor(credentialInput(lookup.claudeHome), fetchProfile, Date.now, lookup),
|
|
).toEqual({
|
|
key: "account-uuid.org-uuid",
|
|
label: "test@example.com",
|
|
});
|
|
expect(calls).toBe(1);
|
|
});
|
|
|
|
it("identifies the same credential route that fetch uses when oauthAccount is absent", async () => {
|
|
const directory = mkdtempSync(join(tmpdir(), "claude-credential-identity-"));
|
|
try {
|
|
writeClaudeCredentials(directory, "fixture-credential-token");
|
|
writeFileSync(join(directory, ".claude.json"), JSON.stringify({}));
|
|
let usageRequested = false;
|
|
await fetchUsage(
|
|
credentialInput(directory),
|
|
async () => {
|
|
usageRequested = true;
|
|
return new Response(null, { status: 401 });
|
|
},
|
|
{ claudeHome: directory },
|
|
);
|
|
expect(usageRequested).toBe(true);
|
|
let profileRequested = false;
|
|
const account = await accountFor(
|
|
credentialInput(directory),
|
|
async () => {
|
|
profileRequested = true;
|
|
return jsonResponse({
|
|
account: { uuid: "account-uuid", email: "owner@example.test" },
|
|
organization: { uuid: "org-uuid" },
|
|
});
|
|
},
|
|
Date.now,
|
|
{ claudeHome: directory, home: directory },
|
|
);
|
|
expect(profileRequested).toBe(true);
|
|
expect(account).toEqual({ key: "account-uuid.org-uuid", label: "owner@example.test" });
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("expires cached token profiles after five minutes", async () => {
|
|
let now = 0;
|
|
let calls = 0;
|
|
const fetchProfile: typeof fetch = async () => {
|
|
calls++;
|
|
return jsonResponse({ account: { uuid: "account-uuid" }, organization: { uuid: "org-uuid" } });
|
|
};
|
|
const lookup = tokenLookup("ttl-fixture-token");
|
|
const input = credentialInput(lookup.claudeHome);
|
|
await accountFor(input, fetchProfile, () => now, lookup);
|
|
await accountFor(input, fetchProfile, () => now, lookup);
|
|
expect(calls).toBe(1);
|
|
now = 300_001;
|
|
await accountFor(input, fetchProfile, () => now, lookup);
|
|
expect(calls).toBe(2);
|
|
});
|
|
|
|
it("discovery and fetch share the explicit macOS keychain route", async () => {
|
|
const directory = mkdtempSync(join(tmpdir(), "claude-keychain-identity-"));
|
|
try {
|
|
const lookup = {
|
|
platform: "darwin" as const,
|
|
env: { USER: "fixture", CLAUDE_CONFIG_DIR: directory },
|
|
claudeHome: directory,
|
|
home: directory,
|
|
readKeychainCredentials: async () => ({
|
|
claudeAiOauth: { accessToken: "keychain-fixture-token" },
|
|
}),
|
|
};
|
|
let usageRequested = false;
|
|
await fetchUsage(
|
|
{
|
|
route: {
|
|
store: "keychain",
|
|
account: "fixture",
|
|
service: `Claude Code-credentials-${hashAccountKey(directory).slice(0, 8)}`,
|
|
},
|
|
},
|
|
async () => {
|
|
usageRequested = true;
|
|
return new Response(null, { status: 401 });
|
|
},
|
|
lookup,
|
|
);
|
|
expect(usageRequested).toBe(true);
|
|
const account = await accountFor(
|
|
{
|
|
route: {
|
|
store: "keychain",
|
|
account: "fixture",
|
|
service: `Claude Code-credentials-${hashAccountKey(directory).slice(0, 8)}`,
|
|
},
|
|
},
|
|
async () =>
|
|
jsonResponse({
|
|
account: { uuid: "keychain-account" },
|
|
organization: { uuid: "keychain-org" },
|
|
}),
|
|
Date.now,
|
|
lookup,
|
|
);
|
|
expect(account).toEqual({ key: "keychain-account.keychain-org" });
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("discovery and fetch share the explicit Claude credential route", async () => {
|
|
const directory = mkdtempSync(join(tmpdir(), "claude-home-identity-"));
|
|
try {
|
|
writeClaudeCredentials(directory, "home-fixture-token");
|
|
const lookup = { claudeHome: directory, home: directory };
|
|
let usageRequested = false;
|
|
await fetchUsage(
|
|
credentialInput(directory),
|
|
async () => {
|
|
usageRequested = true;
|
|
return new Response(null, { status: 401 });
|
|
},
|
|
lookup,
|
|
);
|
|
expect(usageRequested).toBe(true);
|
|
const identity = await accountFor(
|
|
credentialInput(directory),
|
|
async () =>
|
|
jsonResponse({
|
|
account: { uuid: "home-account" },
|
|
organization: { uuid: "home-org" },
|
|
}),
|
|
Date.now,
|
|
lookup,
|
|
);
|
|
expect(identity).toEqual({ key: "home-account.home-org" });
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("bounds cached profiles across many token rotations", async () => {
|
|
let calls = 0;
|
|
const fetchProfile: typeof fetch = async () => {
|
|
calls++;
|
|
return jsonResponse({ account: { uuid: "account" }, organization: { uuid: "org" } });
|
|
};
|
|
for (let index = 0; index < 129; index++) {
|
|
const lookup = tokenLookup(`rotation-fixture-${index}`);
|
|
await accountFor(credentialInput(lookup.claudeHome), fetchProfile, Date.now, lookup);
|
|
}
|
|
const lookup = tokenLookup("rotation-fixture-0");
|
|
await accountFor(credentialInput(lookup.claudeHome), fetchProfile, Date.now, lookup);
|
|
expect(calls).toBe(130);
|
|
});
|
|
|
|
it("discovery input requires a route and rejects raw credentials", () => {
|
|
expect(inputSchema.safeParse({ accessToken: "unused" }).success).toBe(false);
|
|
expect(inputSchema.safeParse({ configDir: "/unused" }).success).toBe(false);
|
|
expect(() => inputSchema.parse({})).toThrow();
|
|
});
|
|
|
|
const tokenDirectories: string[] = [];
|
|
afterEach(() => {
|
|
for (const directory of tokenDirectories.splice(0))
|
|
rmSync(directory, { recursive: true, force: true });
|
|
});
|
|
function tokenLookup(accessToken: string) {
|
|
const directory = mkdtempSync(join(tmpdir(), "claude-profile-token-"));
|
|
tokenDirectories.push(directory);
|
|
writeClaudeCredentials(directory, accessToken);
|
|
return { claudeHome: directory, home: directory };
|
|
}
|
|
|
|
it("discovers a Pi OAuth login independently of the CLI login", async () => {
|
|
const { mkdir, mkdtemp, writeFile, rm } = await import("node:fs/promises");
|
|
const directory = await mkdtemp(join(tmpdir(), "usage-pi-discovery-"));
|
|
try {
|
|
await mkdir(join(directory, ".pi", "agent"), { recursive: true });
|
|
await writeFile(
|
|
join(directory, ".pi", "agent", "auth.json"),
|
|
JSON.stringify({
|
|
anthropic: { type: "oauth", access: "fixture-pi", accountId: "pi-account" },
|
|
}),
|
|
);
|
|
const inputs = await discover(
|
|
{ kind: "global" },
|
|
{ home: directory, env: {}, platform: "linux" },
|
|
);
|
|
expect(inputs.map((account) => account.input)).toContainEqual({
|
|
route: { store: "pi", path: join(directory, ".pi", "agent", "auth.json") },
|
|
});
|
|
} finally {
|
|
await rm(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("keeps an expired Keychain login without calling the network and prefers Keychain over the fallback file", async () => {
|
|
const directory = mkdtempSync(join(tmpdir(), "claude-expired-"));
|
|
try {
|
|
writeClaudeCredentials(directory, "stale-file");
|
|
const lookup = {
|
|
home: directory,
|
|
claudeHome: directory,
|
|
env: { USER: "fixture" },
|
|
platform: "darwin" as const,
|
|
now: () => 10_800_000,
|
|
readKeychainCredentials: async () => ({
|
|
claudeAiOauth: { accessToken: "expired-keychain", expiresAt: 0 },
|
|
}),
|
|
};
|
|
let calls = 0;
|
|
const network: typeof fetch = async () => {
|
|
calls++;
|
|
throw new Error("must not call network");
|
|
};
|
|
const accounts = await discover({ kind: "global" }, lookup, network);
|
|
expect(accounts).toEqual([
|
|
{
|
|
key: expect.stringMatching(/^[a-f0-9]{64}$/),
|
|
harness: "Claude",
|
|
input: {
|
|
route: { store: "keychain", service: "Claude Code-credentials", account: "fixture" },
|
|
},
|
|
},
|
|
]);
|
|
expect(await fetchUsage(accounts[0]!.input as UsageInput, network, lookup)).toEqual({
|
|
status: "unavailable",
|
|
problem: { kind: "expired", expiresAt: "1970-01-01T00:00:00.000Z", refreshedBy: "claude" },
|
|
});
|
|
expect(calls).toBe(0);
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
async function accountFor(
|
|
input: UsageInput,
|
|
fetchApi: typeof fetch,
|
|
now: () => number,
|
|
lookup: Parameters<typeof discover>[0] = {},
|
|
) {
|
|
const accounts = await discover({ kind: "global" }, { ...lookup, now }, fetchApi);
|
|
const account = accounts.find((candidate) => isDeepStrictEqual(candidate.input, input));
|
|
if (!account) throw new Error("Expected discovered login");
|
|
return { key: account.key, ...(account.label ? { label: account.label } : {}) };
|
|
}
|
|
it.each([401, 403])("fetch reports rejected usage HTTP %i", async (status) => {
|
|
const lookup = tokenLookup(`usage-rejection-${status}`);
|
|
expect(
|
|
await fetchUsage(
|
|
credentialInput(lookup.claudeHome),
|
|
async () => new Response(null, { status }),
|
|
lookup,
|
|
),
|
|
).toEqual({
|
|
status: "unavailable",
|
|
problem: { kind: "rejected", status, refreshedBy: "claude" },
|
|
});
|
|
});
|
|
|
|
it.each([401, 403])("a rejected profile HTTP %i keeps a locator-keyed account", async (status) => {
|
|
const lookup = tokenLookup(`profile-rejection-${status}`);
|
|
const api: typeof fetch = async () => new Response(null, { status });
|
|
const accounts = await discover({ kind: "global" }, lookup, api);
|
|
expect(accounts).toEqual([
|
|
{
|
|
key: expect.stringMatching(/^[a-f0-9]{64}$/),
|
|
harness: "Claude",
|
|
input: credentialInput(lookup.claudeHome),
|
|
},
|
|
]);
|
|
let fetches = 0;
|
|
expect(
|
|
await fetchUsage(
|
|
credentialInput(lookup.claudeHome),
|
|
async () => {
|
|
fetches++;
|
|
throw new Error("profile already rejected");
|
|
},
|
|
lookup,
|
|
),
|
|
).toEqual({
|
|
status: "unavailable",
|
|
problem: { kind: "rejected", status, refreshedBy: "claude" },
|
|
});
|
|
expect(fetches).toBe(0);
|
|
});
|
|
|
|
it.each([
|
|
{ failure: "HTTP 500", networkError: false, error: "Claude usage API returned 500" },
|
|
{ failure: "a network error", networkError: true, error: "Network unavailable" },
|
|
])(
|
|
"keeps a locator-keyed login when the profile fails with $failure",
|
|
async ({ networkError, error }) => {
|
|
const lookup = tokenLookup(`profile-failure-${networkError}`);
|
|
const input = credentialInput(lookup.claudeHome);
|
|
const api: typeof fetch = async () => {
|
|
if (networkError) throw new Error(error);
|
|
return new Response(null, { status: 500 });
|
|
};
|
|
const accounts = await discover({ kind: "global" }, lookup, api);
|
|
expect(accounts).toEqual([
|
|
{ key: hashAccountKey(JSON.stringify(input.route)), harness: "Claude", input },
|
|
]);
|
|
await expect(fetchUsage(accounts[0]!.input as UsageInput, api, lookup)).rejects.toThrow(error);
|
|
},
|
|
);
|
|
|
|
it("says when a rate-limited login can retry and does not call the API before then", async () => {
|
|
let now = Date.parse("2026-10-04T21:00:00Z");
|
|
const lookup = { ...tokenLookup("rate-limited-token"), now: () => now };
|
|
const input = credentialInput(lookup.claudeHome);
|
|
let usageCalls = 0;
|
|
const api: typeof fetch = async () => {
|
|
usageCalls += 1;
|
|
if (usageCalls > 1) return jsonResponse(makeClaudeResponse());
|
|
return jsonResponse(
|
|
{ error: { type: "rate_limit_error", message: "Rate limited. Please try again later." } },
|
|
429,
|
|
{ "Retry-After": "2240" },
|
|
);
|
|
};
|
|
|
|
await expect(fetchUsage(input, api, lookup)).rejects.toThrow(
|
|
"Rate limited by Claude. Try again in 38m.",
|
|
);
|
|
now += 30 * 60_000;
|
|
await expect(fetchUsage(input, api, lookup)).rejects.toThrow(
|
|
"Rate limited by Claude. Try again in 8m.",
|
|
);
|
|
expect(usageCalls).toBe(1);
|
|
|
|
now += 8 * 60_000;
|
|
expect((await fetchUsage(input, api, lookup)).status).toBe("available");
|
|
expect(usageCalls).toBe(2);
|
|
});
|
|
|
|
it("re-reads a refreshed login at the original locator", async () => {
|
|
const lookup = tokenLookup("expiring-token");
|
|
writeFileSync(
|
|
join(lookup.claudeHome, ".credentials.json"),
|
|
JSON.stringify({ claudeAiOauth: { accessToken: "expiring-token", expiresAt: 0 } }),
|
|
);
|
|
const [account] = await discover({ kind: "global" }, lookup, async () => {
|
|
throw new Error("expired token must not fetch");
|
|
});
|
|
expect(await fetchUsage(account!.input as UsageInput, fetch, lookup)).toEqual({
|
|
status: "unavailable",
|
|
problem: { kind: "expired", expiresAt: "1970-01-01T00:00:00.000Z", refreshedBy: "claude" },
|
|
});
|
|
writeClaudeCredentials(lookup.claudeHome, "rotated-token");
|
|
const report = await fetchUsage(
|
|
account!.input as UsageInput,
|
|
async (_url, init) => {
|
|
expect(new Headers(init?.headers).get("Authorization")).toBe("Bearer rotated-token");
|
|
return jsonResponse(makeClaudeResponse());
|
|
},
|
|
lookup,
|
|
);
|
|
expect(report.status).toBe("available");
|
|
});
|
|
|
|
it("session discovery reads only its Claude home and excludes foreign sessions", async () => {
|
|
const home = mkdtempSync(join(tmpdir(), "claude-session-scope-"));
|
|
try {
|
|
writeClaudeCredentials(home, "scoped-unique-token");
|
|
const scope = {
|
|
kind: "session" as const,
|
|
provider: "claude",
|
|
env: { HOME: home, CLAUDE_CONFIG_DIR: home },
|
|
};
|
|
const calls: string[] = [];
|
|
const api: typeof fetch = async (url) => {
|
|
calls.push(String(url));
|
|
return jsonResponse({
|
|
account: { uuid: "session-account", email: "session@example.test" },
|
|
organization: { uuid: "org" },
|
|
});
|
|
};
|
|
const lookup = { platform: "linux" as const, home: "/unused-default-home", env: {} };
|
|
const accounts = await discover(scope, lookup, api);
|
|
expect(accounts).toEqual([
|
|
{
|
|
key: "session-account.org",
|
|
label: "session@example.test",
|
|
harness: "Claude",
|
|
input: credentialInput(home),
|
|
},
|
|
]);
|
|
await discover(scope, lookup, api);
|
|
expect(calls).toEqual(["https://api.anthropic.com/api/oauth/profile"]);
|
|
for (const env of [
|
|
{ CLAUDE_CODE_USE_BEDROCK: "1" },
|
|
{ CLAUDE_CODE_USE_VERTEX: "1" },
|
|
{ ANTHROPIC_BASE_URL: "https://other.test" },
|
|
]) {
|
|
expect(await discover({ ...scope, env: { ...scope.env, ...env } }, lookup, api)).toEqual([]);
|
|
}
|
|
expect(await discover({ ...scope, provider: "codex" }, lookup, api)).toEqual([]);
|
|
expect(
|
|
await discover(
|
|
{ ...scope, env: { ...scope.env, CLAUDE_CONFIG_DIR: join(home, "missing") } },
|
|
lookup,
|
|
api,
|
|
),
|
|
).toEqual([]);
|
|
} finally {
|
|
rmSync(home, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("session Keychain input retains the selected config-directory locator through fetch", async () => {
|
|
const reads: string[] = [];
|
|
const scope = {
|
|
kind: "session" as const,
|
|
provider: "claude",
|
|
env: { HOME: "/fixture", USER: "fixture-user", CLAUDE_CONFIG_DIR: "/fixture/work" },
|
|
};
|
|
const service = `Claude Code-credentials-${hashAccountKey(scope.env.CLAUDE_CONFIG_DIR).slice(0, 8)}`;
|
|
const lookup = {
|
|
platform: "darwin" as const,
|
|
readKeychainCredentials: async (selected: string, account: string) => {
|
|
reads.push(`${selected}:${account}`);
|
|
return { claudeAiOauth: { accessToken: "scoped-keychain-fixture" } };
|
|
},
|
|
};
|
|
const accounts = await discover(scope, lookup, async () =>
|
|
jsonResponse({ account: { uuid: "keychain-scoped" }, organization: { uuid: "org" } }),
|
|
);
|
|
expect(accounts).toHaveLength(1);
|
|
const input = inputSchema.parse(accounts[0]?.input);
|
|
expect(input.route).toEqual({ store: "keychain", service, account: "fixture-user" });
|
|
await fetchUsage(input, async () => jsonResponse(makeClaudeResponse()), {
|
|
...lookup,
|
|
env: { CLAUDE_CONFIG_DIR: "/somewhere-else" },
|
|
});
|
|
expect(reads).toEqual(Array(3).fill(`${service}:fixture-user`));
|
|
});
|