fix(FN-1388): display Claude session reset time in usage dialog
- Fix usage dialog to show Claude session reset time when session is paused - Add unit tests for reset time display logic in usage calculations - Update usage summary and history components to include reset time
This commit is contained in:
@@ -851,6 +851,93 @@ describe("usage", () => {
|
||||
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: {
|
||||
|
||||
@@ -829,15 +829,31 @@ async function fetchClaudeUsage(): Promise<ProviderUsage> {
|
||||
const FIVE_HOURS_MS = 5 * 60 * 60 * 1000;
|
||||
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
|
||||
const parseWindow = (key: string, label: string, windowDurationMs: number): UsageWindow | null => {
|
||||
const w = data[key];
|
||||
if (!w || typeof w !== "object") return null;
|
||||
/**
|
||||
* Get a window data object from the API response, checking multiple possible keys
|
||||
* for backward compatibility (API may use `session` instead of `five_hour`).
|
||||
*/
|
||||
const getWindowData = (primaryKey: string, fallbackKeys: string[] = []): any => {
|
||||
if (data[primaryKey] && typeof data[primaryKey] === "object") {
|
||||
return data[primaryKey];
|
||||
}
|
||||
for (const key of fallbackKeys) {
|
||||
if (data[key] && typeof data[key] === "object") {
|
||||
return data[key];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const pctUsed: number = w.utilization ?? w.percent_used ?? w.percentUsed ?? 0;
|
||||
const parseWindow = (key: string, label: string, windowDurationMs: number, fallbackKeys: string[] = []): UsageWindow | null => {
|
||||
const w = getWindowData(key, fallbackKeys);
|
||||
if (!w) return null;
|
||||
|
||||
const pctUsed: number = w.utilization ?? w.percent_used ?? w.percentUsed ?? w.used_percent ?? w.usage_percent ?? 0;
|
||||
let resetText: string | null = null;
|
||||
let resetMs: number | undefined;
|
||||
|
||||
let resetAtValue = w.resets_at || w.reset_at || w.resetAt;
|
||||
let resetAtValue = w.resets_at || w.reset_at || w.resetAt || w.resetsAt || w.reset_time || w.resetsAtTime;
|
||||
if (resetAtValue) {
|
||||
const msLeft = new Date(resetAtValue).getTime() - Date.now();
|
||||
resetMs = msLeft > 0 ? msLeft : 0;
|
||||
@@ -863,7 +879,7 @@ async function fetchClaudeUsage(): Promise<ProviderUsage> {
|
||||
};
|
||||
};
|
||||
|
||||
const fiveHour = parseWindow("five_hour", "Session (5h)", FIVE_HOURS_MS);
|
||||
const fiveHour = parseWindow("five_hour", "Session (5h)", FIVE_HOURS_MS, ["session"]);
|
||||
const sevenDay = parseWindow("seven_day", "Weekly", SEVEN_DAYS_MS);
|
||||
const sonnet = parseWindow("seven_day_sonnet", "Weekly (Sonnet)", SEVEN_DAYS_MS);
|
||||
const opus = parseWindow("seven_day_opus", "Weekly (Opus)", SEVEN_DAYS_MS);
|
||||
|
||||
Reference in New Issue
Block a user