diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/GuardrailsPanel.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/GuardrailsPanel.test.tsx
index 29655e5910a..88e52e0cdd1 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/GuardrailsPanel.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/GuardrailsPanel.test.tsx
@@ -35,10 +35,19 @@ vi.mock("./guardrail_info", () => ({
default: () =>
Mock Guardrail Info View
,
}));
-vi.mock("./GuardrailTestPlayground", () => ({
- __esModule: true,
- default: () => Mock Guardrail Test Playground
,
-}));
+vi.mock("./GuardrailTestPlayground", async () => {
+ const { useState } = await import("react");
+ const MockGuardrailTestPlayground = () => {
+ const [draft, setDraft] = useState("");
+ return (
+
+
Mock Guardrail Test Playground
+
setDraft(e.target.value)} />
+
+ );
+ };
+ return { __esModule: true, default: MockGuardrailTestPlayground };
+});
vi.mock("./TeamGuardrailsTab", () => ({
TeamGuardrailsTab: () => Mock Team Guardrails Tab
,
@@ -129,6 +138,21 @@ describe("GuardrailsPanel", () => {
expect(mockGetGuardrailsList).toHaveBeenCalledTimes(2);
});
+ it("should keep test playground state when switching tabs away and back", async () => {
+ render();
+
+ fireEvent.click(screen.getByText("Test Playground"));
+
+ const draft = await screen.findByLabelText("playground draft");
+ fireEvent.change(draft, { target: { value: "keep me" } });
+ expect(draft).toHaveValue("keep me");
+
+ fireEvent.click(screen.getByText("Guardrails"));
+ fireEvent.click(screen.getByText("Test Playground"));
+
+ expect(await screen.findByLabelText("playground draft")).toHaveValue("keep me");
+ });
+
it("should not delete anything when the modal is cancelled", async () => {
render();
fireEvent.click(screen.getByText("Guardrails"));
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/ContentFilterConfiguration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/ContentFilterConfiguration.test.tsx
new file mode 100644
index 00000000000..37a3d40128e
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/ContentFilterConfiguration.test.tsx
@@ -0,0 +1,136 @@
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { renderWithProviders, screen } from "@/../tests/test-utils";
+import userEvent from "@testing-library/user-event";
+import ContentFilterConfiguration from "./ContentFilterConfiguration";
+
+vi.mock("@/components/networking", () => ({
+ validateBlockedWordsFile: vi.fn(),
+ getCategoryYaml: vi.fn(),
+}));
+
+vi.mock("@/components/molecules/notifications_manager", () => ({
+ default: { success: vi.fn(), error: vi.fn(), fromBackend: vi.fn() },
+}));
+
+const PREBUILT = [
+ { name: "us_ssn", display_name: "US Social Security Number", category: "PII Patterns", description: "d" },
+];
+
+describe("ContentFilterConfiguration", () => {
+ const handlers = {
+ onPatternAdd: vi.fn(),
+ onPatternRemove: vi.fn(),
+ onPatternActionChange: vi.fn(),
+ onBlockedWordAdd: vi.fn(),
+ onBlockedWordRemove: vi.fn(),
+ onBlockedWordUpdate: vi.fn(),
+ };
+
+ const renderConfig = (overrides = {}) =>
+ renderWithProviders(
+ ,
+ );
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("should render the pattern and keyword sections", () => {
+ renderConfig();
+
+ expect(screen.getByText("Pattern Detection")).toBeInTheDocument();
+ expect(
+ screen.getByText("Detect sensitive information using regex patterns (SSN, credit cards, API keys, etc.)"),
+ ).toBeInTheDocument();
+ expect(screen.getByText("Blocked Keywords")).toBeInTheDocument();
+ expect(screen.getByText("Block or mask specific sensitive terms and phrases")).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /add prebuilt pattern/i })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /add custom regex/i })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /add keyword/i })).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /upload yaml file/i })).toBeInTheDocument();
+ });
+
+ it("should show the empty states for patterns and keywords", () => {
+ renderConfig();
+
+ expect(screen.getByText("No patterns added.")).toBeInTheDocument();
+ expect(screen.getByText("No keywords added.")).toBeInTheDocument();
+ });
+
+ it("should open the prebuilt pattern modal", async () => {
+ const user = userEvent.setup();
+ renderConfig();
+
+ expect(screen.queryByText("Pattern type")).not.toBeInTheDocument();
+
+ await user.click(screen.getByRole("button", { name: /add prebuilt pattern/i }));
+
+ expect(await screen.findByText("Pattern type")).toBeInTheDocument();
+ });
+
+ it("should open the custom regex modal", async () => {
+ const user = userEvent.setup();
+ renderConfig();
+
+ expect(screen.queryByText("Add custom regex pattern")).not.toBeInTheDocument();
+
+ await user.click(screen.getByRole("button", { name: /add custom regex/i }));
+
+ expect(await screen.findByText("Add custom regex pattern")).toBeInTheDocument();
+ expect(screen.getByPlaceholderText("e.g., ID-[0-9]{6}")).toBeInTheDocument();
+ });
+
+ it("should open the keyword modal", async () => {
+ const user = userEvent.setup();
+ renderConfig();
+
+ expect(screen.queryByText("Add blocked keyword")).not.toBeInTheDocument();
+
+ await user.click(screen.getByRole("button", { name: /add keyword/i }));
+
+ expect(await screen.findByText("Add blocked keyword")).toBeInTheDocument();
+ expect(screen.getByPlaceholderText("Enter sensitive keyword or phrase")).toBeInTheDocument();
+ });
+
+ it("should list already selected patterns and keywords", () => {
+ renderConfig({
+ selectedPatterns: [
+ {
+ id: "pattern-1",
+ type: "prebuilt" as const,
+ name: "us_ssn",
+ display_name: "US Social Security Number",
+ action: "BLOCK" as const,
+ },
+ ],
+ blockedWords: [{ id: "word-1", keyword: "secret", action: "MASK" as const, description: "Sensitive" }],
+ });
+
+ expect(screen.getByText("US Social Security Number")).toBeInTheDocument();
+ expect(screen.getByText("secret")).toBeInTheDocument();
+ expect(screen.queryByText("No patterns added.")).not.toBeInTheDocument();
+ expect(screen.queryByText("No keywords added.")).not.toBeInTheDocument();
+ });
+
+ it("should show only the keyword section when the keywords step is requested", () => {
+ renderConfig({ showStep: "keywords" });
+
+ expect(screen.getByText("Blocked Keywords")).toBeInTheDocument();
+ expect(screen.queryByText("Pattern Detection")).not.toBeInTheDocument();
+ });
+
+ it("should show only the pattern section when the patterns step is requested", () => {
+ renderConfig({ showStep: "patterns" });
+
+ expect(screen.getByText("Pattern Detection")).toBeInTheDocument();
+ expect(screen.queryByText("Blocked Keywords")).not.toBeInTheDocument();
+ });
+});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/ContentFilterDisplay.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/ContentFilterDisplay.test.tsx
new file mode 100644
index 00000000000..8fb48791f76
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/ContentFilterDisplay.test.tsx
@@ -0,0 +1,89 @@
+import { describe, it, expect } from "vitest";
+import { renderWithProviders, screen } from "@/../tests/test-utils";
+import ContentFilterDisplay from "./ContentFilterDisplay";
+
+const PATTERN = {
+ id: "pattern-1",
+ type: "prebuilt" as const,
+ name: "email",
+ display_name: "Email address",
+ action: "BLOCK" as const,
+};
+
+const KEYWORD = {
+ id: "word-1",
+ keyword: "secret",
+ action: "MASK" as const,
+ description: "Sensitive term",
+};
+
+const CATEGORY = {
+ id: "category-1",
+ category: "self_harm",
+ display_name: "Self Harm",
+ action: "BLOCK" as const,
+ severity_threshold: "high" as const,
+};
+
+describe("ContentFilterDisplay", () => {
+ it("should render nothing when there is no content filter data", () => {
+ const { container } = renderWithProviders();
+
+ expect(container).toBeEmptyDOMElement();
+ });
+
+ it("should render the categories section with a configured count", () => {
+ renderWithProviders();
+
+ expect(screen.getByText("Content Categories")).toBeInTheDocument();
+ expect(screen.getByText("1 categories configured")).toBeInTheDocument();
+ expect(screen.getByText("Self Harm")).toBeInTheDocument();
+ expect(screen.queryByText("Pattern Detection")).not.toBeInTheDocument();
+ expect(screen.queryByText("Blocked Keywords")).not.toBeInTheDocument();
+ });
+
+ it("should render the patterns section with a configured count", () => {
+ renderWithProviders();
+
+ expect(screen.getByText("Pattern Detection")).toBeInTheDocument();
+ expect(screen.getByText("1 patterns configured")).toBeInTheDocument();
+ expect(screen.getByText("Email address")).toBeInTheDocument();
+ expect(screen.queryByText("Content Categories")).not.toBeInTheDocument();
+ });
+
+ it("should render the keywords section with a configured count", () => {
+ renderWithProviders();
+
+ expect(screen.getByText("Blocked Keywords")).toBeInTheDocument();
+ expect(screen.getByText("1 keywords configured")).toBeInTheDocument();
+ expect(screen.getByText("secret")).toBeInTheDocument();
+ expect(screen.getByText("Sensitive term")).toBeInTheDocument();
+ });
+
+ it("should render every section when all three kinds of data are present", () => {
+ renderWithProviders();
+
+ expect(screen.getByText("Content Categories")).toBeInTheDocument();
+ expect(screen.getByText("Pattern Detection")).toBeInTheDocument();
+ expect(screen.getByText("Blocked Keywords")).toBeInTheDocument();
+ });
+
+ it("should render category severity and action as static text in read-only mode", () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.getByText("HIGH")).toBeInTheDocument();
+ expect(screen.getByText("BLOCK")).toBeInTheDocument();
+ expect(screen.getAllByRole("button", { name: /delete/i })).toHaveLength(2);
+ });
+
+ it("should render category severity and action as editable controls when not read-only", () => {
+ renderWithProviders(
+ ,
+ );
+
+ expect(screen.queryByText("HIGH")).not.toBeInTheDocument();
+ expect(screen.getAllByRole("button", { name: /delete/i })).toHaveLength(3);
+ });
+});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/ContentFilterManager.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/ContentFilterManager.test.tsx
index 66758ac72f1..c3fd2e3eb59 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/ContentFilterManager.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/ContentFilterManager.test.tsx
@@ -2,7 +2,6 @@ import { describe, it, expect, vi, beforeEach } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import ContentFilterManager, { formatContentFilterDataForAPI } from "./ContentFilterManager";
-import React from "react";
const CONTENT_FILTER_GUARDRAIL_DATA = {
litellm_params: {
@@ -85,18 +84,7 @@ vi.mock("./ContentFilterDisplay", () => ({
),
}));
-vi.mock("antd", async (importOriginal) => {
- const actual = await importOriginal();
- return {
- ...actual,
- Divider: ({ children }: { children: React.ReactNode }) => {children}
,
- Alert: ({ message, type }: { message: React.ReactNode; type: string }) => (
-
- {message}
-
- ),
- };
-});
+const UNSAVED_CHANGES_TEXT = /You have unsaved changes to patterns or keywords/;
describe("ContentFilterManager", () => {
beforeEach(() => {
@@ -117,7 +105,7 @@ describe("ContentFilterManager", () => {
expect(screen.getByTestId("content-filter-config")).toBeInTheDocument();
});
- expect(screen.getByTestId("divider")).toHaveTextContent("Content Filter Configuration");
+ expect(screen.getByText("Content Filter Configuration")).toBeInTheDocument();
});
it("should return null when guardrail is not litellm_content_filter", () => {
@@ -275,15 +263,15 @@ describe("ContentFilterManager", () => {
expect(screen.getByTestId("content-filter-config")).toBeInTheDocument();
});
- expect(screen.queryByTestId("unsaved-alert")).not.toBeInTheDocument();
+ expect(screen.queryByText(UNSAVED_CHANGES_TEXT)).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: /add pattern/i }));
await waitFor(() => {
- expect(screen.getByTestId("unsaved-alert")).toBeInTheDocument();
+ expect(screen.getByText(UNSAVED_CHANGES_TEXT)).toBeInTheDocument();
});
- expect(screen.getByTestId("unsaved-alert")).toHaveTextContent(/unsaved changes.*Save Changes/i);
+ expect(screen.getByText(UNSAVED_CHANGES_TEXT)).toHaveTextContent(/Save Changes/i);
});
it("should call onDataChange when patterns or keywords change", async () => {
@@ -371,7 +359,7 @@ describe("ContentFilterManager", () => {
);
await waitFor(() => {
- expect(screen.getByTestId("divider")).toBeInTheDocument();
+ expect(screen.getByText("Content Filter Configuration")).toBeInTheDocument();
});
expect(screen.queryByTestId("content-filter-config")).not.toBeInTheDocument();
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/ContentFilterTables.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/ContentFilterTables.test.tsx
index cc27394f2c3..c56ee8ff528 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/ContentFilterTables.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/ContentFilterTables.test.tsx
@@ -130,4 +130,77 @@ describe("content filter tables", () => {
expect(onCategoryRemove).toHaveBeenCalledWith("category-1");
});
+
+ it("should report a pattern action change", async () => {
+ const onActionChange = vi.fn();
+ const user = userEvent.setup();
+
+ renderWithProviders(
+ ,
+ );
+
+ await user.click(screen.getByRole("combobox"));
+ const maskOptions = await screen.findAllByText("Mask");
+ await user.click(maskOptions[maskOptions.length - 1]);
+
+ expect(onActionChange).toHaveBeenCalledWith("pattern-1", "MASK");
+ });
+
+ it("should report a keyword action change", async () => {
+ const onActionChange = vi.fn();
+ const user = userEvent.setup();
+
+ renderWithProviders(
+ ,
+ );
+
+ await user.click(screen.getByRole("combobox"));
+ const maskOptions = await screen.findAllByText("Mask");
+ await user.click(maskOptions[maskOptions.length - 1]);
+
+ expect(onActionChange).toHaveBeenCalledWith("keyword-1", "action", "MASK");
+ });
+
+ it("should report category severity and action changes", async () => {
+ const onSeverityChange = vi.fn();
+ const onActionChange = vi.fn();
+ const user = userEvent.setup();
+
+ renderWithProviders(
+ ,
+ );
+
+ await user.click(screen.getAllByRole("combobox")[0]);
+ const lowOptions = await screen.findAllByText("Low");
+ await user.click(lowOptions[lowOptions.length - 1]);
+
+ expect(onSeverityChange).toHaveBeenCalledWith("category-1", "low");
+
+ await user.click(screen.getAllByRole("combobox")[1]);
+ const maskOptions = await screen.findAllByText("Mask");
+ await user.click(maskOptions[maskOptions.length - 1]);
+
+ expect(onActionChange).toHaveBeenCalledWith("category-1", "MASK");
+ });
});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/KeywordModal.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/KeywordModal.test.tsx
new file mode 100644
index 00000000000..dda479f1579
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/KeywordModal.test.tsx
@@ -0,0 +1,87 @@
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import KeywordModal from "./KeywordModal";
+
+describe("KeywordModal", () => {
+ const handlers = {
+ onKeywordChange: vi.fn(),
+ onActionChange: vi.fn(),
+ onDescriptionChange: vi.fn(),
+ onAdd: vi.fn(),
+ onCancel: vi.fn(),
+ };
+
+ const renderModal = (overrides: Partial> = {}) =>
+ render();
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ it("should render the keyword, action and description fields", async () => {
+ renderModal();
+
+ expect(await screen.findByText("Add blocked keyword")).toBeInTheDocument();
+ expect(screen.getByPlaceholderText("Enter sensitive keyword or phrase")).toBeInTheDocument();
+ expect(screen.getByPlaceholderText("Explain why this keyword is sensitive")).toBeInTheDocument();
+ expect(screen.getByText("Description (optional)")).toBeInTheDocument();
+ expect(
+ screen.getByText("Choose what action the guardrail should take when this keyword is detected"),
+ ).toBeInTheDocument();
+ });
+
+ it("should report keyword edits", async () => {
+ const user = userEvent.setup();
+ renderModal();
+
+ await user.type(await screen.findByPlaceholderText("Enter sensitive keyword or phrase"), "s");
+
+ expect(handlers.onKeywordChange).toHaveBeenCalledWith("s");
+ });
+
+ it("should report description edits", async () => {
+ const user = userEvent.setup();
+ renderModal();
+
+ await user.type(await screen.findByPlaceholderText("Explain why this keyword is sensitive"), "x");
+
+ expect(handlers.onDescriptionChange).toHaveBeenCalledWith("x");
+ });
+
+ it("should report the chosen action", async () => {
+ const user = userEvent.setup();
+ renderModal();
+
+ await user.click(await screen.findByRole("combobox"));
+ const maskOptions = await screen.findAllByText("Mask");
+ await user.click(maskOptions[maskOptions.length - 1]);
+
+ expect(handlers.onActionChange).toHaveBeenCalled();
+ expect(handlers.onActionChange.mock.calls[0][0]).toBe("MASK");
+ });
+
+ it("should show the current keyword and description values", async () => {
+ renderModal({ keyword: "secret", description: "sensitive term" });
+
+ expect(await screen.findByDisplayValue("secret")).toBeInTheDocument();
+ expect(screen.getByDisplayValue("sensitive term")).toBeInTheDocument();
+ });
+
+ it("should add and cancel through the footer buttons", async () => {
+ const user = userEvent.setup();
+ renderModal();
+
+ await user.click(await screen.findByRole("button", { name: "Add" }));
+ expect(handlers.onAdd).toHaveBeenCalledTimes(1);
+
+ await user.click(screen.getByRole("button", { name: "Cancel" }));
+ expect(handlers.onCancel).toHaveBeenCalledTimes(1);
+ });
+
+ it("should not render its content when not visible", () => {
+ renderModal({ visible: false });
+
+ expect(screen.queryByText("Add blocked keyword")).not.toBeInTheDocument();
+ });
+});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/PatternModal.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/PatternModal.test.tsx
index 4e6fd0db809..4311f4e13aa 100644
--- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/PatternModal.test.tsx
+++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/content_filter/PatternModal.test.tsx
@@ -1,5 +1,5 @@
import { describe, it, expect, vi, beforeEach } from "vitest";
-import { render, screen, waitFor } from "@testing-library/react";
+import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import PatternModal from "./PatternModal";
@@ -10,25 +10,25 @@ describe("PatternModal", () => {
const mockOnActionChange = vi.fn();
const mockPrebuiltPatterns = [
- { name: "us_ssn", category: "PII Patterns", description: "US Social Security Number" },
- { name: "email", category: "PII Patterns", description: "Email addresses" },
- { name: "visa", category: "Financial Patterns", description: "Visa credit card numbers" },
- { name: "aws_access_key", category: "Credential Patterns", description: "AWS Access Keys" },
+ {
+ name: "us_ssn",
+ display_name: "US Social Security Number",
+ category: "PII Patterns",
+ description: "US Social Security Number",
+ },
+ { name: "email", display_name: "Email address", category: "PII Patterns", description: "Email addresses" },
+ { name: "visa", display_name: "Visa card", category: "Financial Patterns", description: "Visa credit cards" },
+ {
+ name: "aws_access_key",
+ display_name: "AWS access key",
+ category: "Credential Patterns",
+ description: "AWS Access Keys",
+ },
];
const mockCategories = ["PII Patterns", "Financial Patterns", "Credential Patterns"];
- beforeEach(() => {
- vi.clearAllMocks();
- });
-
- it("should show dropdown with prebuilt pattern options grouped by category", async () => {
- /**
- * Tests that the modal displays a dropdown with prebuilt patterns
- * organized by category. This verifies the pattern selection UI is working.
- */
- const user = userEvent.setup();
-
+ const renderModal = () =>
render(
{
/>,
);
- // Wait for modal to be visible
- await waitFor(() => {
- expect(screen.getByText("Add prebuilt pattern")).toBeInTheDocument();
- });
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
- // Find the pattern type dropdown by looking for the first combobox input
- const comboboxes = screen.getAllByRole("combobox");
- const dropdown = comboboxes[0]; // First combobox is the pattern selector
- expect(dropdown).toBeInTheDocument();
+ it("should show prebuilt pattern options grouped by category and report the picked pattern", async () => {
+ const user = userEvent.setup();
+ renderModal();
- // Click to open the dropdown
- await user.click(dropdown);
+ expect(await screen.findByText("Add prebuilt pattern")).toBeInTheDocument();
- // Verify that pattern options are available in the dropdown
- // Ant Design renders Select options in a portal, so we need to query the whole document
- await waitFor(() => {
- const options = document.querySelectorAll(".ant-select-item-option");
- expect(options.length).toBeGreaterThan(0);
- });
+ await user.click(screen.getAllByRole("combobox")[0]);
- // Verify categories are shown as group labels
- await waitFor(() => {
- expect(document.body).toHaveTextContent("PII Patterns");
- expect(document.body).toHaveTextContent("Financial Patterns");
- expect(document.body).toHaveTextContent("Credential Patterns");
- });
+ expect(await screen.findByText("PII Patterns")).toBeInTheDocument();
+ expect(screen.getByText("Financial Patterns")).toBeInTheDocument();
+ expect(screen.getByText("Credential Patterns")).toBeInTheDocument();
- // Verify pattern options are available
- expect(document.body).toHaveTextContent("us_ssn");
- expect(document.body).toHaveTextContent("email");
- expect(document.body).toHaveTextContent("visa");
- expect(document.body).toHaveTextContent("aws_access_key");
+ expect(screen.getByText("Email address")).toBeInTheDocument();
+ expect(screen.getByText("Visa card")).toBeInTheDocument();
+ expect(screen.getByText("AWS access key")).toBeInTheDocument();
- // Select a pattern by clicking on its option element
- const ssnOption = Array.from(document.querySelectorAll(".ant-select-item-option")).find(
- (el) => el.textContent === "us_ssn",
- ) as HTMLElement;
- await user.click(ssnOption);
+ const ssnOptions = await screen.findAllByText("US Social Security Number");
+ await user.click(ssnOptions[ssnOptions.length - 1]);
- // Verify the change handler was called with the pattern name
- // Note: Ant Design Select calls onChange with (value, option), so we check if it was called
expect(mockOnPatternNameChange).toHaveBeenCalled();
- const callArgs = mockOnPatternNameChange.mock.calls[0];
- expect(callArgs[0]).toBe("us_ssn");
+ expect(mockOnPatternNameChange.mock.calls[0][0]).toBe("us_ssn");
+ });
+
+ it("should report the chosen action", async () => {
+ const user = userEvent.setup();
+ renderModal();
+
+ expect(await screen.findByText("Add prebuilt pattern")).toBeInTheDocument();
+
+ await user.click(screen.getAllByRole("combobox")[1]);
+ const maskOptions = await screen.findAllByText("Mask");
+ await user.click(maskOptions[maskOptions.length - 1]);
+
+ expect(mockOnActionChange).toHaveBeenCalled();
+ expect(mockOnActionChange.mock.calls[0][0]).toBe("MASK");
+ });
+
+ it("should add and cancel through the footer buttons", async () => {
+ const user = userEvent.setup();
+ renderModal();
+
+ expect(await screen.findByText("Add prebuilt pattern")).toBeInTheDocument();
+
+ await user.click(screen.getByRole("button", { name: "Add" }));
+ expect(mockOnAdd).toHaveBeenCalledTimes(1);
+
+ await user.click(screen.getByRole("button", { name: "Cancel" }));
+ expect(mockOnCancel).toHaveBeenCalledTimes(1);
+ });
+
+ it("should not render its content when not visible", () => {
+ render(
+ ,
+ );
+
+ expect(screen.queryByText("Add prebuilt pattern")).not.toBeInTheDocument();
});
});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/custom_code/CustomCodeModal.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/custom_code/CustomCodeModal.test.tsx
new file mode 100644
index 00000000000..ff3432a0d82
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/custom_code/CustomCodeModal.test.tsx
@@ -0,0 +1,162 @@
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { render, screen, waitFor } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import CustomCodeModal from "./CustomCodeModal";
+import { createGuardrailCall, updateGuardrailCall, testCustomCodeGuardrail } from "@/components/networking";
+
+vi.mock("@/components/networking", () => ({
+ createGuardrailCall: vi.fn(),
+ updateGuardrailCall: vi.fn(),
+ testCustomCodeGuardrail: vi.fn(),
+}));
+
+vi.mock("@/components/molecules/notifications_manager", () => ({
+ default: { success: vi.fn(), error: vi.fn(), fromBackend: vi.fn() },
+}));
+
+const mockCreate = vi.mocked(createGuardrailCall);
+const mockUpdate = vi.mocked(updateGuardrailCall);
+const mockTest = vi.mocked(testCustomCodeGuardrail);
+
+describe("CustomCodeModal", () => {
+ const onClose = vi.fn();
+ const onSuccess = vi.fn();
+
+ const renderModal = (overrides = {}) =>
+ render();
+
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockCreate.mockResolvedValue({} as never);
+ mockUpdate.mockResolvedValue({} as never);
+ });
+
+ it("should render the create heading and the editor scaffolding", async () => {
+ renderModal();
+
+ expect(await screen.findByText("Create Custom Guardrail")).toBeInTheDocument();
+ expect(screen.getByText("Define custom logic using Python-like syntax")).toBeInTheDocument();
+ expect(screen.getByPlaceholderText("e.g., block-pii-custom")).toBeInTheDocument();
+ expect(screen.getByText("Guardrail Name")).toBeInTheDocument();
+ expect(screen.getByText("Mode (can select multiple)")).toBeInTheDocument();
+ expect(screen.getByText("Available Primitives")).toBeInTheDocument();
+ expect(screen.getByText("Python Logic")).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /save guardrail/i })).toBeInTheDocument();
+ });
+
+ it("should seed the editor with the empty template", async () => {
+ renderModal();
+
+ const editor = await screen.findByDisplayValue(/async def apply_guardrail/);
+ expect(editor).toBeInTheDocument();
+ });
+
+ it("should not render its content when not visible", () => {
+ renderModal({ visible: false });
+
+ expect(screen.queryByText("Create Custom Guardrail")).not.toBeInTheDocument();
+ });
+
+ it("should render the edit heading and existing values in edit mode", async () => {
+ renderModal({
+ editData: {
+ guardrail_id: "g-1",
+ guardrail_name: "existing-guardrail",
+ litellm_params: { mode: "post_call", default_on: true, custom_code: "def apply_guardrail(): pass" },
+ },
+ });
+
+ expect(await screen.findByText("Edit Custom Guardrail")).toBeInTheDocument();
+ expect(screen.getByDisplayValue("existing-guardrail")).toBeInTheDocument();
+ expect(screen.getByDisplayValue("def apply_guardrail(): pass")).toBeInTheDocument();
+ expect(screen.getByRole("button", { name: /update guardrail/i })).toBeInTheDocument();
+ });
+
+ it("should keep save disabled until a guardrail name is entered", async () => {
+ const user = userEvent.setup();
+ renderModal();
+
+ await user.click(await screen.findByRole("button", { name: /save guardrail/i }));
+ expect(mockCreate).not.toHaveBeenCalled();
+
+ await user.type(screen.getByPlaceholderText("e.g., block-pii-custom"), "my-guardrail");
+ await user.click(screen.getByRole("button", { name: /save guardrail/i }));
+
+ await waitFor(() => {
+ expect(mockCreate).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ it("should create the guardrail with the entered name, mode and code", async () => {
+ const user = userEvent.setup();
+ renderModal();
+
+ await user.type(await screen.findByPlaceholderText("e.g., block-pii-custom"), "block-pii");
+ await user.click(screen.getByRole("button", { name: /save guardrail/i }));
+
+ await waitFor(() => {
+ expect(mockCreate).toHaveBeenCalled();
+ });
+
+ const [token, payload] = mockCreate.mock.calls[0] as [string, Record];
+ expect(token).toBe("test-token");
+ expect(payload).toMatchObject({
+ guardrail_name: "block-pii",
+ litellm_params: { guardrail: "custom_code", mode: ["pre_call"], default_on: false },
+ });
+ await waitFor(() => {
+ expect(onSuccess).toHaveBeenCalled();
+ });
+ });
+
+ it("should switch the editor contents when a template is chosen", async () => {
+ const user = userEvent.setup();
+ renderModal();
+
+ expect(await screen.findByDisplayValue(/async def apply_guardrail/)).toBeInTheDocument();
+
+ const comboboxes = screen.getAllByRole("combobox");
+ await user.click(comboboxes[comboboxes.length - 1]);
+ const options = await screen.findAllByText("Block SSN");
+ await user.click(options[options.length - 1]);
+
+ expect(await screen.findByDisplayValue(/SSN detected/)).toBeInTheDocument();
+ });
+
+ it("should expand the test section and run a test against the backend", async () => {
+ const user = userEvent.setup();
+ mockTest.mockResolvedValue({ success: true, result: { action: "allow" } } as never);
+ renderModal();
+
+ await user.click(await screen.findByText("Test Your Guardrail"));
+
+ const runButton = await screen.findByRole("button", { name: /run test/i });
+ await user.click(runButton);
+
+ await waitFor(() => {
+ expect(mockTest).toHaveBeenCalled();
+ });
+ expect(await screen.findByText("Allowed")).toBeInTheDocument();
+ });
+
+ it("should surface a backend test error", async () => {
+ const user = userEvent.setup();
+ mockTest.mockResolvedValue({ success: false, error: "boom", error_type: "SyntaxError" } as never);
+ renderModal();
+
+ await user.click(await screen.findByText("Test Your Guardrail"));
+ await user.click(await screen.findByRole("button", { name: /run test/i }));
+
+ expect(await screen.findByText("boom")).toBeInTheDocument();
+ expect(screen.getByText("[SyntaxError]")).toBeInTheDocument();
+ });
+
+ it("should cancel through the footer button", async () => {
+ const user = userEvent.setup();
+ renderModal();
+
+ await user.click(await screen.findByRole("button", { name: "Cancel" }));
+
+ expect(onClose).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_garden.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_garden.test.tsx
new file mode 100644
index 00000000000..e20c8a6fa55
--- /dev/null
+++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails/_components/guardrail_garden.test.tsx
@@ -0,0 +1,104 @@
+import { describe, it, expect, vi, beforeEach } from "vitest";
+import { render, screen } from "@testing-library/react";
+import userEvent from "@testing-library/user-event";
+import GuardrailGarden from "./guardrail_garden";
+import { ALL_CARDS } from "./guardrail_garden_data";
+
+vi.mock("./guardrail_garden_detail", () => ({
+ __esModule: true,
+ default: ({ card, onBack }: { card: { name: string }; onBack: () => void }) => (
+
+ Detail for {card.name}
+
+
+ ),
+}));
+
+const LITELLM_CARDS = ALL_CARDS.filter((c) => c.category === "litellm");
+const PARTNER_CARDS = ALL_CARDS.filter((c) => c.category === "partner");
+
+describe("GuardrailGarden", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ });
+
+ const renderGarden = () => render();
+
+ it("should render both sections with their descriptions", () => {
+ renderGarden();
+
+ expect(screen.getByText("LiteLLM Content Filter")).toBeInTheDocument();
+ expect(
+ screen.getByText(
+ "Built-in guardrails powered by LiteLLM. Zero latency, no external dependencies, no additional cost.",
+ ),
+ ).toBeInTheDocument();
+ expect(screen.getByText("Partner Guardrails")).toBeInTheDocument();
+ expect(
+ screen.getByText("Third-party guardrail integrations from leading AI security providers."),
+ ).toBeInTheDocument();
+ });
+
+ it("should show a capped set of litellm cards behind a show all toggle", async () => {
+ const user = userEvent.setup();
+ renderGarden();
+
+ expect(screen.getByText(`Show all (${LITELLM_CARDS.length})`)).toBeInTheDocument();
+ expect(screen.getByText(LITELLM_CARDS[0].name)).toBeInTheDocument();
+ expect(screen.queryByText(LITELLM_CARDS[LITELLM_CARDS.length - 1].name)).not.toBeInTheDocument();
+
+ await user.click(screen.getByText(`Show all (${LITELLM_CARDS.length})`));
+
+ expect(screen.getByText("Show less")).toBeInTheDocument();
+ expect(screen.getByText(LITELLM_CARDS[LITELLM_CARDS.length - 1].name)).toBeInTheDocument();
+ });
+
+ it("should always render every partner card", () => {
+ renderGarden();
+
+ PARTNER_CARDS.forEach((card) => {
+ expect(screen.getByText(card.name)).toBeInTheDocument();
+ });
+ });
+
+ it("should filter cards by the search query", async () => {
+ const user = userEvent.setup();
+ renderGarden();
+
+ const target = PARTNER_CARDS[0];
+ await user.type(screen.getByPlaceholderText("Search guardrails"), target.name);
+
+ expect(await screen.findByText(target.name)).toBeInTheDocument();
+ const otherPartner = PARTNER_CARDS.find((c) => c.name !== target.name);
+ if (otherPartner) {
+ expect(screen.queryByText(otherPartner.name)).not.toBeInTheDocument();
+ }
+ });
+
+ it("should show an empty result set for a query that matches nothing", async () => {
+ const user = userEvent.setup();
+ renderGarden();
+
+ await user.type(screen.getByPlaceholderText("Search guardrails"), "zzzzznotaguardrailzzzzz");
+
+ expect(screen.getByText("Show all (0)")).toBeInTheDocument();
+ PARTNER_CARDS.forEach((card) => {
+ expect(screen.queryByText(card.name)).not.toBeInTheDocument();
+ });
+ });
+
+ it("should open the detail view for a clicked card and return to the garden", async () => {
+ const user = userEvent.setup();
+ renderGarden();
+
+ const target = PARTNER_CARDS[0];
+ await user.click(screen.getByText(target.name));
+
+ expect(await screen.findByText(`Detail for ${target.name}`)).toBeInTheDocument();
+ expect(screen.queryByPlaceholderText("Search guardrails")).not.toBeInTheDocument();
+
+ await user.click(screen.getByRole("button", { name: "Back to garden" }));
+
+ expect(await screen.findByPlaceholderText("Search guardrails")).toBeInTheDocument();
+ });
+});