vitests for GuardrailViewer components
This commit is contained in:
parent
78cb8c71fd
commit
f80660acd9
@ -2879,7 +2879,34 @@ jobs:
|
||||
name: Install Playwright Browsers
|
||||
command: |
|
||||
npx playwright install
|
||||
|
||||
- run:
|
||||
name: Run UI unit tests (Vitest)
|
||||
command: |
|
||||
# Use the same Node version we installed earlier with nvm
|
||||
export NVM_DIR="/opt/circleci/.nvm"
|
||||
source "$NVM_DIR/nvm.sh"
|
||||
nvm use v18.17.0
|
||||
|
||||
cd ui/litellm-dashboard
|
||||
# Ensure clean, deterministic install (skip if you prefer npm install)
|
||||
npm ci || npm install
|
||||
|
||||
# Run Vitest via your package.json script:
|
||||
# - --run to avoid watch mode on CI
|
||||
# - --coverage to produce coverage
|
||||
# - --coverage.reporter=lcov so Codecov can ingest lcov.info
|
||||
# - Feel free to drop --reporter if you don’t need extra output
|
||||
npm run test -- --run --coverage --coverage.reporter=lcov
|
||||
|
||||
# If you use a JUnit reporter (e.g., vitest-junit-reporter),
|
||||
# ensure it writes to ./test-results/junit.xml so Circle collects it
|
||||
mkdir -p test-results || true
|
||||
- store_test_results:
|
||||
path: ui/litellm-dashboard/test-results
|
||||
- store_artifacts:
|
||||
path: ui/litellm-dashboard/coverage
|
||||
destination: ui-coverage
|
||||
|
||||
- run:
|
||||
name: Build Docker image
|
||||
command: docker build -t my-app:latest -f ./docker/Dockerfile.database .
|
||||
|
||||
2153
ui/litellm-dashboard/package-lock.json
generated
2153
ui/litellm-dashboard/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -6,7 +6,9 @@
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"lint": "next lint",
|
||||
"test": "vitest",
|
||||
"test:watch": "vitest -w"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.54.0",
|
||||
@ -40,6 +42,9 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@testing-library/jest-dom": "^6.8.0",
|
||||
"@testing-library/react": "^16.3.0",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/lodash": "^4.17.15",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "18.2.48",
|
||||
@ -50,10 +55,13 @@
|
||||
"autoprefixer": "^10.4.17",
|
||||
"eslint": "^8",
|
||||
"eslint-config-next": "14.2.32",
|
||||
"jsdom": "^27.0.0",
|
||||
"postcss": "^8.4.33",
|
||||
"prettier": "3.2.5",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"typescript": "5.3.3"
|
||||
"typescript": "5.3.3",
|
||||
"vite": "^5.4.20",
|
||||
"vitest": "^1.6.1"
|
||||
},
|
||||
"overrides": {
|
||||
"prismjs": ">=1.30.0",
|
||||
|
||||
@ -0,0 +1,118 @@
|
||||
import React from 'react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import BedrockGuardrailDetails, {
|
||||
BedrockGuardrailResponse,
|
||||
} from '@/components/view_logs/GuardrailViewer/BedrockGuardrailDetails';
|
||||
import { renderWithProviders, screen } from "../../../../tests/test-utils"
|
||||
import {
|
||||
makeAssessment,
|
||||
makeBedrockCoverage,
|
||||
makeBedrockResponse,
|
||||
makeBedrockUsage,
|
||||
} from "@/components/view_logs/GuardrailViewer/__tests__/fixtures"
|
||||
|
||||
describe('BedrockGuardrailDetails', () => {
|
||||
it('returns null when response is falsy', () => {
|
||||
// @ts-expect-error testing nullish handling
|
||||
const { container } = renderWithProviders(<BedrockGuardrailDetails response={undefined} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('renders top summary: action chip, reason, blocked response', () => {
|
||||
const resp: BedrockGuardrailResponse = makeBedrockResponse({
|
||||
action: 'GUARDRAIL_INTERVENED',
|
||||
actionReason: 'Policy violation',
|
||||
blockedResponse: '[blocked]',
|
||||
});
|
||||
renderWithProviders(<BedrockGuardrailDetails response={resp} />);
|
||||
|
||||
expect(screen.getByText('Action:')).toBeInTheDocument();
|
||||
expect(screen.getByText('Policy violation')).toBeInTheDocument();
|
||||
expect(screen.getByText('[blocked]')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders coverage and usage pills', () => {
|
||||
const resp = makeBedrockResponse({
|
||||
guardrailCoverage: makeBedrockCoverage(),
|
||||
usage: makeBedrockUsage({ contentPolicyUnits: 7, wordPolicyUnits: 1 }),
|
||||
});
|
||||
renderWithProviders(<BedrockGuardrailDetails response={resp} />);
|
||||
|
||||
expect(screen.getByText(/text guarded 27\/100/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/images guarded 1\/3/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/contentPolicyUnits: 7/)).toBeInTheDocument();
|
||||
expect(screen.getByText(/wordPolicyUnits: 1/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders outputs when present (prefers `outputs`, falls back to `output`)', () => {
|
||||
// Using outputs
|
||||
let resp = makeBedrockResponse({ outputs: [{ text: 'hello' }] });
|
||||
const { rerender } = renderWithProviders(<BedrockGuardrailDetails response={resp} />);
|
||||
expect(screen.getByText('Outputs')).toBeInTheDocument();
|
||||
expect(screen.getByText('hello')).toBeInTheDocument();
|
||||
|
||||
// Using output
|
||||
resp = makeBedrockResponse({ outputs: undefined, output: [{ text: 'world' }] });
|
||||
rerender(<BedrockGuardrailDetails response={resp} />);
|
||||
expect(screen.getByText('world')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders assessments with all policy sections and metrics', () => {
|
||||
const resp = makeBedrockResponse({
|
||||
assessments: [makeAssessment()],
|
||||
});
|
||||
renderWithProviders(<BedrockGuardrailDetails response={resp} />);
|
||||
|
||||
// Assessment section present
|
||||
expect(screen.getByText('Assessment #1')).toBeInTheDocument();
|
||||
|
||||
// Word policy sections
|
||||
expect(screen.getByText('Word Policy')).toBeInTheDocument();
|
||||
expect(screen.getByText('Custom Words')).toBeInTheDocument();
|
||||
expect(screen.getByText('Managed Word Lists')).toBeInTheDocument();
|
||||
|
||||
// Contextual grounding table headers
|
||||
expect(screen.getByText('Contextual Grounding')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('Score').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('Threshold').length).toBeGreaterThan(0);
|
||||
|
||||
// Sensitive Info sections
|
||||
expect(screen.getByText('Sensitive Information')).toBeInTheDocument();
|
||||
expect(screen.getByText('PII Entities')).toBeInTheDocument();
|
||||
expect(screen.getByText('Custom Regexes')).toBeInTheDocument();
|
||||
|
||||
// Topic Policy
|
||||
expect(screen.getByText('Topic Policy')).toBeInTheDocument();
|
||||
expect(screen.getByText('weapons')).toBeInTheDocument();
|
||||
|
||||
// Invocation Metrics
|
||||
expect(screen.getByText('Invocation Metrics')).toBeInTheDocument();
|
||||
|
||||
// Raw JSON section exists (closed by default)
|
||||
expect(screen.getByText('Raw Bedrock Guardrail Response')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles non-text outputs gracefully', () => {
|
||||
const resp = makeBedrockResponse({ outputs: [{}, { text: 'texty' }] });
|
||||
renderWithProviders(<BedrockGuardrailDetails response={resp} />);
|
||||
expect(screen.getByText('(non-text output)')).toBeInTheDocument();
|
||||
expect(screen.getByText('texty')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('gracefully handles missing optional sections', () => {
|
||||
const resp = makeBedrockResponse({
|
||||
assessments: [
|
||||
{
|
||||
// only include minimal fields; others omitted
|
||||
invocationMetrics: { guardrailProcessingLatency: 5 },
|
||||
} as any,
|
||||
],
|
||||
usage: undefined,
|
||||
guardrailCoverage: undefined,
|
||||
outputs: [],
|
||||
});
|
||||
renderWithProviders(<BedrockGuardrailDetails response={resp} />);
|
||||
// No crash, minimal render: Assessment + Invocation Metrics present, but no usage/coverage chips at top
|
||||
expect(screen.getByText('Assessment #1')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,154 @@
|
||||
import React from 'react';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { renderWithProviders, screen } from "../../../../tests/test-utils"
|
||||
import {
|
||||
makeBedrockResponse, makeEntity,
|
||||
makeGuardrailInformation,
|
||||
} from "@/components/view_logs/GuardrailViewer/__tests__/fixtures"
|
||||
import GuardrailViewer from "@/components/view_logs/GuardrailViewer/GuardrailViewer"
|
||||
|
||||
// We will mock child components selectively for some tests to assert prop passthrough,
|
||||
// but also run an integration-style render without mocks.
|
||||
const PresidioPath = '@/components/view_logs/GuardrailViewer/PresidioDetectedEntities';
|
||||
const BedrockPath = '@/components/view_logs/GuardrailViewer/BedrockGuardrailDetails';
|
||||
|
||||
describe('GuardrailViewer', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetModules();
|
||||
});
|
||||
|
||||
it('shows header, status pill color, duration rounding, and time labels', () => {
|
||||
const data = makeGuardrailInformation({ duration: 1.23456, guardrail_status: 'success' });
|
||||
renderWithProviders(<GuardrailViewer data={data} />);
|
||||
|
||||
expect(screen.getByText('Guardrail Information')).toBeInTheDocument();
|
||||
// header status pill (success => green)
|
||||
const statusBadges = screen.getAllByText('success');
|
||||
// there are two status locations: header chip and grid "Status"
|
||||
expect(statusBadges.length).toBeGreaterThanOrEqual(1);
|
||||
// Quick class assertion for at least one of them
|
||||
expect(statusBadges[0].className).toMatch(/bg-green-100/);
|
||||
|
||||
// duration displays with 4 decimals
|
||||
expect(screen.getByText(/1\.2346s/)).toBeInTheDocument();
|
||||
|
||||
// time labels exist
|
||||
expect(screen.getByText('Start Time:')).toBeInTheDocument();
|
||||
expect(screen.getByText('End Time:')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calculates and displays masked entity totals with pluralization', () => {
|
||||
const data = makeGuardrailInformation({
|
||||
masked_entity_count: { EMAIL_ADDRESS: 2, PHONE_NUMBER: 1 },
|
||||
});
|
||||
renderWithProviders(<GuardrailViewer data={data} />);
|
||||
|
||||
expect(screen.getByText('3 masked entities')).toBeInTheDocument();
|
||||
// summary chips for each entry
|
||||
expect(screen.getByText('EMAIL_ADDRESS: 2')).toBeInTheDocument();
|
||||
expect(screen.getByText('PHONE_NUMBER: 1')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('hides masked badge & summary when count is zero/empty', () => {
|
||||
const data = makeGuardrailInformation({ masked_entity_count: {} });
|
||||
renderWithProviders(<GuardrailViewer data={data} />);
|
||||
|
||||
expect(screen.queryByText(/masked entity/)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Masked Entity Summary')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('toggles main section open/closed and chevron rotation class', async () => {
|
||||
const user = userEvent.setup();
|
||||
const data = makeGuardrailInformation();
|
||||
renderWithProviders(<GuardrailViewer data={data} />);
|
||||
|
||||
const header = screen.getByText('Guardrail Information').closest('div')!;
|
||||
// Initially expanded
|
||||
expect(screen.getByText('Click to collapse')).toBeInTheDocument();
|
||||
// Click to collapse
|
||||
await user.click(header);
|
||||
expect(screen.getByText('Click to expand')).toBeInTheDocument();
|
||||
// Details gone
|
||||
expect(screen.queryByText('Masked Entity Summary')).not.toBeInTheDocument();
|
||||
|
||||
// Click to expand again
|
||||
await user.click(header);
|
||||
expect(screen.getByText('Click to collapse')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('defaults to presidio provider when guardrail_provider is undefined', async () => {
|
||||
vi.doMock(PresidioPath, () => ({
|
||||
__esModule: true,
|
||||
default: ({ entities }: any) => <div data-testid="presidio-mock">presidio {entities?.length}</div>,
|
||||
}));
|
||||
const { default: Component } = await import('@/components/view_logs/GuardrailViewer/GuardrailViewer');
|
||||
|
||||
const data = makeGuardrailInformation({
|
||||
guardrail_provider: undefined,
|
||||
guardrail_response: [makeEntity(), makeEntity()],
|
||||
});
|
||||
renderWithProviders(<Component data={data} />);
|
||||
|
||||
expect(screen.getByTestId('presidio-mock')).toHaveTextContent('presidio 2');
|
||||
});
|
||||
|
||||
it('renders PresidioDetectedEntities when provider="presidio" and response has entities', async () => {
|
||||
vi.doMock(PresidioPath, () => ({
|
||||
__esModule: true,
|
||||
default: ({ entities }: any) => <div data-testid="presidio-mock">count:{entities?.length}</div>,
|
||||
}));
|
||||
const { default: Component } = await import('@/components/view_logs/GuardrailViewer/GuardrailViewer');
|
||||
|
||||
const data = makeGuardrailInformation({
|
||||
guardrail_provider: 'presidio',
|
||||
guardrail_response: [makeEntity()],
|
||||
});
|
||||
renderWithProviders(<Component data={data} />);
|
||||
expect(screen.getByTestId('presidio-mock')).toHaveTextContent('count:1');
|
||||
});
|
||||
|
||||
it('renders BedrockGuardrailDetails when provider="bedrock"', async () => {
|
||||
vi.doMock(BedrockPath, () => ({
|
||||
__esModule: true,
|
||||
default: ({ response }: any) => (
|
||||
<div data-testid="bedrock-mock">{response?.action ?? 'no-action'}</div>
|
||||
),
|
||||
}));
|
||||
const { default: Component } = await import('@/components/view_logs/GuardrailViewer/GuardrailViewer');
|
||||
|
||||
const data = makeGuardrailInformation({
|
||||
guardrail_provider: 'bedrock',
|
||||
guardrail_response: makeBedrockResponse({ action: 'GUARDRAIL_INTERVENED' }),
|
||||
});
|
||||
renderWithProviders(<Component data={data} />);
|
||||
expect(screen.getByTestId('bedrock-mock')).toHaveTextContent('GUARDRAIL_INTERVENED');
|
||||
});
|
||||
|
||||
it('unknown provider renders neither Presidio nor Bedrock details', () => {
|
||||
const data = makeGuardrailInformation({
|
||||
guardrail_provider: 'unknown',
|
||||
});
|
||||
renderWithProviders(<GuardrailViewer data={data} />);
|
||||
// Summary still present
|
||||
expect(screen.getByText('Guardrail Information')).toBeInTheDocument();
|
||||
// No provider sections
|
||||
expect(screen.queryByText(/Detected Entities/)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/Raw Bedrock Guardrail Response/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('integration: renders with real Bedrock details without mocks', () => {
|
||||
const data = makeGuardrailInformation({
|
||||
guardrail_provider: 'bedrock',
|
||||
guardrail_response: makeBedrockResponse({
|
||||
action: 'NONE',
|
||||
outputs: [{ text: 'ok' }],
|
||||
}),
|
||||
});
|
||||
renderWithProviders(<GuardrailViewer data={data} />);
|
||||
|
||||
// Bedrock summary bits
|
||||
expect(screen.getByText('Outputs')).toBeInTheDocument();
|
||||
expect(screen.getByText('ok')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,55 @@
|
||||
import React from 'react';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import PresidioDetectedEntities from '@/components/view_logs/GuardrailViewer/PresidioDetectedEntities';
|
||||
import { renderWithProviders, screen } from "../../../../tests/test-utils"
|
||||
import { makeEntity } from "@/components/view_logs/GuardrailViewer/__tests__/fixtures"
|
||||
|
||||
describe('PresidioDetectedEntities', () => {
|
||||
it('renders null when entities empty', () => {
|
||||
const { container } = renderWithProviders(<PresidioDetectedEntities entities={[]} />);
|
||||
expect(container).toBeEmptyDOMElement();
|
||||
});
|
||||
|
||||
it('renders per-entity header info including score color and position', async () => {
|
||||
const user = userEvent.setup();
|
||||
const e = makeEntity({ start: 10, end: 20, score: 0.92, entity_type: 'EMAIL_ADDRESS' });
|
||||
renderWithProviders(<PresidioDetectedEntities entities={[e]} />);
|
||||
|
||||
// Header row values
|
||||
expect(screen.getByText('EMAIL_ADDRESS')).toBeInTheDocument();
|
||||
expect(screen.getByText(/Score: 0\.92/)).toBeInTheDocument();
|
||||
expect(screen.getByText('Position: 10-20')).toBeInTheDocument();
|
||||
|
||||
// Expand details
|
||||
await user.click(screen.getByText('EMAIL_ADDRESS'));
|
||||
expect(screen.getByText('Entity Type:')).toBeInTheDocument();
|
||||
expect(screen.getByText('Characters 10-20')).toBeInTheDocument();
|
||||
expect(screen.getByText('Confidence:')).toBeInTheDocument();
|
||||
// Recognizer details
|
||||
expect(screen.getByText('EmailRecognizer')).toBeInTheDocument();
|
||||
expect(screen.getByText('email_v1')).toBeInTheDocument();
|
||||
// Explanation
|
||||
expect(screen.getByText('Matched via pattern')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles missing metadata & low scores gracefully', async () => {
|
||||
const user = userEvent.setup();
|
||||
const e = makeEntity({
|
||||
score: 0.3,
|
||||
recognition_metadata: undefined as any,
|
||||
analysis_explanation: null,
|
||||
entity_type: 'NAME',
|
||||
start: 0,
|
||||
end: 0,
|
||||
});
|
||||
renderWithProviders(<PresidioDetectedEntities entities={[e]} />);
|
||||
|
||||
await user.click(screen.getByText('NAME'));
|
||||
// No recognizer/explanation rows
|
||||
expect(screen.queryByText('Recognizer:')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Explanation:')).not.toBeInTheDocument();
|
||||
// Position still renders
|
||||
expect(screen.getByText('Characters 0-0')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,119 @@
|
||||
import type {
|
||||
BedrockGuardrailResponse,
|
||||
BedrockAssessment,
|
||||
BedrockGuardrailCoverage,
|
||||
BedrockGuardrailUsage,
|
||||
} from '@/components/view_logs/GuardrailViewer/BedrockGuardrailDetails';
|
||||
|
||||
export interface RecognitionMetadata {
|
||||
recognizer_name: string;
|
||||
recognizer_identifier: string;
|
||||
}
|
||||
|
||||
export interface GuardrailEntity {
|
||||
end: number;
|
||||
score: number;
|
||||
start: number;
|
||||
entity_type: string;
|
||||
analysis_explanation: string | null;
|
||||
recognition_metadata: RecognitionMetadata;
|
||||
}
|
||||
|
||||
export interface GuardrailInformation {
|
||||
duration: number;
|
||||
end_time: number;
|
||||
start_time: number;
|
||||
guardrail_mode: string;
|
||||
guardrail_name: string;
|
||||
guardrail_status: string;
|
||||
guardrail_response: GuardrailEntity[] | BedrockGuardrailResponse;
|
||||
masked_entity_count: Record<string, number>;
|
||||
guardrail_provider?: string;
|
||||
}
|
||||
|
||||
// ===== Builders =====
|
||||
export const makeEntity = (overrides: Partial<GuardrailEntity> = {}): GuardrailEntity => ({
|
||||
end: 18,
|
||||
start: 5,
|
||||
score: 0.92,
|
||||
entity_type: 'EMAIL_ADDRESS',
|
||||
analysis_explanation: 'Matched via pattern',
|
||||
recognition_metadata: {
|
||||
recognizer_name: 'EmailRecognizer',
|
||||
recognizer_identifier: 'email_v1',
|
||||
},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
export const makeMaskedCounts = (overrides: Record<string, number> = {}) => ({
|
||||
EMAIL_ADDRESS: 2,
|
||||
PHONE_NUMBER: 1,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
export const makeBedrockUsage = (overrides: Partial<BedrockGuardrailUsage> = {}): BedrockGuardrailUsage => ({
|
||||
contentPolicyUnits: 4,
|
||||
topicPolicyUnits: 2,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
export const makeBedrockCoverage = (
|
||||
overrides: Partial<BedrockGuardrailCoverage> = {}
|
||||
): BedrockGuardrailCoverage => ({
|
||||
textCharacters: { guarded: 27, total: 100 },
|
||||
images: { guarded: 1, total: 3 },
|
||||
...overrides,
|
||||
});
|
||||
|
||||
export const makeAssessment = (overrides: Partial<BedrockAssessment> = {}): BedrockAssessment => ({
|
||||
wordPolicy: {
|
||||
customWords: [{ action: 'BLOCKED', detected: true, match: 'badword' }],
|
||||
managedWordLists: [{ action: 'ALLOWED', detected: false, match: 'ok', type: 'PROFANITY' }],
|
||||
},
|
||||
contentPolicy: {
|
||||
filters: [
|
||||
{ type: 'HATE', action: 'BLOCKED', detected: true, filterStrength: 'HIGH', confidence: 'MEDIUM' },
|
||||
{ type: 'VIOLENCE', action: 'NONE', detected: false, filterStrength: 'LOW', confidence: 'LOW' },
|
||||
],
|
||||
},
|
||||
topicPolicy: { topics: [{ name: 'weapons', type: 'DENY', detected: true, action: 'BLOCKED' }] },
|
||||
sensitiveInformationPolicy: {
|
||||
piiEntities: [{ type: 'EMAIL', match: 'x@y.com', detected: true, action: 'ANONYMIZED' }],
|
||||
regexes: [{ name: 'ticket', regex: '#[0-9]+', match: '#123', detected: true, action: 'BLOCKED' }],
|
||||
},
|
||||
contextualGroundingPolicy: {
|
||||
filters: [{ type: 'GROUNDING', action: 'BLOCKED', detected: true, score: 0.2, threshold: 0.5 }],
|
||||
},
|
||||
automatedReasoningPolicy: { findings: [{ foo: 'bar' }] },
|
||||
invocationMetrics: {
|
||||
guardrailProcessingLatency: 42,
|
||||
usage: makeBedrockUsage(),
|
||||
guardrailCoverage: makeBedrockCoverage(),
|
||||
},
|
||||
...overrides,
|
||||
});
|
||||
|
||||
export const makeBedrockResponse = (
|
||||
overrides: Partial<BedrockGuardrailResponse> = {}
|
||||
): BedrockGuardrailResponse => ({
|
||||
action: 'NONE',
|
||||
outputs: [{ text: 'ok' }],
|
||||
usage: makeBedrockUsage(),
|
||||
guardrailCoverage: makeBedrockCoverage(),
|
||||
assessments: [makeAssessment()],
|
||||
...overrides,
|
||||
});
|
||||
|
||||
export const makeGuardrailInformation = (
|
||||
overrides: Partial<GuardrailInformation> = {}
|
||||
): GuardrailInformation => ({
|
||||
guardrail_name: 'pii-rail',
|
||||
guardrail_mode: 'post',
|
||||
guardrail_status: 'success',
|
||||
start_time: 1_700_000_000,
|
||||
end_time: 1_700_000_123,
|
||||
duration: 0.123456,
|
||||
guardrail_response: [makeEntity()],
|
||||
masked_entity_count: makeMaskedCounts(),
|
||||
...overrides,
|
||||
});
|
||||
15
ui/litellm-dashboard/tests/setupTests.ts
Normal file
15
ui/litellm-dashboard/tests/setupTests.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { afterEach, vi } from 'vitest';
|
||||
import { cleanup } from '@testing-library/react';
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
// Make toLocaleString deterministic in tests; individual tests can override
|
||||
// This returns ISO-like strings to keep assertions stable.
|
||||
vi.spyOn(Date.prototype, 'toLocaleString').mockImplementation(function (this: Date, ..._args: unknown[]) {
|
||||
const d = this;
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||
});
|
||||
12
ui/litellm-dashboard/tests/test-utils.tsx
Normal file
12
ui/litellm-dashboard/tests/test-utils.tsx
Normal file
@ -0,0 +1,12 @@
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import { render, RenderOptions } from '@testing-library/react';
|
||||
|
||||
const Providers: React.FC<PropsWithChildren> = ({ children }) => {
|
||||
// Add future providers here (Theme/Router/QueryClient/etc.)
|
||||
return <>{children}</>;
|
||||
};
|
||||
|
||||
export const renderWithProviders = (ui: React.ReactElement, options?: RenderOptions) =>
|
||||
render(ui, { wrapper: Providers, ...options });
|
||||
|
||||
export * from '@testing-library/react';
|
||||
17
ui/litellm-dashboard/vitest.config.ts
Normal file
17
ui/litellm-dashboard/vitest.config.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { defineConfig } from 'vitest/config'
|
||||
import { resolve } from "path"
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
setupFiles: ['tests/setupTests.ts'],
|
||||
globals: true,
|
||||
css: true, // lets you import CSS/modules without extra mocks
|
||||
coverage: { reporter: ['text', 'lcov'] },
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, 'src'),
|
||||
},
|
||||
},
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user