fix(ui): anchor guardrail lifecycle on timed entries and show not_run skip reason

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yucheng 2026-09-14 18:39:10 +00:00
parent 4be84a859d
commit e41faf54a8
3 changed files with 62 additions and 14 deletions

View file

@ -49,17 +49,55 @@ describe("GuardrailViewer", () => {
expect(screen.queryByText("FAILED")).not.toBeInTheDocument();
});
it("renders not_run as NOT RUN (muted) and keeps it out of the evaluated and passed counts", () => {
const data = makeGuardrailInformation({ guardrail_status: "not_run", guardrail_mode: "pre_call" });
it("renders not_run as NOT RUN (muted) and keeps it out of the evaluated and passed counts", async () => {
const user = userEvent.setup();
const data = makeGuardrailInformation({
guardrail_status: "not_run",
guardrail_mode: "pre_call",
guardrail_response: "no scannable content after message scoping",
start_time: null,
end_time: null,
duration: null,
});
renderWithProviders(<GuardrailViewer data={data} />);
expect(screen.getByText(/0 guardrails evaluated/)).toBeInTheDocument();
expect(screen.getByText(/0 Passed/)).toHaveClass("text-muted-foreground");
expect(screen.getByText(/1 Not run/)).toBeInTheDocument();
const badges = screen.getAllByText("NOT RUN");
expect(badges).toHaveLength(2);
expect(badges[0]).toHaveClass("text-muted-foreground");
const badge = screen.getByText("NOT RUN");
expect(badge).toHaveClass("text-muted-foreground");
expect(screen.queryByText("FAILED")).not.toBeInTheDocument();
expect(screen.queryByText(/^T\+/)).not.toBeInTheDocument();
await user.click(screen.getByText("pii-rail"));
expect(screen.getByText("no scannable content after message scoping")).toBeInTheDocument();
});
it("anchors the lifecycle timeline on timed entries when an untimed not_run entry sorts first", () => {
const skipped = makeGuardrailInformation({
guardrail_name: "skipped-rail",
guardrail_status: "not_run",
guardrail_mode: "pre_call",
start_time: null,
end_time: null,
duration: null,
});
const ran = makeGuardrailInformation({
guardrail_name: "ran-rail",
guardrail_status: "success",
guardrail_mode: "post_call",
start_time: 1_700_000_000,
end_time: 1_700_000_000.25,
duration: 0.25,
});
renderWithProviders(<GuardrailViewer data={[skipped, ran]} />);
expect(screen.getByText(/1 guardrail evaluated/)).toBeInTheDocument();
expect(screen.getByText("Request received").parentElement).toHaveTextContent("T+0ms");
expect(screen.getByText(/Post-call guardrail: ran-rail/).parentElement).toHaveTextContent("T+250ms");
expect(screen.getByText("Response returned").parentElement).toHaveTextContent("T+251ms");
expect(screen.queryByText(/Pre-call guardrail: skipped-rail/)).not.toBeInTheDocument();
expect(screen.getByText("—")).toBeInTheDocument();
});
it("calculates and displays masked entity totals", async () => {

View file

@ -38,9 +38,9 @@ interface MatchDetail {
}
interface GuardrailInformation {
duration: number;
end_time: number;
start_time: number;
duration: number | null;
end_time: number | null;
start_time: number | null;
guardrail_mode: string | string[] | Record<string, unknown> | null;
guardrail_name: string;
guardrail_status: string;
@ -121,7 +121,8 @@ const formatMode = (mode: GuardrailInformation["guardrail_mode"]): string => {
return s.replace(/_/g, "-").toUpperCase();
};
const formatDurationMs = (seconds: number): string => {
const formatDurationMs = (seconds: number | null): string => {
if (seconds == null) return "—";
const ms = Math.round(seconds * 1000);
return `${ms}ms`;
};
@ -364,8 +365,13 @@ interface TimelineEntry {
outcome?: EntryOutcome;
}
type TimedGuardrailInformation = GuardrailInformation & { start_time: number; end_time: number };
const isTimed = (e: GuardrailInformation): e is TimedGuardrailInformation =>
typeof e.start_time === "number" && typeof e.end_time === "number";
const RequestLifecycle = ({ entries }: { entries: GuardrailInformation[] }) => {
const sorted = useMemo(() => [...entries].sort((a, b) => (a.start_time ?? 0) - (b.start_time ?? 0)), [entries]);
const sorted = useMemo(() => entries.filter(isTimed).sort((a, b) => a.start_time - b.start_time), [entries]);
const timeline = useMemo(() => {
if (sorted.length === 0) return [];
@ -669,6 +675,10 @@ const EvaluationCard = ({ entry }: { entry: GuardrailInformation }) => {
</div>
)}
{outcome === "not_run" && typeof guardrailResponse === "string" && (
<p className="text-sm text-muted-foreground">{guardrailResponse}</p>
)}
{/* Provider-specific details */}
{guardrailProvider === "presidio" && presidioEntities.length > 0 && (
<div className="mt-3">

View file

@ -20,13 +20,13 @@ export interface GuardrailEntity {
}
export interface GuardrailInformation {
duration: number;
end_time: number;
start_time: number;
duration: number | null;
end_time: number | null;
start_time: number | null;
guardrail_mode: string | string[] | Record<string, unknown> | null;
guardrail_name: string;
guardrail_status: string;
guardrail_response: GuardrailEntity[] | BedrockGuardrailResponse;
guardrail_response: GuardrailEntity[] | BedrockGuardrailResponse | string;
masked_entity_count: Record<string, number>;
guardrail_usage?: Record<string, number>;
guardrail_cost?: number;