fix(FN-1014): add fallback reset time for Claude session window when API omits resets_at

- When the Claude usage API omits resets_at for the 5-hour session window, fall back to using the full window duration as resetMs
- This enables pace calculation and reset text display even without an explicit reset timestamp
- Update existing test to verify fallback behavior (resetMs = 5h, resetText = 'resets in 5h')
- Add new test for pace calculation with fallback reset time
This commit is contained in:
gsxdsm
2026-04-05 19:45:17 -07:00
parent 9e499df124
commit fb6db73a89
2 changed files with 43 additions and 3 deletions

View File

@@ -769,7 +769,7 @@ describe("usage", () => {
expect(weeklyWindow.resetAt).toBeDefined();
});
it("omits resetAt when API response has no resets_at field", async () => {
it("uses fallback reset time for session window when API omits resets_at", async () => {
setupClaudeMocks({
credFileContent: {
accessToken: "test-token",
@@ -781,7 +781,7 @@ describe("usage", () => {
setupClaudeApiResponse({
five_hour: {
utilization: 30.0,
// no resets_at field
// no resets_at field — triggers fallback
},
});
@@ -791,7 +791,40 @@ describe("usage", () => {
expect(claude.status).toBe("ok");
const sessionWindow = claude.windows.find((w) => w.label.includes("Session"))!;
expect(sessionWindow.resetAt).toBeUndefined();
expect(sessionWindow.resetText).toBeNull();
// 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");
// resetAt stays undefined because the API didn't provide it
expect(sessionWindow.resetAt).toBeUndefined();
});
it("handles empty JSON object from API gracefully", async () => {

View File

@@ -842,6 +842,13 @@ async function fetchClaudeUsage(): Promise<ProviderUsage> {
const msLeft = new Date(resetAtValue).getTime() - Date.now();
resetMs = msLeft > 0 ? msLeft : 0;
resetText = msLeft > 0 ? `resets in ${formatDuration(msLeft)}` : "resetting now";
} else if (windowDurationMs === FIVE_HOURS_MS) {
// Fallback for session window: when the API doesn't provide reset time,
// use the full window duration as a best-effort estimate. This enables
// pace calculation and reset text for the session (5h) window even when
// the API omits resets_at / reset_at / resetAt fields.
resetMs = windowDurationMs;
resetText = "resets in 5h";
}
return {