Merge pull request #18601 from BerriAI/litellm_ui_test_coverage_xd

[Infra] UI - Adding Unit Tests to Increase Coverage
This commit is contained in:
yuneng-jiang 2026-01-02 18:56:45 -08:00 • committed by GitHub
commit dacfe153ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 1403 additions and 0 deletions

View file

@ -0,0 +1,185 @@
import { getUiSettings } from "@/components/networking";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { renderHook, waitFor } from "@testing-library/react";
import React, { ReactNode } from "react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { useUISettings } from "./useUISettings";
// Mock the networking function
vi.mock("@/components/networking", () => ({
getUiSettings: vi.fn(),
}));
// Mock useAuthorized hook - we can override this in individual tests
const mockUseAuthorized = vi.fn();
vi.mock("../useAuthorized", () => ({
default: () => mockUseAuthorized(),
}));
// Mock data
const mockUISettings: Record<string, any> = {
theme: "dark",
language: "en",
notifications: true,
dashboard_layout: "compact",
api_keys_visible: false,
};
describe("useUISettings", () => {
let queryClient: QueryClient;
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});
// Reset all mocks
vi.clearAllMocks();
// Set default mock for useAuthorized (enabled state)
mockUseAuthorized.mockReturnValue({
accessToken: "test-access-token",
userRole: "Admin",
userId: "test-user-id",
token: "test-token",
userEmail: "test@example.com",
premiumUser: false,
disabledPersonalKeyCreation: null,
showSSOBanner: false,
});
});
const wrapper = ({ children }: { children: ReactNode }) =>
React.createElement(QueryClientProvider, { client: queryClient }, children);
it("should return UI settings data when query is successful", async () => {
// Mock successful API call
(getUiSettings as any).mockResolvedValue(mockUISettings);
const { result } = renderHook(() => useUISettings(), { wrapper });
// Initially loading
expect(result.current.isLoading).toBe(true);
expect(result.current.data).toBeUndefined();
// Wait for success
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toEqual(mockUISettings);
expect(result.current.error).toBeNull();
expect(getUiSettings).toHaveBeenCalledWith("test-access-token");
expect(getUiSettings).toHaveBeenCalledTimes(1);
});
it("should handle error when getUiSettings fails", async () => {
const errorMessage = "Failed to fetch UI settings";
const testError = new Error(errorMessage);
// Mock failed API call
(getUiSettings as any).mockRejectedValue(testError);
const { result } = renderHook(() => useUISettings(), { wrapper });
// Initially loading
expect(result.current.isLoading).toBe(true);
// Wait for error
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
expect(result.current.isError).toBe(true);
});
expect(result.current.error).toEqual(testError);
expect(result.current.data).toBeUndefined();
expect(getUiSettings).toHaveBeenCalledWith("test-access-token");
expect(getUiSettings).toHaveBeenCalledTimes(1);
});
it("should not execute query when accessToken is missing", async () => {
// Mock missing accessToken
mockUseAuthorized.mockReturnValue({
accessToken: null,
userRole: "Admin",
userId: "test-user-id",
token: null,
userEmail: "test@example.com",
premiumUser: false,
disabledPersonalKeyCreation: null,
showSSOBanner: false,
});
const { result } = renderHook(() => useUISettings(), { wrapper });
// Query should not execute
expect(result.current.isLoading).toBe(false);
expect(result.current.data).toBeUndefined();
expect(result.current.isFetched).toBe(false);
// API should not be called
expect(getUiSettings).not.toHaveBeenCalled();
});
it("should not execute query when accessToken is empty string", async () => {
// Mock empty accessToken
mockUseAuthorized.mockReturnValue({
accessToken: "",
userRole: "Admin",
userId: "test-user-id",
token: "",
userEmail: "test@example.com",
premiumUser: false,
disabledPersonalKeyCreation: null,
showSSOBanner: false,
});
const { result } = renderHook(() => useUISettings(), { wrapper });
// Query should not execute
expect(result.current.isLoading).toBe(false);
expect(result.current.data).toBeUndefined();
expect(result.current.isFetched).toBe(false);
// API should not be called
expect(getUiSettings).not.toHaveBeenCalled();
});
it("should return empty object when API returns empty settings", async () => {
// Mock API returning empty object
(getUiSettings as any).mockResolvedValue({});
const { result } = renderHook(() => useUISettings(), { wrapper });
// Wait for success
await waitFor(() => {
expect(result.current.isLoading).toBe(false);
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toEqual({});
expect(getUiSettings).toHaveBeenCalledWith("test-access-token");
});
it("should handle network timeout error", async () => {
const timeoutError = new Error("Network timeout");
// Mock network timeout
(getUiSettings as any).mockRejectedValue(timeoutError);
const { result } = renderHook(() => useUISettings(), { wrapper });
// Wait for error
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(result.current.error).toEqual(timeoutError);
expect(result.current.data).toBeUndefined();
});
});

View file

@ -0,0 +1,219 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { ModelGroup } from "../llm_calls/fetch_models";
import { determineEndpointType } from "./EndpointUtils";
import { EndpointType } from "./mode_endpoint_mapping";
// Mock the getEndpointType function
vi.mock("./mode_endpoint_mapping", () => ({
EndpointType: {
IMAGE: "image",
VIDEO: "video",
CHAT: "chat",
RESPONSES: "responses",
IMAGE_EDITS: "image_edits",
ANTHROPIC_MESSAGES: "anthropic_messages",
EMBEDDINGS: "embeddings",
SPEECH: "speech",
TRANSCRIPTION: "transcription",
A2A_AGENTS: "a2a_agents",
},
getEndpointType: vi.fn(),
ModelMode: {
AUDIO_SPEECH: "audio_speech",
AUDIO_TRANSCRIPTION: "audio_transcription",
IMAGE_GENERATION: "image_generation",
VIDEO_GENERATION: "video_generation",
CHAT: "chat",
RESPONSES: "responses",
IMAGE_EDITS: "image_edits",
ANTHROPIC_MESSAGES: "anthropic_messages",
EMBEDDING: "embedding",
},
}));
// Import the mocked function
import { getEndpointType } from "./mode_endpoint_mapping";
describe("determineEndpointType", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should return the correct endpoint type when model is found and has a valid mode", () => {
const mockModelInfo: ModelGroup[] = [
{
model_group: "gpt-3.5-turbo",
mode: "chat",
},
{
model_group: "dall-e-3",
mode: "image_generation",
},
];
// Mock getEndpointType to return IMAGE for image_generation mode
vi.mocked(getEndpointType).mockReturnValue(EndpointType.IMAGE);
const result = determineEndpointType("dall-e-3", mockModelInfo);
expect(getEndpointType).toHaveBeenCalledWith("image_generation");
expect(result).toBe(EndpointType.IMAGE);
});
it("should return CHAT endpoint type when model is found but has no mode", () => {
const mockModelInfo: ModelGroup[] = [
{
model_group: "gpt-3.5-turbo",
// No mode property
},
];
const result = determineEndpointType("gpt-3.5-turbo", mockModelInfo);
expect(getEndpointType).not.toHaveBeenCalled();
expect(result).toBe(EndpointType.CHAT);
});
it("should return CHAT endpoint type when model is not found in modelInfo", () => {
const mockModelInfo: ModelGroup[] = [
{
model_group: "gpt-3.5-turbo",
mode: "chat",
},
];
const result = determineEndpointType("non-existent-model", mockModelInfo);
expect(getEndpointType).not.toHaveBeenCalled();
expect(result).toBe(EndpointType.CHAT);
});
it("should return CHAT endpoint type when modelInfo array is empty", () => {
const mockModelInfo: ModelGroup[] = [];
const result = determineEndpointType("any-model", mockModelInfo);
expect(getEndpointType).not.toHaveBeenCalled();
expect(result).toBe(EndpointType.CHAT);
});
it("should handle different mode types correctly", () => {
const mockModelInfo: ModelGroup[] = [
{
model_group: "tts-model",
mode: "audio_speech",
},
{
model_group: "whisper-model",
mode: "audio_transcription",
},
{
model_group: "embedding-model",
mode: "embedding",
},
{
model_group: "video-model",
mode: "video_generation",
},
];
// Test speech mode
vi.mocked(getEndpointType).mockReturnValueOnce(EndpointType.SPEECH);
const speechResult = determineEndpointType("tts-model", mockModelInfo);
expect(getEndpointType).toHaveBeenCalledWith("audio_speech");
expect(speechResult).toBe(EndpointType.SPEECH);
// Reset mock for next test
vi.clearAllMocks();
// Test transcription mode
vi.mocked(getEndpointType).mockReturnValueOnce(EndpointType.TRANSCRIPTION);
const transcriptionResult = determineEndpointType("whisper-model", mockModelInfo);
expect(getEndpointType).toHaveBeenCalledWith("audio_transcription");
expect(transcriptionResult).toBe(EndpointType.TRANSCRIPTION);
// Reset mock for next test
vi.clearAllMocks();
// Test embedding mode
vi.mocked(getEndpointType).mockReturnValueOnce(EndpointType.EMBEDDINGS);
const embeddingResult = determineEndpointType("embedding-model", mockModelInfo);
expect(getEndpointType).toHaveBeenCalledWith("embedding");
expect(embeddingResult).toBe(EndpointType.EMBEDDINGS);
// Reset mock for next test
vi.clearAllMocks();
// Test video mode
vi.mocked(getEndpointType).mockReturnValueOnce(EndpointType.VIDEO);
const videoResult = determineEndpointType("video-model", mockModelInfo);
expect(getEndpointType).toHaveBeenCalledWith("video_generation");
expect(videoResult).toBe(EndpointType.VIDEO);
});
it("should prioritize the first matching model when there are duplicates", () => {
const mockModelInfo: ModelGroup[] = [
{
model_group: "gpt-3.5-turbo",
mode: "chat",
},
{
model_group: "gpt-3.5-turbo",
mode: "image_generation", // Different mode for same model name
},
];
vi.mocked(getEndpointType).mockReturnValue(EndpointType.CHAT);
const result = determineEndpointType("gpt-3.5-turbo", mockModelInfo);
expect(getEndpointType).toHaveBeenCalledWith("chat");
expect(result).toBe(EndpointType.CHAT);
});
it("should handle models with undefined mode property explicitly set", () => {
const mockModelInfo: ModelGroup[] = [
{
model_group: "test-model",
mode: undefined,
},
];
const result = determineEndpointType("test-model", mockModelInfo);
expect(getEndpointType).not.toHaveBeenCalled();
expect(result).toBe(EndpointType.CHAT);
});
it("should handle models with empty string mode", () => {
const mockModelInfo: ModelGroup[] = [
{
model_group: "test-model",
mode: "",
},
];
const result = determineEndpointType("test-model", mockModelInfo);
// Empty string is falsy, so getEndpointType should not be called
expect(getEndpointType).not.toHaveBeenCalled();
expect(result).toBe(EndpointType.CHAT);
});
it("should handle case-sensitive model group matching", () => {
const mockModelInfo: ModelGroup[] = [
{
model_group: "GPT-3.5-TURBO",
mode: "chat",
},
];
vi.mocked(getEndpointType).mockReturnValue(EndpointType.CHAT);
// Test with different case - should not match
const result = determineEndpointType("gpt-3.5-turbo", mockModelInfo);
expect(getEndpointType).not.toHaveBeenCalled();
expect(result).toBe(EndpointType.CHAT);
});
});

View file

@ -0,0 +1,82 @@
import { act, fireEvent, render, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import ToolsCard from "./ToolsCard";
import { Tool } from "./types";
describe("ToolsCard", () => {
const mockTools: Tool[] = [
{
name: "Calculator",
description: "Performs mathematical calculations",
json: '{"type": "function", "function": {"name": "calculate"}}',
},
{
name: "Weather API",
description: "Gets current weather information",
json: '{"type": "function", "function": {"name": "get_weather"}}',
},
];
const defaultProps = {
tools: [] as Tool[],
onAddTool: vi.fn(),
onEditTool: vi.fn(),
onRemoveTool: vi.fn(),
};
it("should render the component", () => {
render(<ToolsCard {...defaultProps} />);
expect(screen.getByText("Tools")).toBeInTheDocument();
});
it("should display no tools message when tools array is empty", () => {
render(<ToolsCard {...defaultProps} />);
expect(screen.getByText("No tools added")).toBeInTheDocument();
});
it("should render tools when provided", () => {
render(<ToolsCard {...defaultProps} tools={mockTools} />);
expect(screen.getByText("Calculator")).toBeInTheDocument();
expect(screen.getByText("Performs mathematical calculations")).toBeInTheDocument();
expect(screen.getByText("Weather API")).toBeInTheDocument();
expect(screen.getByText("Gets current weather information")).toBeInTheDocument();
});
it("should call onAddTool when Add button is clicked", () => {
const mockOnAddTool = vi.fn();
render(<ToolsCard {...defaultProps} onAddTool={mockOnAddTool} />);
act(() => {
fireEvent.click(screen.getByRole("button", { name: /add/i }));
});
expect(mockOnAddTool).toHaveBeenCalledTimes(1);
});
it("should call onEditTool with correct index when Edit button is clicked", () => {
const mockOnEditTool = vi.fn();
render(<ToolsCard {...defaultProps} tools={mockTools} onEditTool={mockOnEditTool} />);
const editButtons = screen.getAllByText("Edit");
act(() => {
fireEvent.click(editButtons[0]);
});
expect(mockOnEditTool).toHaveBeenCalledWith(0);
expect(mockOnEditTool).toHaveBeenCalledTimes(1);
});
it("should call onRemoveTool with correct index when remove button is clicked", () => {
const mockOnRemoveTool = vi.fn();
render(<ToolsCard {...defaultProps} tools={mockTools} onRemoveTool={mockOnRemoveTool} />);
const removeButtons = screen.getAllByRole("button", { name: "" });
act(() => {
fireEvent.click(removeButtons[0]);
});
expect(mockOnRemoveTool).toHaveBeenCalledWith(0);
expect(mockOnRemoveTool).toHaveBeenCalledTimes(1);
});
});

View file

@ -0,0 +1,473 @@
import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from "vitest";
import { render, screen, fireEvent, act, waitFor } from "@testing-library/react";
import VersionHistorySidePanel from "./VersionHistorySidePanel";
import { getPromptVersions } from "../../networking";
import type { PromptSpec } from "../../networking";
// Mock the networking function
vi.mock("../../networking", () => ({
getPromptVersions: vi.fn(),
}));
const mockGetPromptVersions = getPromptVersions as Mock;
// Mock Ant Design components that might need special handling
vi.mock("antd", async () => {
const actual = await vi.importActual("antd");
return {
...actual,
Drawer: ({ children, title, onClose, open, width, placement, mask, maskClosable }: any) => (
<div
data-testid="drawer"
data-open={open}
data-title={title}
data-mask={String(mask)}
data-maskclosable={String(maskClosable)}
>
<div data-testid="drawer-header">{title}</div>
<button data-testid="drawer-close" onClick={onClose}>
×
</button>
<div data-testid="drawer-content">{children}</div>
</div>
),
List: ({ children, dataSource, renderItem }: any) => (
<div data-testid="list">{dataSource?.map((item: any, index: number) => renderItem(item, index))}</div>
),
Skeleton: ({ active }: any) => (
<div data-testid="skeleton" data-active={active}>
Loading...
</div>
),
Tag: ({ children, color, className }: any) => (
<span data-testid="tag" data-color={color} className={className}>
{children}
</span>
),
Typography: {
Text: ({ children, type, className }: any) => (
<span data-testid="text" data-type={type} className={className}>
{children}
</span>
),
},
};
});
describe("VersionHistorySidePanel", () => {
// Mock data
const mockPromptVersions: PromptSpec[] = [
{
prompt_id: "test-prompt.v2",
litellm_params: { prompt_id: "test-prompt.v2" },
prompt_info: { prompt_type: "db" },
version: 2,
created_at: "2024-01-15T10:30:00Z",
},
{
prompt_id: "test-prompt.v1",
litellm_params: { prompt_id: "test-prompt.v1" },
prompt_info: { prompt_type: "db" },
version: 1,
created_at: "2024-01-10T09:00:00Z",
},
{
prompt_id: "test-prompt.v3",
litellm_params: { prompt_id: "test-prompt.v3" },
prompt_info: { prompt_type: "config" },
version: 3,
created_at: "2024-01-20T14:15:00Z",
},
];
const mockPromptVersionsWithoutExplicitVersion = [
{
prompt_id: "test-prompt.v2",
litellm_params: { prompt_id: "test-prompt.v2" },
prompt_info: { prompt_type: "db" },
created_at: "2024-01-15T10:30:00Z",
},
{
prompt_id: "test-prompt.v1",
litellm_params: { prompt_id: "test-prompt.v1" },
prompt_info: { prompt_type: "db" },
created_at: "2024-01-10T09:00:00Z",
},
];
const defaultProps = {
isOpen: true,
onClose: vi.fn(),
accessToken: "test-token",
promptId: "test-prompt.v2",
activeVersionId: "test-prompt.v2",
onSelectVersion: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
// Mock successful response by default
mockGetPromptVersions.mockResolvedValue({
prompts: mockPromptVersions,
});
});
afterEach(() => {
vi.clearAllTimers();
});
describe("Component Rendering", () => {
it("should render the component with drawer", async () => {
await act(async () => {
render(<VersionHistorySidePanel {...defaultProps} />);
});
expect(screen.getByTestId("drawer")).toBeInTheDocument();
expect(screen.getByText("Version History")).toBeInTheDocument();
});
it("should not render when isOpen is false", async () => {
await act(async () => {
render(<VersionHistorySidePanel {...defaultProps} isOpen={false} />);
});
// The drawer should still be rendered but with open=false
const drawer = screen.getByTestId("drawer");
expect(drawer).toHaveAttribute("data-open", "false");
});
it("should show loading skeleton initially", async () => {
// Mock a delayed response to show loading state
mockGetPromptVersions.mockImplementationOnce(
() => new Promise((resolve) => setTimeout(() => resolve({ prompts: mockPromptVersions }), 100)),
);
render(<VersionHistorySidePanel {...defaultProps} />);
expect(screen.getByTestId("skeleton")).toBeInTheDocument();
// Wait for loading to complete
await waitFor(() => {
expect(screen.queryByTestId("skeleton")).not.toBeInTheDocument();
});
});
it("should show empty state when no versions are available", async () => {
mockGetPromptVersions.mockResolvedValueOnce({ prompts: [] });
render(<VersionHistorySidePanel {...defaultProps} />);
await waitFor(() => {
expect(screen.getByText("No version history available.")).toBeInTheDocument();
});
});
it("should render version list when data is loaded", async () => {
render(<VersionHistorySidePanel {...defaultProps} />);
await waitFor(() => {
expect(screen.getByText("v2")).toBeInTheDocument();
expect(screen.getByText("v1")).toBeInTheDocument();
expect(screen.getByText("v3")).toBeInTheDocument();
});
// Check that Latest tag is shown for the first item
const latestTags = screen.getAllByText("Latest");
expect(latestTags.length).toBeGreaterThan(0);
// Check Active tag is shown for the active version
expect(screen.getByText("Active")).toBeInTheDocument();
});
});
describe("Version Selection and Highlighting", () => {
it("should highlight the active version correctly", async () => {
render(<VersionHistorySidePanel {...defaultProps} />);
await waitFor(() => {
const versionItems = screen.getAllByTestId("tag");
// Should have Active tag for the selected version
expect(screen.getByText("Active")).toBeInTheDocument();
});
});
it("should highlight the latest version when no activeVersionId is provided", async () => {
render(<VersionHistorySidePanel {...defaultProps} activeVersionId={undefined} />);
await waitFor(() => {
const latestTags = screen.getAllByText("Latest");
expect(latestTags.length).toBeGreaterThan(0);
});
});
it("should call onSelectVersion when a version is clicked", async () => {
const mockOnSelectVersion = vi.fn();
render(<VersionHistorySidePanel {...defaultProps} onSelectVersion={mockOnSelectVersion} />);
await waitFor(() => {
expect(screen.getByText("v1")).toBeInTheDocument();
});
const versionItem = screen.getByText("v1").closest("div");
expect(versionItem).toBeInTheDocument();
act(() => {
fireEvent.click(versionItem!);
});
expect(mockOnSelectVersion).toHaveBeenCalledWith(mockPromptVersions[1]);
});
});
describe("Version Number Extraction", () => {
it("should extract version from explicit version field", async () => {
render(<VersionHistorySidePanel {...defaultProps} />);
await waitFor(() => {
expect(screen.getByText("v2")).toBeInTheDocument();
expect(screen.getByText("v3")).toBeInTheDocument();
});
});
it("should extract version from prompt_id with .v suffix", async () => {
mockGetPromptVersions.mockResolvedValueOnce({
prompts: mockPromptVersionsWithoutExplicitVersion,
});
render(<VersionHistorySidePanel {...defaultProps} />);
await waitFor(() => {
expect(screen.getByText("v2")).toBeInTheDocument();
expect(screen.getByText("v1")).toBeInTheDocument();
});
});
it("should extract version from prompt_id with _v suffix", async () => {
const versionsWithUnderscore = [
{
prompt_id: "test-prompt_v2",
litellm_params: { prompt_id: "test-prompt_v2" },
prompt_info: { prompt_type: "db" },
created_at: "2024-01-15T10:30:00Z",
},
];
mockGetPromptVersions.mockResolvedValueOnce({
prompts: versionsWithUnderscore,
});
render(<VersionHistorySidePanel {...defaultProps} />);
await waitFor(() => {
expect(screen.getByText("v2")).toBeInTheDocument();
});
});
it("should default to v1 when no version info is available", async () => {
const versionWithoutVersionInfo = [
{
prompt_id: "test-prompt",
litellm_params: { prompt_id: "test-prompt" },
prompt_info: { prompt_type: "db" },
created_at: "2024-01-15T10:30:00Z",
},
];
mockGetPromptVersions.mockResolvedValueOnce({
prompts: versionWithoutVersionInfo,
});
render(<VersionHistorySidePanel {...defaultProps} />);
await waitFor(() => {
expect(screen.getByText("v1")).toBeInTheDocument();
});
});
});
describe("Date Formatting", () => {
it("should format dates correctly", async () => {
render(<VersionHistorySidePanel {...defaultProps} />);
await waitFor(() => {
// Check that dates are displayed (format: YYYY-MM-DD HH:MM:SS)
const dateElements = screen.getAllByTestId("text");
const dateText = dateElements.find((el) => el.textContent?.match(/\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/));
expect(dateText).toBeTruthy();
});
});
it("should show dash for missing dates", async () => {
const versionsWithoutDates = [
{
prompt_id: "test-prompt.v1",
litellm_params: { prompt_id: "test-prompt.v1" },
prompt_info: { prompt_type: "db" },
version: 1,
},
];
mockGetPromptVersions.mockResolvedValueOnce({
prompts: versionsWithoutDates,
});
render(<VersionHistorySidePanel {...defaultProps} />);
await waitFor(() => {
expect(screen.getByText("-")).toBeInTheDocument();
});
});
});
describe("Prompt Type Display", () => {
it("should show 'Saved to Database' for db prompts", async () => {
render(<VersionHistorySidePanel {...defaultProps} />);
await waitFor(() => {
const dbTexts = screen.getAllByText("Saved to Database");
expect(dbTexts.length).toBeGreaterThan(0);
});
});
it("should show 'Config Prompt' for config prompts", async () => {
render(<VersionHistorySidePanel {...defaultProps} />);
await waitFor(() => {
expect(screen.getByText("Config Prompt")).toBeInTheDocument();
});
});
});
describe("Network Calls and Data Fetching", () => {
it("should call getPromptVersions with correct parameters", async () => {
render(<VersionHistorySidePanel {...defaultProps} />);
await waitFor(() => {
expect(getPromptVersions).toHaveBeenCalledWith("test-token", "test-prompt");
});
});
it("should strip .v suffix from promptId when fetching versions", async () => {
render(<VersionHistorySidePanel {...defaultProps} promptId="test-prompt.v3" />);
await waitFor(() => {
expect(getPromptVersions).toHaveBeenCalledWith("test-token", "test-prompt");
});
});
it("should not fetch versions when isOpen is false", () => {
render(<VersionHistorySidePanel {...defaultProps} isOpen={false} />);
expect(getPromptVersions).not.toHaveBeenCalled();
});
it("should not fetch versions when accessToken is null", () => {
render(<VersionHistorySidePanel {...defaultProps} accessToken={null} />);
expect(getPromptVersions).not.toHaveBeenCalled();
});
it("should not fetch versions when promptId is not provided", () => {
render(<VersionHistorySidePanel {...defaultProps} promptId="" />);
expect(getPromptVersions).not.toHaveBeenCalled();
});
it("should refetch versions when props change", async () => {
const { rerender } = render(<VersionHistorySidePanel {...defaultProps} />);
await waitFor(() => {
expect(getPromptVersions).toHaveBeenCalledTimes(1);
});
rerender(<VersionHistorySidePanel {...defaultProps} promptId="different-prompt.v1" />);
await waitFor(() => {
expect(getPromptVersions).toHaveBeenCalledTimes(2);
expect(getPromptVersions).toHaveBeenCalledWith("test-token", "different-prompt");
});
});
});
describe("Error Handling", () => {
it("should handle network errors gracefully", async () => {
const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {});
mockGetPromptVersions.mockRejectedValueOnce(new Error("Network error"));
render(<VersionHistorySidePanel {...defaultProps} />);
await waitFor(() => {
expect(consoleSpy).toHaveBeenCalledWith("Error fetching prompt versions:", expect.any(Error));
});
// Should show empty state when there's an error
expect(screen.getByText("No version history available.")).toBeInTheDocument();
consoleSpy.mockRestore();
});
});
describe("User Interactions", () => {
it("should call onClose when close button is clicked", () => {
const mockOnClose = vi.fn();
render(<VersionHistorySidePanel {...defaultProps} onClose={mockOnClose} />);
const drawer = screen.getByTestId("drawer");
act(() => {
fireEvent.click(drawer); // Simulate close action
});
// Note: This test assumes the drawer handles close events.
// In a real scenario, you'd test the actual close trigger.
});
it("should prevent interaction with main content when drawer is open", () => {
render(<VersionHistorySidePanel {...defaultProps} />);
const drawer = screen.getByTestId("drawer");
// The mask and maskClosable props are passed as boolean false to disable them
expect(drawer).toHaveAttribute("data-mask", "false");
expect(drawer).toHaveAttribute("data-maskclosable", "false");
});
});
describe("Edge Cases", () => {
it("should handle activeVersionId with .v suffix correctly", async () => {
render(<VersionHistorySidePanel {...defaultProps} activeVersionId="test-prompt.v1" />);
await waitFor(() => {
expect(screen.getByText("Active")).toBeInTheDocument();
});
});
it("should handle activeVersionId with _v suffix correctly", async () => {
const versionsWithUnderscore = [
{
prompt_id: "test-prompt_v2",
litellm_params: { prompt_id: "test-prompt_v2" },
prompt_info: { prompt_type: "db" },
version: 2,
created_at: "2024-01-15T10:30:00Z",
},
];
mockGetPromptVersions.mockResolvedValueOnce({
prompts: versionsWithUnderscore,
});
render(<VersionHistorySidePanel {...defaultProps} activeVersionId="test-prompt_v2" />);
await waitFor(() => {
expect(screen.getByText("Active")).toBeInTheDocument();
});
});
it("should sort versions correctly with version field", async () => {
// The component doesn't explicitly sort, but we can verify the order from the API response
render(<VersionHistorySidePanel {...defaultProps} />);
await waitFor(() => {
const versionElements = screen.getAllByTestId("tag");
// Verify versions are displayed as they come from the API
expect(screen.getByText("v2")).toBeInTheDocument();
});
});
});
});

View file

@ -0,0 +1,444 @@
import { describe, expect, it } from "vitest";
import { PromptType } from "./types";
import {
convertToDotPrompt,
extractVariables,
getVersionNumber,
parseExistingPrompt,
stripVersionFromPromptId,
} from "./utils";
describe("extractVariables", () => {
it("should extract variables from messages", () => {
const prompt: PromptType = {
name: "test",
model: "gpt-4",
config: {},
tools: [],
developerMessage: "",
messages: [
{ role: "user", content: "Hello {{name}}, how are you?" },
{ role: "assistant", content: "I am fine {{name}}" },
],
};
const result = extractVariables(prompt);
expect(result).toEqual(["name"]);
});
it("should extract variables from developer message", () => {
const prompt: PromptType = {
name: "test",
model: "gpt-4",
config: {},
tools: [],
developerMessage: "You are {{role}} assistant",
messages: [{ role: "user", content: "Hello" }],
};
const result = extractVariables(prompt);
expect(result).toEqual(["role"]);
});
it("should extract variables from both messages and developer message", () => {
const prompt: PromptType = {
name: "test",
model: "gpt-4",
config: {},
tools: [],
developerMessage: "You are {{role}} assistant",
messages: [
{ role: "user", content: "Hello {{name}}" },
{ role: "assistant", content: "Hi {{name}}, I am {{role}}" },
],
};
const result = extractVariables(prompt);
expect(result.sort()).toEqual(["name", "role"].sort());
});
it("should return empty array when no variables present", () => {
const prompt: PromptType = {
name: "test",
model: "gpt-4",
config: {},
tools: [],
developerMessage: "You are an assistant",
messages: [{ role: "user", content: "Hello world" }],
};
const result = extractVariables(prompt);
expect(result).toEqual([]);
});
it("should handle duplicate variables", () => {
const prompt: PromptType = {
name: "test",
model: "gpt-4",
config: {},
tools: [],
developerMessage: "",
messages: [
{ role: "user", content: "Hello {{name}}" },
{ role: "assistant", content: "Hi {{name}} again" },
],
};
const result = extractVariables(prompt);
expect(result).toEqual(["name"]);
});
});
describe("convertToDotPrompt", () => {
it("should convert basic prompt to dot prompt format", () => {
const prompt: PromptType = {
name: "test",
model: "gpt-4",
config: {},
tools: [],
developerMessage: "",
messages: [{ role: "user", content: "Hello world" }],
};
const result = convertToDotPrompt(prompt);
expect(result).toContain("---");
expect(result).toContain("model: gpt-4");
expect(result).toContain("input:");
expect(result).toContain("schema:");
expect(result).toContain("output:");
expect(result).toContain("format: text");
expect(result).toContain("User: Hello world");
});
it("should include config parameters when set", () => {
const prompt: PromptType = {
name: "test",
model: "gpt-4",
config: {
temperature: 0.7,
max_tokens: 100,
top_p: 0.9,
},
tools: [],
developerMessage: "",
messages: [{ role: "user", content: "Hello" }],
};
const result = convertToDotPrompt(prompt);
expect(result).toContain("temperature: 0.7");
expect(result).toContain("max_tokens: 100");
expect(result).toContain("top_p: 0.9");
});
it("should include input schema with variables", () => {
const prompt: PromptType = {
name: "test",
model: "gpt-4",
config: {},
tools: [],
developerMessage: "",
messages: [{ role: "user", content: "Hello {{name}}" }],
};
const result = convertToDotPrompt(prompt);
expect(result).toContain("input:");
expect(result).toContain("schema:");
expect(result).toContain("name: string");
});
it("should include developer message when present", () => {
const prompt: PromptType = {
name: "test",
model: "gpt-4",
config: {},
tools: [],
developerMessage: "You are a helpful assistant",
messages: [{ role: "user", content: "Hello" }],
};
const result = convertToDotPrompt(prompt);
expect(result).toContain("Developer: You are a helpful assistant");
});
it("should include tools when present", () => {
const prompt: PromptType = {
name: "test",
model: "gpt-4",
config: {},
tools: [
{
name: "get_weather",
description: "Get weather information",
json: '{"type": "function", "function": {"name": "get_weather"}}',
},
],
developerMessage: "",
messages: [{ role: "user", content: "Hello" }],
};
const result = convertToDotPrompt(prompt);
expect(result).toContain("tools:");
expect(result).toContain('{"type":"function","function":{"name":"get_weather"}}');
});
it("should handle multiple messages with different roles", () => {
const prompt: PromptType = {
name: "test",
model: "gpt-4",
config: {},
tools: [],
developerMessage: "",
messages: [
{ role: "user", content: "Hello" },
{ role: "assistant", content: "Hi there" },
{ role: "user", content: "How are you?" },
],
};
const result = convertToDotPrompt(prompt);
expect(result).toContain("User: Hello");
expect(result).toContain("Assistant: Hi there");
expect(result).toContain("User: How are you?");
});
});
describe("parseExistingPrompt", () => {
it("should parse basic dotprompt content", () => {
const apiResponse = {
prompt_spec: {
litellm_params: {
dotprompt_content: `---
model: gpt-4
input:
schema:
output:
format: text
---
User: Hello world`,
},
prompt_id: "test-prompt",
},
};
const result = parseExistingPrompt(apiResponse);
expect(result.name).toBe("test-prompt");
expect(result.model).toBe("gpt-4");
expect(result.messages).toEqual([{ role: "user", content: "Hello world" }]);
});
it("should parse with config parameters", () => {
const apiResponse = {
prompt_spec: {
litellm_params: {
dotprompt_content: `---
model: gpt-4
temperature: 0.7
max_tokens: 100
top_p: 0.9
input:
schema:
output:
format: text
---
User: Hello`,
},
prompt_id: "test-prompt",
},
};
const result = parseExistingPrompt(apiResponse);
expect(result.config.temperature).toBe(0.7);
expect(result.config.max_tokens).toBe(100);
expect(result.config.top_p).toBe(0.9);
});
it("should parse with developer message", () => {
const apiResponse = {
prompt_spec: {
litellm_params: {
dotprompt_content: `---
model: gpt-4
input:
schema:
output:
format: text
---
Developer: You are a helpful assistant
User: Hello`,
},
prompt_id: "test-prompt",
},
};
const result = parseExistingPrompt(apiResponse);
expect(result.developerMessage).toBe("You are a helpful assistant");
});
it("should parse multiple messages", () => {
const apiResponse = {
prompt_spec: {
litellm_params: {
dotprompt_content: `---
model: gpt-4
input:
schema:
output:
format: text
---
User: Hello
How are you?
Assistant: I am fine
Thank you for asking
User: Great!`,
},
prompt_id: "test-prompt",
},
};
const result = parseExistingPrompt(apiResponse);
expect(result.messages).toEqual([
{ role: "user", content: "Hello\nHow are you?" },
{ role: "assistant", content: "I am fine\nThank you for asking" },
{ role: "user", content: "Great!" },
]);
});
it("should handle prompt with version suffix", () => {
const apiResponse = {
prompt_spec: {
litellm_params: {
dotprompt_content: `---
model: gpt-4
input:
schema:
output:
format: text
---
User: Hello`,
},
prompt_id: "test-prompt.v2",
},
};
const result = parseExistingPrompt(apiResponse);
expect(result.name).toBe("test-prompt");
});
it("should throw error when no dotprompt_content", () => {
const apiResponse = {
prompt_spec: {
litellm_params: {},
},
};
expect(() => parseExistingPrompt(apiResponse)).toThrow("No dotprompt_content found in API response");
});
it("should throw error for invalid dotprompt format", () => {
const apiResponse = {
prompt_spec: {
litellm_params: {
dotprompt_content: "invalid format",
},
},
};
expect(() => parseExistingPrompt(apiResponse)).toThrow("Invalid dotprompt format");
});
it("should provide default values when parsing fails", () => {
const apiResponse = {
prompt_spec: {
litellm_params: {
dotprompt_content: `---
model: gpt-4
input:
schema:
output:
format: text
---
`,
},
prompt_id: "test-prompt",
},
};
const result = parseExistingPrompt(apiResponse);
expect(result.messages).toEqual([
{ role: "user", content: "Enter task specifics. Use {{template_variables}} for dynamic inputs" },
]);
});
});
describe("getVersionNumber", () => {
it("should return '1' for undefined promptId", () => {
const result = getVersionNumber(undefined);
expect(result).toBe("1");
});
it("should return '1' for promptId without version", () => {
const result = getVersionNumber("test-prompt");
expect(result).toBe("1");
});
it("should extract version with dot separator", () => {
const result = getVersionNumber("test-prompt.v2");
expect(result).toBe("2");
});
it("should extract version with underscore separator", () => {
const result = getVersionNumber("test-prompt_v3");
expect(result).toBe("3");
});
it("should extract version with hyphen separator", () => {
const result = getVersionNumber("test-prompt-v4");
expect(result).toBe("4");
});
it("should extract multi-digit version", () => {
const result = getVersionNumber("test-prompt.v123");
expect(result).toBe("123");
});
});
describe("stripVersionFromPromptId", () => {
it("should return empty string for undefined promptId", () => {
const result = stripVersionFromPromptId(undefined);
expect(result).toBe("");
});
it("should return promptId unchanged when no version present", () => {
const result = stripVersionFromPromptId("test-prompt");
expect(result).toBe("test-prompt");
});
it("should strip version with dot separator", () => {
const result = stripVersionFromPromptId("test-prompt.v2");
expect(result).toBe("test-prompt");
});
it("should strip version with underscore separator", () => {
const result = stripVersionFromPromptId("test-prompt_v3");
expect(result).toBe("test-prompt");
});
it("should strip version with hyphen separator", () => {
const result = stripVersionFromPromptId("test-prompt-v4");
expect(result).toBe("test-prompt");
});
it("should strip multi-digit version", () => {
const result = stripVersionFromPromptId("test-prompt.v123");
expect(result).toBe("test-prompt");
});
});