diff --git a/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/SectionHeader.test.tsx b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/SectionHeader.test.tsx
new file mode 100644
index 00000000000..5aee6b33ec5
--- /dev/null
+++ b/ui/litellm-dashboard/src/components/view_logs/LogDetailsDrawer/SectionHeader.test.tsx
@@ -0,0 +1,64 @@
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import { describe, expect, it, vi } from "vitest";
+import { SectionHeader } from "./SectionHeader";
+
+describe("SectionHeader", () => {
+ it("renders the input label with token, cost and turn metrics", () => {
+ render();
+
+ expect(screen.getByText("Input")).toBeInTheDocument();
+ expect(screen.getByText("Tokens: 1,234")).toBeInTheDocument();
+ expect(screen.getByText("Cost: $0.000123")).toBeInTheDocument();
+ expect(screen.getByText("Turns: 3")).toBeInTheDocument();
+ });
+
+ it("renders the output label", () => {
+ render();
+
+ expect(screen.getByText("Output")).toBeInTheDocument();
+ });
+
+ it("omits metrics that were not provided", () => {
+ render();
+
+ expect(screen.queryByText(/^Tokens:/)).not.toBeInTheDocument();
+ expect(screen.queryByText(/^Cost:/)).not.toBeInTheDocument();
+ expect(screen.queryByText(/^Turns:/)).not.toBeInTheDocument();
+ });
+
+ it("omits the turn count when there are no turns", () => {
+ render();
+
+ expect(screen.queryByText(/^Turns:/)).not.toBeInTheDocument();
+ });
+
+ it("copies without toggling the section", async () => {
+ const onCopy = vi.fn();
+ const onToggleCollapse = vi.fn();
+ render();
+
+ await userEvent.click(screen.getByRole("button", { name: /copy/i }));
+
+ expect(onCopy).toHaveBeenCalledTimes(1);
+ expect(onToggleCollapse).not.toHaveBeenCalled();
+ });
+
+ it("toggles the section when the header is clicked", async () => {
+ const onToggleCollapse = vi.fn();
+ render();
+
+ await userEvent.click(screen.getByText("Input"));
+
+ expect(onToggleCollapse).toHaveBeenCalledTimes(1);
+ });
+
+ it("stays inert when no toggle handler is given", async () => {
+ const onCopy = vi.fn();
+ render();
+
+ await userEvent.click(screen.getByText("Input"));
+
+ expect(onCopy).not.toHaveBeenCalled();
+ });
+});
diff --git a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.test.tsx b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.test.tsx
index 753a552b6db..2e9d83d9c6d 100644
--- a/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.test.tsx
+++ b/ui/litellm-dashboard/src/components/view_logs/ToolsSection/ToolsSection.test.tsx
@@ -2,10 +2,46 @@
* Core tests for Tools section
*/
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
import { describe, it, expect } from "vitest";
import { parseToolsFromLog } from "./utils";
+import { ToolsSection } from "./ToolsSection";
import { LogEntry } from "../columns";
+const logWithTools = (toolNames: string[], calledName?: string): LogEntry => ({
+ request_id: "render-1",
+ api_key: "key",
+ team_id: "team",
+ model: "gpt-4",
+ model_id: "gpt-4",
+ call_type: "completion",
+ spend: 0.01,
+ total_tokens: 100,
+ prompt_tokens: 50,
+ completion_tokens: 50,
+ startTime: "2024-01-01T00:00:00Z",
+ endTime: "2024-01-01T00:00:01Z",
+ cache_hit: "none",
+ messages: JSON.stringify({
+ model: "gpt-4",
+ messages: [{ role: "user", content: "hi" }],
+ tools: toolNames.map((name) => ({
+ type: "function",
+ function: { name, description: `${name} description`, parameters: { type: "object", properties: {} } },
+ })),
+ }),
+ response: JSON.stringify({
+ choices: [
+ {
+ message: calledName
+ ? { tool_calls: [{ id: "call_1", type: "function", function: { name: calledName, arguments: "{}" } }] }
+ : { content: "done" },
+ },
+ ],
+ }),
+});
+
describe("ToolsSection", () => {
it("should parse tools from request and match with response tool calls", () => {
const mockLog: LogEntry = {
@@ -115,3 +151,36 @@ describe("ToolsSection", () => {
expect(tools).toHaveLength(0);
});
});
+
+describe("ToolsSection rendering", () => {
+ it("summarises how many tools were provided and called", () => {
+ render();
+
+ expect(screen.getByText("Tools")).toBeInTheDocument();
+ expect(screen.getByText("2 provided, 1 called")).toBeInTheDocument();
+ });
+
+ it("previews the first two tool names", () => {
+ render();
+
+ expect(screen.getByText(/alpha, beta/)).toBeInTheDocument();
+ });
+
+ it("renders nothing when the log has no tools", () => {
+ const { container } = render();
+
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it("reveals the tool list only after the section is expanded", async () => {
+ render();
+
+ expect(screen.queryByText("called")).not.toBeInTheDocument();
+ expect(screen.queryByText("not called")).not.toBeInTheDocument();
+
+ await userEvent.click(screen.getByText("Tools"));
+
+ expect(await screen.findByText("called")).toBeInTheDocument();
+ expect(screen.getByText("not called")).toBeInTheDocument();
+ });
+});