diff --git a/frontend/__tests__/components/features/conversation-panel/system-message-modal/tool-item.test.tsx b/frontend/__tests__/components/features/conversation-panel/system-message-modal/tool-item.test.tsx new file mode 100644 index 0000000000..15fcef2b5f --- /dev/null +++ b/frontend/__tests__/components/features/conversation-panel/system-message-modal/tool-item.test.tsx @@ -0,0 +1,551 @@ +import { screen } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, describe, expect, it, vi } from "vitest"; +import { renderWithProviders } from "test-utils"; +import { ToolItem } from "#/components/features/conversation-panel/system-message-modal/tool-item"; +import type { ChatCompletionToolParam } from "#/types/v1/core"; + +describe("ToolItem", () => { + const user = userEvent.setup(); + const onToggleMock = vi.fn(); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe("Name/Title Extraction", () => { + it("should display name from V0 format function.name", () => { + // Arrange + const v0Tool: ChatCompletionToolParam = { + type: "function", + function: { + name: "test_function", + description: "Test description", + parameters: {}, + }, + }; + + // Act + renderWithProviders( + , + ); + + // Assert + const toggleButton = screen.getByTestId("toggle-button"); + expect(toggleButton).toHaveTextContent("test_function"); + }); + + it("should display title from V1 format root level", () => { + // Arrange + const v1Tool = { + title: "V1 Tool Title", + description: "V1 description", + parameters: { type: "object" }, + }; + + // Act + renderWithProviders( + , + ); + + // Assert + const toggleButton = screen.getByTestId("toggle-button"); + expect(toggleButton).toHaveTextContent("V1 Tool Title"); + }); + + it("should prioritize root title over annotations.title in V1 format", () => { + // Arrange + const v1Tool = { + title: "Root Title", + annotations: { + title: "Annotations Title", + }, + description: "Description", + }; + + // Act + renderWithProviders( + , + ); + + // Assert + const toggleButton = screen.getByTestId("toggle-button"); + expect(toggleButton).toHaveTextContent("Root Title"); + }); + + it("should fallback to annotations.title when root title is missing", () => { + // Arrange + const v1Tool = { + annotations: { + title: "Annotations Title", + }, + description: "Description", + }; + + // Act + renderWithProviders( + , + ); + + // Assert + const toggleButton = screen.getByTestId("toggle-button"); + expect(toggleButton).toHaveTextContent("Annotations Title"); + }); + + it("should display empty string when no name or title is available", () => { + // Arrange + const toolWithoutName = { + description: "Description only", + }; + + // Act + renderWithProviders( + , + ); + + // Assert + const toggleButton = screen.getByTestId("toggle-button"); + expect(toggleButton).toHaveTextContent(""); + }); + }); + + describe("Description Extraction", () => { + it("should display description from V0 format function.description when expanded", () => { + // Arrange + const v0Tool: ChatCompletionToolParam = { + type: "function", + function: { + name: "test_function", + description: "V0 function description", + parameters: {}, + }, + }; + + // Act + renderWithProviders( + , + ); + + // Assert + const markdownRenderer = screen.getByTestId("markdown-renderer"); + expect(markdownRenderer).toHaveTextContent("V0 function description"); + }); + + it("should display description from V1 format root level when expanded", () => { + // Arrange + const v1Tool = { + title: "V1 Tool", + description: "V1 root description", + parameters: {}, + }; + + // Act + renderWithProviders( + , + ); + + // Assert + const markdownRenderer = screen.getByTestId("markdown-renderer"); + expect(markdownRenderer).toHaveTextContent("V1 root description"); + }); + + it("should prioritize root description over function.description in V1 format", () => { + // Arrange + const v1Tool = { + title: "V1 Tool", + description: "Root description", + function: { + description: "Function description", + }, + }; + + // Act + renderWithProviders( + , + ); + + // Assert + const markdownRenderer = screen.getByTestId("markdown-renderer"); + expect(markdownRenderer).toHaveTextContent("Root description"); + }); + + it("should display empty string when no description is available", () => { + // Arrange + const toolWithoutDescription = { + title: "Tool Name", + }; + + // Act + renderWithProviders( + , + ); + + // Assert + const markdownRenderer = screen.getByTestId("markdown-renderer"); + expect(markdownRenderer).toHaveTextContent(""); + }); + + it("should not display description when collapsed", () => { + // Arrange + const v0Tool: ChatCompletionToolParam = { + type: "function", + function: { + name: "test_function", + description: "Should not be visible", + parameters: {}, + }, + }; + + // Act + renderWithProviders( + , + ); + + // Assert + expect(screen.queryByTestId("markdown-renderer")).not.toBeInTheDocument(); + }); + }); + + describe("Parameters Extraction", () => { + it("should display parameters from V0 format function.parameters when expanded", () => { + // Arrange + const v0Tool: ChatCompletionToolParam = { + type: "function", + function: { + name: "test_function", + description: "Description", + parameters: { + type: "object", + properties: { + param1: { type: "string" }, + }, + }, + }, + }; + + // Act + renderWithProviders( + , + ); + + // Assert + const toolParameters = screen.getByTestId("tool-parameters"); + expect(toolParameters).toBeInTheDocument(); + // Verify that the parameters are rendered (ReactJsonView will render the JSON) + expect(toolParameters).toHaveTextContent("param1"); + }); + + it("should display parameters from V1 format root level when expanded", () => { + // Arrange + const v1Tool = { + title: "V1 Tool", + description: "Description", + parameters: { + type: "object", + properties: { + param2: { type: "number" }, + }, + }, + }; + + // Act + renderWithProviders( + , + ); + + // Assert + const toolParameters = screen.getByTestId("tool-parameters"); + expect(toolParameters).toBeInTheDocument(); + // Verify that the parameters are rendered (ReactJsonView will render the JSON) + expect(toolParameters).toHaveTextContent("param2"); + }); + + it("should prioritize function.parameters over root parameters in V0 format", () => { + // Arrange + const v0Tool = { + type: "function", + function: { + name: "test_function", + description: "Description", + parameters: { + type: "object", + source: "function", + }, + }, + parameters: { + type: "object", + source: "root", + }, + }; + + // Act + renderWithProviders( + , + ); + + // Assert + const toolParameters = screen.getByTestId("tool-parameters"); + expect(toolParameters).toBeInTheDocument(); + // Verify that function parameters are used (not root parameters) + expect(toolParameters).toHaveTextContent("function"); + expect(toolParameters).not.toHaveTextContent("root"); + }); + + it("should not display parameters when they are null", () => { + // Arrange + const toolWithoutParameters = { + title: "Tool Name", + description: "Description", + }; + + // Act + renderWithProviders( + , + ); + + // Assert + expect(screen.queryByTestId("tool-parameters")).not.toBeInTheDocument(); + }); + + it("should not display parameters when collapsed", () => { + // Arrange + const v0Tool: ChatCompletionToolParam = { + type: "function", + function: { + name: "test_function", + description: "Description", + parameters: { + type: "object", + }, + }, + }; + + // Act + renderWithProviders( + , + ); + + // Assert + expect(screen.queryByTestId("tool-parameters")).not.toBeInTheDocument(); + }); + }); + + describe("Toggle Functionality", () => { + it("should call onToggle with correct index when toggle button is clicked", async () => { + // Arrange + const v0Tool: ChatCompletionToolParam = { + type: "function", + function: { + name: "test_function", + description: "Description", + parameters: {}, + }, + }; + + // Act + renderWithProviders( + , + ); + + const toggleButton = screen.getByTestId("toggle-button"); + await user.click(toggleButton); + + // Assert + expect(onToggleMock).toHaveBeenCalledOnce(); + expect(onToggleMock).toHaveBeenCalledWith(2); + }); + + it("should show expanded content when isExpanded is true", () => { + // Arrange + const v0Tool: ChatCompletionToolParam = { + type: "function", + function: { + name: "test_function", + description: "Test description", + parameters: { type: "object" }, + }, + }; + + // Act + renderWithProviders( + , + ); + + // Assert + expect(screen.getByTestId("markdown-renderer")).toBeInTheDocument(); + expect(screen.getByTestId("tool-parameters")).toBeInTheDocument(); + }); + + it("should hide expanded content when isExpanded is false", () => { + // Arrange + const v0Tool: ChatCompletionToolParam = { + type: "function", + function: { + name: "test_function", + description: "Test description", + parameters: { type: "object" }, + }, + }; + + // Act + renderWithProviders( + , + ); + + // Assert + expect(screen.queryByTestId("markdown-renderer")).not.toBeInTheDocument(); + expect(screen.queryByTestId("tool-parameters")).not.toBeInTheDocument(); + }); + }); + + describe("Complex Scenarios", () => { + it("should handle V0 format with type field correctly", () => { + // Arrange + const v0Tool = { + type: "function", + function: { + name: "typed_function", + description: "Typed description", + parameters: { type: "object" }, + }, + }; + + // Act + renderWithProviders( + , + ); + + // Assert + expect(screen.getByTestId("toggle-button")).toHaveTextContent( + "typed_function", + ); + expect(screen.getByTestId("markdown-renderer")).toHaveTextContent( + "Typed description", + ); + expect(screen.getByTestId("tool-parameters")).toBeInTheDocument(); + }); + + it("should handle tool data where function is at root level (fallback behavior)", () => { + // Arrange + const toolWithFunctionAtRoot = { + name: "root_function", + description: "Root function description", + parameters: { type: "object" }, + }; + + // Act + renderWithProviders( + , + ); + + // Assert + expect(screen.getByTestId("toggle-button")).toHaveTextContent( + "root_function", + ); + expect(screen.getByTestId("markdown-renderer")).toHaveTextContent( + "Root function description", + ); + expect(screen.getByTestId("tool-parameters")).toBeInTheDocument(); + }); + }); +}); diff --git a/frontend/src/components/features/conversation-panel/system-message-modal/system-message-header.tsx b/frontend/src/components/features/conversation-panel/system-message-modal/system-message-header.tsx index e1ed8fb401..f974f9c055 100644 --- a/frontend/src/components/features/conversation-panel/system-message-modal/system-message-header.tsx +++ b/frontend/src/components/features/conversation-panel/system-message-modal/system-message-header.tsx @@ -1,6 +1,7 @@ import { useTranslation } from "react-i18next"; import { BaseModalTitle } from "#/components/shared/modals/confirmation-modals/base-modal"; import { Typography } from "#/ui/typography"; +import { cn } from "#/utils/utils"; interface SystemMessageHeaderProps { agentClass: string | null; @@ -14,7 +15,12 @@ export function SystemMessageHeader({ const { t } = useTranslation(); return ( -
+
{agentClass && ( diff --git a/frontend/src/components/features/conversation-panel/system-message-modal/toggle-button.tsx b/frontend/src/components/features/conversation-panel/system-message-modal/toggle-button.tsx index cd2d1d972b..eff4ed4a4f 100644 --- a/frontend/src/components/features/conversation-panel/system-message-modal/toggle-button.tsx +++ b/frontend/src/components/features/conversation-panel/system-message-modal/toggle-button.tsx @@ -17,6 +17,7 @@ export function ToggleButton({ return (