diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.integration.test.tsx new file mode 100644 index 00000000000..c97f6a70b4f --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.integration.test.tsx @@ -0,0 +1,121 @@ +import { fireEvent, screen, waitFor } from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +import { EMPTY_DAILY_ACTIVITY_RESPONSE } from "@/components/UsagePage/dailyActivityApi"; +import { renderWithProviders, testQueryClient } from "../../../../../tests/test-utils"; +import type { AutoRouterBenchmarksResponse } from "./autoRouterBenchmarks"; +import CostOptimizationView from "./CostOptimizationView"; + +const emptyBucket = { turns: 0, hits: 0, hit_rate_pct: 0 }; +const emptyBenchmarks: AutoRouterBenchmarksResponse = { + start_date: "2026-09-14", + end_date: "2026-10-14", + routers_in_scope: 0, + groups: [], + totals: { + sessions: 0, + turns: 0, + avg_turns_per_session: 0, + avg_session_seconds: 0, + avg_tokens_per_session: 0, + spend: 0, + savings_estimated_turns: 0, + savings_estimated_actual_spend: 0, + savings_estimated_classifier_cost: 0, + classifier_cost: 0, + saved_spend: 0, + baseline_spend: 0, + saved_pct: 0, + cache: { + coverage_pct: 0, + hit_rate_pct: 0, + same_model: emptyBucket, + first_visit: emptyBucket, + return_to_tier: emptyBucket, + unordered_turns: 0, + return_misses_expired: 0, + return_misses_within_ttl: 0, + return_misses_unknown: 0, + ttl_5m_turns: 0, + ttl_1h_turns: 0, + }, + }, +}; + +const requestUrl = (input: Parameters[0]): URL => + new URL(input instanceof Request ? input.url : String(input), "http://localhost:4000"); + +describe("CostOptimizationView shared date range", () => { + beforeEach(() => { + vi.useFakeTimers({ toFake: ["Date"] }); + vi.setSystemTime(new Date("2026-10-14T12:00:00")); + vi.stubGlobal("requestIdleCallback", (callback: IdleRequestCallback) => { + callback({ didTimeout: false, timeRemaining: () => 50 }); + return 0; + }); + const payload = btoa(JSON.stringify({ key: "sk-test", user_id: "u1", user_role: "proxy_admin" })); + document.cookie = `token=e30.${payload}.test; path=/`; + testQueryClient.clear(); + }); + + afterEach(() => { + document.cookie = "token=; max-age=0; path=/"; + testQueryClient.clear(); + vi.useRealTimers(); + vi.unstubAllGlobals(); + }); + + it("shares the initial seven days and a custom wider range across the three date pickers", async () => { + const mockFetch = vi.fn().mockImplementation(async (input) => { + const path = requestUrl(input).pathname; + const responses: Readonly> = { + "/user/daily/activity/aggregated": EMPTY_DAILY_ACTIVITY_RESPONSE, + "/litellm/.well-known/litellm-ui-config": { admin_ui_disabled: false }, + "/v1/tool/spend": { by_tool: [], daily: [] }, + "/config/list": [], + "/v2/model/info": { data: [] }, + "/auto_router/benchmarks": emptyBenchmarks, + "/user/daily/activity/aggregated/cache_leakage_keys": { api_keys: [] }, + "/cost_optimization/prompt_caching/requests": { requests: [], next_cursor: null }, + }; + const data = responses[path] ?? []; + return new Response(JSON.stringify(data), { status: 200, headers: { "Content-Type": "application/json" } }); + }); + vi.stubGlobal("fetch", mockFetch); + renderWithProviders(); + + const activityRequests = () => + mockFetch.mock.calls + .map(([input]) => requestUrl(input)) + .filter((url) => url.pathname === "/user/daily/activity/aggregated"); + await waitFor(() => expect(activityRequests()).toHaveLength(1)); + expect(activityRequests()[0].searchParams.get("start_date")).toBe("2026-10-08"); + expect(activityRequests()[0].searchParams.get("end_date")).toBe("2026-10-14"); + + const initialRange = /8 Oct, 00:00 - 14 Oct, 23:59/; + expect(screen.getByRole("button", { name: initialRange })).toBeInTheDocument(); + fireEvent.click(screen.getByRole("tab", { name: "Prompt Caching" })); + fireEvent.click(screen.getByRole("button", { name: initialRange })); + fireEvent.change(screen.getByDisplayValue("2026-10-08"), { target: { value: "2026-09-14" } }); + fireEvent.click(screen.getByRole("button", { name: "Apply" })); + + await waitFor(() => expect(activityRequests().at(-1)?.searchParams.get("start_date")).toBe("2026-09-14")); + expect(activityRequests().at(-1)?.searchParams.get("end_date")).toBe("2026-10-14"); + const selectedRange = /14 Sep, 00:00 - 14 Oct, 23:59/; + expect(screen.getByRole("button", { name: selectedRange })).toBeInTheDocument(); + fireEvent.click(screen.getByRole("tab", { name: "Auto-Router" })); + expect(screen.getByRole("button", { name: selectedRange })).toBeInTheDocument(); + await waitFor(() => + expect(mockFetch).toHaveBeenCalledWith( + expect.objectContaining({ url: expect.stringContaining("/auto_router/benchmarks?") }), + ), + ); + const benchmarkUrl = mockFetch.mock.calls + .map(([input]) => requestUrl(input)) + .find((url) => url.pathname === "/auto_router/benchmarks"); + expect(benchmarkUrl?.searchParams.get("start_date")).toBe("2026-09-14"); + expect(benchmarkUrl?.searchParams.get("end_date")).toBe("2026-10-14"); + fireEvent.click(screen.getByRole("tab", { name: "Overall" })); + expect(screen.getByRole("button", { name: selectedRange })).toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/costOptimizationDateRange.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/costOptimizationDateRange.test.ts new file mode 100644 index 00000000000..a5bbe101d39 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/costOptimizationDateRange.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it } from "vitest"; + +import { costOptimizationDateRange } from "./costOptimizationDateRange"; + +describe("costOptimizationDateRange", () => { + it.each([ + ["2026-10-14", "2026-10-08", "12:34:56"], + ["2026-03-02", "2026-02-24", "12:34:56"], + ["2028-03-02", "2028-02-25", "12:34:56"], + ["2026-01-02", "2025-12-27", "12:34:56"], + ["2026-03-09", "2026-03-03", "00:30:00"], + ["2026-11-02", "2026-10-27", "23:30:00"], + ])("includes exactly seven local calendar days ending on %s", (today, firstDay, time) => { + const now = new Date(`${today}T${time}`); + + expect(costOptimizationDateRange(now)).toEqual({ + from: new Date(`${firstDay}T00:00:00`), + to: new Date(`${today}T23:59:59.999`), + }); + expect(now).toEqual(new Date(`${today}T${time}`)); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/costOptimizationDateRange.ts b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/costOptimizationDateRange.ts new file mode 100644 index 00000000000..55639a03af4 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/costOptimizationDateRange.ts @@ -0,0 +1,6 @@ +import { endOfDay, startOfDay, subDays } from "date-fns"; + +export const costOptimizationDateRange = (now: Date = new Date()): { from: Date; to: Date } => ({ + from: startOfDay(subDays(now, 6)), + to: endOfDay(now), +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.integration.test.tsx index 51dfe9fda90..abd0bb1f28d 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.integration.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.integration.test.tsx @@ -1,7 +1,7 @@ -import { renderHook, waitFor } from "@testing-library/react"; +import { act, renderHook, waitFor } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; -import { useScopedDailyActivityRange } from "./useDailyActivityRange"; +import { useDailyActivityRange, useScopedDailyActivityRange } from "./useDailyActivityRange"; // The sibling unit test mocks networking, so it pins the positional args array against itself and // would still pass if the array and the two networking signatures drifted apart. Nothing else runs @@ -13,6 +13,38 @@ describe("useScopedDailyActivityRange wiring", () => { afterEach(() => { global.fetch = originalFetch; + vi.useRealTimers(); + }); + + it("requests seven days including today on mount and preserves a wider selected range", async () => { + vi.useFakeTimers({ toFake: ["Date"] }); + vi.setSystemTime(new Date("2026-03-09T12:00:00")); + const mockFetch = vi.fn().mockImplementation( + async () => + new Response(JSON.stringify({ results: [], metadata: {} }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }), + ); + global.fetch = mockFetch; + const { result } = renderHook(() => useDailyActivityRange("sk-token", "u1", "proxy_admin")); + + await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(1)); + const initialUrl = new URL(String(mockFetch.mock.calls[0][0])); + expect(initialUrl.pathname).toBe("/user/daily/activity/aggregated"); + expect(initialUrl.searchParams.get("start_date")).toBe("2026-03-03"); + expect(initialUrl.searchParams.get("end_date")).toBe("2026-03-09"); + expect(initialUrl.searchParams.get("include_current_utc_day")).toBe("true"); + expect(initialUrl.searchParams.get("timezone")).toBe(String(new Date().getTimezoneOffset())); + + const selectedRange = { from: new Date(2026, 0, 1), to: new Date(2026, 2, 9, 23, 59, 59, 999) }; + act(() => result.current.onDateChange(selectedRange)); + + await waitFor(() => expect(mockFetch).toHaveBeenCalledTimes(2)); + const selectedUrl = new URL(String(mockFetch.mock.calls[1][0])); + expect(selectedUrl.searchParams.get("start_date")).toBe("2026-01-01"); + expect(selectedUrl.searchParams.get("end_date")).toBe("2026-03-09"); + expect(result.current.dateValue).toEqual(selectedRange); }); it("lands each scope field on its own query param through the real networking callers", async () => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.test.tsx index 90d6694ccba..f31df435373 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.test.tsx @@ -37,6 +37,9 @@ describe("useDailyActivityRange", () => { expect(result.current.dateValue.from).toBeInstanceOf(Date); expect(result.current.dateValue.to).toBeInstanceOf(Date); + expect(result.current.dateValue.to!.getTime() - result.current.dateValue.from!.getTime()).toBe( + 30 * 24 * 60 * 60 * 1000, + ); expect(mockUseAggregatedDailyActivity).not.toHaveBeenCalled(); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.ts b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.ts index 1af009397b1..dd25cca6624 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/useDailyActivityRange.ts @@ -10,6 +10,7 @@ import { import { DailyData } from "@/components/UsagePage/types"; import { spendScopeUserId } from "@/utils/roles"; import { useAggregatedDailyActivity } from "@/app/(dashboard)/usage/_components/hooks/useAggregatedDailyActivity"; +import { costOptimizationDateRange } from "./costOptimizationDateRange"; const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000; @@ -38,10 +39,14 @@ export interface DailyActivityRange { export type ActivityDateRange = Pick; -export const useActivityDateRange = (): ActivityDateRange => { - const initialFrom = useMemo(() => new Date(new Date().getTime() - THIRTY_DAYS_MS), []); - const initialTo = useMemo(() => new Date(), []); - const [dateValue, setDateValue] = useState({ from: initialFrom, to: initialTo }); +export const useActivityDateRange = (initialRange?: () => DateRange): ActivityDateRange => { + const [dateValue, setDateValue] = useState( + initialRange ?? + (() => { + const now = new Date(); + return { from: new Date(now.getTime() - THIRTY_DAYS_MS), to: now }; + }), + ); return { dateValue, onDateChange: setDateValue }; }; @@ -96,6 +101,6 @@ export const useDailyActivityRange = ( userId: string | null, userRole: string, ): DailyActivityRange => { - const dateRange = useActivityDateRange(); + const dateRange = useActivityDateRange(costOptimizationDateRange); return useScopedDailyActivityRange(accessToken, { userId: spendScopeUserId(userRole, userId) }, dateRange); };