From 8b633a2efc29bc5a14ac40db8caee053aea70d40 Mon Sep 17 00:00:00 2001 From: Ryan Crabbe Date: Mon, 20 Apr 2026 11:58:23 -0700 Subject: [PATCH] cover new useLogFilterLogic invariants: activeTab gate, filterByCurrentUser fallback, debounce negative, partial merge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follow-up to the test refactor. Adds coverage for invariants the refactored hook contract introduced but that the first pass didn't assert: - query enablement: expand the single accessToken-null case into an it.each over all four credential props (accessToken, token, userRole, userID), plus a separate test for activeTab !== "request logs" - filterByCurrentUser: when true with a blank User ID filter, the outbound request carries user_id = userID - debounce: also assert the negative case — no call in the first 100ms after a filter change (first waiting out the initial mount fire) - handleFilterChange: partial updates merge without clobbering other filter keys (protects the spread + default-fill semantics) - handleFilterReset: calls setCurrentPage(1) alongside restoring filters --- .../view_logs/log_filter_logic.test.tsx | 95 ++++++++++++++++++- 1 file changed, 93 insertions(+), 2 deletions(-) diff --git a/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.test.tsx b/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.test.tsx index 919c815b2df..450ad2345ed 100644 --- a/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.test.tsx +++ b/ui/litellm-dashboard/src/components/view_logs/log_filter_logic.test.tsx @@ -109,6 +109,17 @@ describe("useLogFilterLogic", () => { expect(result.current.filters["Status"]).toBe(""); }); + it("calls setCurrentPage(1)", () => { + const setCurrentPage = vi.fn(); + const { result } = renderFilterHook({ setCurrentPage }); + + act(() => { + result.current.handleFilterReset(); + }); + + expect(setCurrentPage).toHaveBeenCalledWith(1); + }); + it("triggers a fetch with all filter params undefined", async () => { vi.mocked(uiSpendLogsCall).mockResolvedValue(emptyResponse); const { result } = renderFilterHook(); @@ -156,6 +167,22 @@ describe("useLogFilterLogic", () => { expect(setCurrentPage).toHaveBeenCalledWith(1); }); + it("merges partial updates without clobbering other filter keys", () => { + const { result } = renderFilterHook(); + + act(() => { + result.current.handleFilterChange({ "Team ID": "team-a" }); + }); + expect(result.current.filters["Team ID"]).toBe("team-a"); + + act(() => { + result.current.handleFilterChange({ Model: "gpt-4" }); + }); + + expect(result.current.filters["Team ID"]).toBe("team-a"); + expect(result.current.filters["Model"]).toBe("gpt-4"); + }); + it("does not call setCurrentPage when filters are identical", async () => { const setCurrentPage = vi.fn(); const { result } = renderFilterHook({ setCurrentPage }); @@ -259,6 +286,24 @@ describe("useLogFilterLogic", () => { await waitFor(() => expect(uiSpendLogsCall).toHaveBeenCalled(), { timeout: 500 }); }); + + it("does not call uiSpendLogsCall before the debounce elapses", async () => { + const { result } = renderFilterHook(); + + // Wait for the initial query fire, then reset the spy so we only observe + // calls triggered by the filter change below. + await waitFor(() => expect(uiSpendLogsCall).toHaveBeenCalled(), { timeout: 500 }); + vi.mocked(uiSpendLogsCall).mockClear(); + + act(() => { + result.current.handleFilterChange({ "Key Alias": "alias-1" }); + }); + + await new Promise((resolve) => setTimeout(resolve, 100)); + expect(uiSpendLogsCall).not.toHaveBeenCalled(); + + await waitFor(() => expect(uiSpendLogsCall).toHaveBeenCalled(), { timeout: 500 }); + }); }); describe("backend filtered logs", () => { @@ -455,8 +500,30 @@ describe("useLogFilterLogic", () => { }); describe("query enablement", () => { - it("does not call uiSpendLogsCall when accessToken is null", async () => { - const { result } = renderFilterHook({ accessToken: null }); + const nullCredentialCases: Array<{ name: string; override: HookOverrides }> = [ + { name: "accessToken", override: { accessToken: null } }, + { name: "token", override: { token: null } }, + { name: "userRole", override: { userRole: null } }, + { name: "userID", override: { userID: null } }, + ]; + + it.each(nullCredentialCases)( + "does not call uiSpendLogsCall when $name is null", + async ({ override }) => { + const { result } = renderFilterHook(override); + + act(() => { + result.current.handleFilterChange({ "Key Alias": "alias-1" }); + }); + + await new Promise((resolve) => setTimeout(resolve, 350)); + + expect(uiSpendLogsCall).not.toHaveBeenCalled(); + }, + ); + + it("does not call uiSpendLogsCall when activeTab is not 'request logs'", async () => { + const { result } = renderFilterHook({ activeTab: "audit logs" }); act(() => { result.current.handleFilterChange({ "Key Alias": "alias-1" }); @@ -468,6 +535,30 @@ describe("useLogFilterLogic", () => { }); }); + describe("filterByCurrentUser", () => { + it("sends user_id: userID when the User ID filter is blank", async () => { + const { result } = renderFilterHook({ + filterByCurrentUser: true, + userID: "me-123", + }); + + act(() => { + result.current.handleFilterChange({ "Key Alias": "alias-1" }); + }); + + await waitFor( + () => { + expect(uiSpendLogsCall).toHaveBeenCalledWith( + expect.objectContaining({ + params: expect.objectContaining({ user_id: "me-123" }), + }), + ); + }, + { timeout: 500 }, + ); + }); + }); + describe("error handling", () => { it("does not crash when uiSpendLogsCall throws", async () => { vi.mocked(uiSpendLogsCall).mockRejectedValue(new Error("Network error"));