Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import type { Verdict } from '@studio/api/guardrail-checks/types';
import { ArrowRight, Clock, ShieldCheck } from 'lucide-react';
import type { FC } from 'react';

/** Solid status badge for a check's latest-run verdict (purple guarded / green allowed). */
/** Solid status badge for a check's latest-run verdict (yellow guarded / green allowed). */
export const ResultIndicator: FC<{ status: Verdict | undefined }> = ({ status }) => {
if (status === 'blocked') {
return (
<Badge color="purple" kind="solid">
<Badge color="yellow" kind="solid">
<ShieldCheck size={14} />
Guarded
</Badge>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,8 @@ interface ResultSummaryProps {
checks: GuardrailCheckEntity[];
}

/** Segment/legend colors for the result summary: purple guarded, green allowed, gray not-run. */
const GUARDED_BG = 'bg-[var(--color-purple-600)]';
const ALLOWED_BG = 'bg-[var(--color-green-200)]';
const GUARDED_BG = 'bg-[var(--text-color-feedback-warning)]';
const ALLOWED_BG = 'bg-[var(--text-color-brand)]';
const NOTRUN_BG = 'bg-[var(--color-gray-200)]';

/** Count of checks by their latest-run verdict: allowed, guarded, or never run. */
Expand Down Expand Up @@ -71,10 +70,10 @@ export const ResultSummary: FC<ResultSummaryProps> = ({ checks }) => {
return (
<Panel slotHeading="Result Summary">
<Stack gap="density-lg">
{/* Left → right: guarded (purple), allowed (green), not-run (gray) at the end. */}
<Flex
className=" h-2 overflow-hidden rounded-full bg-surface-sunken"
className=" h-3 overflow-hidden rounded-full bg-surface-sunken"
role="img"
gap="0.5"
aria-label={`${guarded} guarded, ${allowed} allowed, ${notRun} not run`}
>
<BarSegment colorClassName={GUARDED_BG} pct={pct(guarded)} />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import type { GuardrailCheckMessage } from '@studio/api/guardrail-checks/types';

/** Coerce a message's `content` (string or content-part array) to plain display text. */
const textFromContent = (content: unknown): string => {
export const textFromContent = (content: unknown): string => {
if (typeof content === 'string') {
return content;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

import { DEFAULT_PAGE_SIZE } from '@nemo/common/src/constants/pagination';
import {
GUARDRAIL_CHECKS_ENTITY_TYPE,
type GuardrailCheckEntity,
type Verdict,
} from '@studio/api/guardrail-checks/types';
import { GuardrailChecksDataView } from '@studio/components/dataViews/GuardrailChecksDataView';
import {
type GuardrailCheckDetail,
GuardrailChecksDataView,
type GuardrailChecksDataViewProps,
} from '@studio/components/dataViews/GuardrailChecksDataView';
import { XL_SELECTOR_TIMEOUT } from '@studio/tests/util/constants';
import { TestProviders } from '@studio/tests/util/TestProviders';
import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { type FC, useState } from 'react';
import { createMemoryRouter, RouterProvider } from 'react-router';

const makeCheck = ({
Expand Down Expand Up @@ -61,9 +67,44 @@ const NOT_RUN = makeCheck({ id: 'chk-not-run', input: 'Hello there' });

const CHECKS = [GUARDED, ALLOWED, NOT_RUN];

const renderComponent = (checks: GuardrailCheckEntity[] = CHECKS) => {
/** Straddles ALLOWED, so filtering to Guarded leaves a gap in the underlying array. */
const SECOND_GUARDED = makeCheck({
id: 'chk-guarded-2',
input: 'My card number is 4111 1111 1111 1111',
output: 'I cannot help with that either',
status: 'blocked',
});
const CHECKS_WITH_GAP = [GUARDED, ALLOWED, SECOND_GUARDED];

/** Renders the detail context as text, so assertions can read it off the DOM. */
const DetailProbe: FC<GuardrailCheckDetail> = ({
check,
checkIndex,
visibleIndex,
visibleCount,
onNavigate,
}) => (
<div>
<span data-testid="detail-id">{check.id}</span>
<span data-testid="detail-number">Test {checkIndex + 1}</span>
<span data-testid="detail-position">
{visibleIndex === null ? 'not shown' : `${visibleIndex + 1} of ${visibleCount}`}
</span>
<button type="button" onClick={() => onNavigate((visibleIndex ?? 0) + 1)}>
next
</button>
<button type="button" onClick={() => onNavigate(visibleCount - 1)}>
last
</button>
</div>
);

const renderComponent = (
checks: GuardrailCheckEntity[] = CHECKS,
renderDetail?: GuardrailChecksDataViewProps['renderDetail']
) => {
const router = createMemoryRouter([
{ path: '/', element: <GuardrailChecksDataView checks={checks} /> },
{ path: '/', element: <GuardrailChecksDataView checks={checks} renderDetail={renderDetail} /> },
]);

return render(
Expand All @@ -73,6 +114,15 @@ const renderComponent = (checks: GuardrailCheckEntity[] = CHECKS) => {
);
};

const renderWithDetail = (checks: GuardrailCheckEntity[] = CHECKS) =>
renderComponent(checks, (detail) => <DetailProbe {...detail} />);

const filterToGuarded = async () => {
fireEvent.click(screen.getByTestId('open-filters-button'));
fireEvent.click(await screen.findByTestId('column-filter-status'));
fireEvent.click(await screen.findByRole('option', { name: 'Guarded' }));
};

describe('GuardrailChecksDataView', () => {
it('renders a row per check with its input and output', async () => {
renderComponent();
Expand Down Expand Up @@ -187,4 +237,132 @@ describe('GuardrailChecksDataView', () => {
).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Clear Filters/i })).toBeInTheDocument();
});

describe('detail selection', () => {
it('renders no detail until a row is clicked', async () => {
renderWithDetail();

await screen.findByText('Hello there', undefined, { timeout: XL_SELECTOR_TIMEOUT });
expect(screen.queryByTestId('detail-id')).not.toBeInTheDocument();
});

it('numbers the clicked row by its place in the full list, not the visible one', async () => {
renderWithDetail(CHECKS_WITH_GAP);

await screen.findByText('What is the weather today', undefined, {
timeout: XL_SELECTOR_TIMEOUT,
});
await filterToGuarded();
await waitFor(
() => expect(screen.queryByText('What is the weather today')).not.toBeInTheDocument(),
{ timeout: XL_SELECTOR_TIMEOUT }
);

fireEvent.click(screen.getByText('My card number is 4111 1111 1111 1111'));

// Second of the two visible rows, but still the third test overall.
expect(screen.getByTestId('detail-position')).toHaveTextContent('2 of 2');
expect(screen.getByTestId('detail-number')).toHaveTextContent('Test 3');
});

it('walks the rows the table is showing, skipping filtered-out checks', async () => {
renderWithDetail(CHECKS_WITH_GAP);

await screen.findByText('What is the weather today', undefined, {
timeout: XL_SELECTOR_TIMEOUT,
});
await filterToGuarded();
await waitFor(
() => expect(screen.queryByText('What is the weather today')).not.toBeInTheDocument(),
{ timeout: XL_SELECTOR_TIMEOUT }
);

fireEvent.click(screen.getByText('My SSN is 123-45-6789'));
expect(screen.getByTestId('detail-id')).toHaveTextContent('chk-guarded');
expect(screen.getByTestId('detail-position')).toHaveTextContent('1 of 2');

fireEvent.click(screen.getByRole('button', { name: 'next' }));

// The next *visible* row, not checks[1] (chk-allowed) — which the filter hid.
expect(screen.getByTestId('detail-id')).toHaveTextContent('chk-guarded-2');
expect(screen.getByTestId('detail-position')).toHaveTextContent('2 of 2');
});

it('drops navigation when the selected check is filtered out from under it', async () => {
renderWithDetail(CHECKS_WITH_GAP);

await screen.findByText('What is the weather today', undefined, {
timeout: XL_SELECTOR_TIMEOUT,
});
fireEvent.click(screen.getByText('What is the weather today'));
expect(screen.getByTestId('detail-position')).toHaveTextContent('2 of 3');

await filterToGuarded();

// The check leaves the table but stays on screen; only its position is gone.
await waitFor(
() => expect(screen.getByTestId('detail-position')).toHaveTextContent('not shown'),
{ timeout: XL_SELECTOR_TIMEOUT }
);
expect(screen.getByTestId('detail-id')).toHaveTextContent('chk-allowed');
});

it('pages the table along when navigation crosses a page boundary', async () => {
// One more check than fits on a page, so the last row starts off-screen.
const many = Array.from({ length: DEFAULT_PAGE_SIZE + 1 }, (_, i) =>
makeCheck({ id: `chk-${i}`, input: `Test input ${i}` })
);
const lastInput = `Test input ${DEFAULT_PAGE_SIZE}`;
renderWithDetail(many);

await screen.findByText('Test input 0', undefined, { timeout: XL_SELECTOR_TIMEOUT });
expect(screen.queryByText(lastInput)).not.toBeInTheDocument();

fireEvent.click(screen.getByText('Test input 0'));
fireEvent.click(screen.getByRole('button', { name: 'last' }));

expect(screen.getByTestId('detail-id')).toHaveTextContent(`chk-${DEFAULT_PAGE_SIZE}`);
// The table follows, so the detailed row is the one sitting behind the panel.
expect(
await screen.findByText(lastInput, undefined, { timeout: XL_SELECTOR_TIMEOUT })
).toBeInTheDocument();
});

it('keeps the detail on the same check when a refetch reorders the list', async () => {
// Running the tests invalidates the checks query, so neither the array
// identity nor its order is guaranteed to survive.
const Harness: FC = () => {
const [checks, setChecks] = useState(CHECKS);
return (
<>
<button type="button" onClick={() => setChecks([NOT_RUN, GUARDED, ALLOWED])}>
refetch
</button>
<GuardrailChecksDataView
checks={checks}
renderDetail={(detail) => <DetailProbe {...detail} />}
/>
</>
);
};

const router = createMemoryRouter([{ path: '/', element: <Harness /> }]);
render(
<TestProviders>
<RouterProvider router={router} />
</TestProviders>
);

await screen.findByText('Hello there', undefined, { timeout: XL_SELECTOR_TIMEOUT });
fireEvent.click(screen.getByText('My SSN is 123-45-6789'));
expect(screen.getByTestId('detail-id')).toHaveTextContent('chk-guarded');
expect(screen.getByTestId('detail-number')).toHaveTextContent('Test 1');

fireEvent.click(screen.getByRole('button', { name: 'refetch' }));

// Same check, renumbered — not whatever slid into the position it held.
expect(screen.getByTestId('detail-id')).toHaveTextContent('chk-guarded');
expect(screen.getByTestId('detail-number')).toHaveTextContent('Test 2');
});
});
});
Loading
Loading