feat(FN-1397): strengthen ProjectOverview tests with sorting and filtering behavior

This commit is contained in:
gsxdsm
2026-04-09 15:03:52 -07:00
parent ebf7e6ec74
commit 5462a3085a
2 changed files with 396 additions and 9 deletions

View File

@@ -10,6 +10,7 @@ import {
_parseClaudePercentLine,
_parseClaudeResetLine,
_parseClaudeResetText,
_parseResetTimestamp,
withTimeout,
CLAUDE_FETCH_TIMEOUT_MS,
_clearRefreshedToken,
@@ -3328,4 +3329,332 @@ describe("usage", () => {
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;
mockReadFileSync.mockImplementation((filePath: string) => {
if (filePath.includes("claude") && credFileContent !== null) {
return JSON.stringify(credFileContent);
}
throw 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();
});
});
});