test(ui): characterise guardrails route behaviour before shadcn migration

Rewrite the PatternModal test off antd class selectors onto role and text
queries, and add characterisation tests for the guardrails components that
had none: the keyword modal, the content filter display and configuration,
the guardrail garden and the custom code modal.

Also covers two behaviours the migration must preserve: the action and
severity dropdowns in the content filter tables, and test playground state
surviving a tab switch away and back.

Every assertion here passes against the current antd and Tremor components
so the same file can prove the shadcn versions unedited.
This commit is contained in:
Yuneng Jiang 2026-08-13 13:45:33 -07:00
parent c1310de342
commit 8ada34fe2c
No known key found for this signature in database
9 changed files with 763 additions and 74 deletions

View file

@ -35,10 +35,19 @@ vi.mock("./guardrail_info", () => ({
default: () => <div>Mock Guardrail Info View</div>,
}));
vi.mock("./GuardrailTestPlayground", () => ({
__esModule: true,
default: () => <div>Mock Guardrail Test Playground</div>,
}));
vi.mock("./GuardrailTestPlayground", async () => {
const { useState } = await import("react");
const MockGuardrailTestPlayground = () => {
const [draft, setDraft] = useState("");
return (
<div>
<div>Mock Guardrail Test Playground</div>
<input aria-label="playground draft" value={draft} onChange={(e) => setDraft(e.target.value)} />
</div>
);
};
return { __esModule: true, default: MockGuardrailTestPlayground };
});
vi.mock("./TeamGuardrailsTab", () => ({
TeamGuardrailsTab: () => <div>Mock Team Guardrails Tab</div>,
@ -129,6 +138,21 @@ describe("GuardrailsPanel", () => {
expect(mockGetGuardrailsList).toHaveBeenCalledTimes(2);
});
it("should keep test playground state when switching tabs away and back", async () => {
render(<GuardrailsPanel {...defaultProps} />);
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(<GuardrailsPanel {...defaultProps} />);
fireEvent.click(screen.getByText("Guardrails"));

View file

@ -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(
<ContentFilterConfiguration
prebuiltPatterns={PREBUILT}
categories={["PII Patterns"]}
selectedPatterns={[]}
blockedWords={[]}
accessToken="test-token"
{...handlers}
{...overrides}
/>,
);
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();
});
});

View file

@ -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(<ContentFilterDisplay patterns={[]} blockedWords={[]} categories={[]} />);
expect(container).toBeEmptyDOMElement();
});
it("should render the categories section with a configured count", () => {
renderWithProviders(<ContentFilterDisplay patterns={[]} blockedWords={[]} categories={[CATEGORY]} />);
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(<ContentFilterDisplay patterns={[PATTERN]} blockedWords={[]} categories={[]} />);
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(<ContentFilterDisplay patterns={[]} blockedWords={[KEYWORD]} categories={[]} />);
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(<ContentFilterDisplay patterns={[PATTERN]} blockedWords={[KEYWORD]} categories={[CATEGORY]} />);
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(
<ContentFilterDisplay patterns={[PATTERN]} blockedWords={[KEYWORD]} categories={[CATEGORY]} readOnly={true} />,
);
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(
<ContentFilterDisplay patterns={[PATTERN]} blockedWords={[KEYWORD]} categories={[CATEGORY]} readOnly={false} />,
);
expect(screen.queryByText("HIGH")).not.toBeInTheDocument();
expect(screen.getAllByRole("button", { name: /delete/i })).toHaveLength(3);
});
});

View file

@ -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<typeof import("antd")>();
return {
...actual,
Divider: ({ children }: { children: React.ReactNode }) => <div data-testid="divider">{children}</div>,
Alert: ({ message, type }: { message: React.ReactNode; type: string }) => (
<div data-testid="unsaved-alert" data-type={type}>
{message}
</div>
),
};
});
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();

View file

@ -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(
<PatternTable
patterns={[{ id: "pattern-1", type: "prebuilt", name: "email", action: "BLOCK" }]}
onActionChange={onActionChange}
onRemove={vi.fn()}
/>,
);
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(
<KeywordTable
keywords={[{ id: "keyword-1", keyword: "secret", action: "BLOCK" }]}
onActionChange={onActionChange}
onRemove={vi.fn()}
/>,
);
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(
<CategoryTable
categories={[
{
id: "category-1",
category: "self_harm",
display_name: "Self Harm",
action: "BLOCK",
severity_threshold: "high",
},
]}
onActionChange={onActionChange}
onSeverityChange={onSeverityChange}
onRemove={vi.fn()}
/>,
);
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");
});
});

View file

@ -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<React.ComponentProps<typeof KeywordModal>> = {}) =>
render(<KeywordModal visible keyword="" action="BLOCK" description="" {...handlers} {...overrides} />);
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();
});
});

View file

@ -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(
<PatternModal
visible={true}
@ -43,49 +43,75 @@ describe("PatternModal", () => {
/>,
);
// 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(
<PatternModal
visible={false}
prebuiltPatterns={mockPrebuiltPatterns}
categories={mockCategories}
selectedPatternName=""
patternAction="BLOCK"
onPatternNameChange={mockOnPatternNameChange}
onActionChange={mockOnActionChange}
onAdd={mockOnAdd}
onCancel={mockOnCancel}
/>,
);
expect(screen.queryByText("Add prebuilt pattern")).not.toBeInTheDocument();
});
});

View file

@ -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(<CustomCodeModal visible onClose={onClose} onSuccess={onSuccess} accessToken="test-token" {...overrides} />);
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<string, never>];
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);
});
});

View file

@ -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 }) => (
<div>
<span>Detail for {card.name}</span>
<button onClick={onBack}>Back to garden</button>
</div>
),
}));
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(<GuardrailGarden accessToken="test-token" onGuardrailCreated={vi.fn()} />);
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();
});
});