fix(ui): reset unsupported directional scope selections

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
yucheng 2026-09-29 10:47:31 +00:00
parent 6388bdf73e
commit bc423af734
4 changed files with 72 additions and 19 deletions

View file

@ -1,5 +1,5 @@
import React from "react";
import { screen, waitFor } from "@testing-library/react";
import { fireEvent, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { chooseSelectOption, renderWithProviders } from "@/../tests/test-utils";
import { beforeEach, describe, expect, it, vi } from "vitest";
@ -151,6 +151,34 @@ describe("AddGuardrailForm create payload characterization", () => {
expect(screen.getByRole("option", { name: "Both (request and response)" })).toBeInTheDocument();
});
it("clears a selected directional scope when switching to an unsupported provider", async () => {
vi.mocked(networking.getGuardrailUISettings).mockResolvedValue({
...uiSettings,
supported_modes: ["pre_call", "logging_only"],
providers_without_directional_logging_only_scope: ["xecguard"],
});
vi.mocked(networking.getGuardrailProviderSpecificParams).mockResolvedValue({
...providerParams,
xecguard: { ui_friendly_name: "XecGuard" },
});
const user = userEvent.setup({ delay: null });
renderForm();
fireEvent.change(await screen.findByLabelText("Guardrail Name"), { target: { value: "switch-scope" } });
await pickProvider(user, "Bedrock Guardrail");
await user.click(screen.getByLabelText("Mode"));
await user.click((await screen.findAllByText("logging_only")).at(-1) as HTMLElement);
await chooseSelectOption(user, await screen.findByLabelText("Logging only scope"), "Output only (response)");
expect(screen.getByLabelText("Logging only scope")).toHaveTextContent("Output only (response)");
await pickProvider(user, "XecGuard");
await user.click(screen.getByRole("button", { name: "Next" }));
await user.click(await screen.findByRole("button", { name: "Create Guardrail" }));
await waitFor(() => expect(networking.createGuardrailCall).toHaveBeenCalledTimes(1));
expect(payload()?.litellm_params.mode).toContain("logging_only");
expect(payload()?.litellm_params).not.toHaveProperty("logging_only_scope");
});
it("hides logging-only scope and omits it from a pre-call payload", async () => {
const user = userEvent.setup({ delay: null });
renderForm();

View file

@ -243,6 +243,14 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
const providerValue = guardrail_provider_map[selectedProvider];
return (providerValue || "").toLowerCase() === "tool_permission";
}, [selectedProvider]);
const directionalScopeSupported = supportsDirectionalLoggingOnlyScope(guardrailSettings, selectedProvider);
useEffect(() => {
const scopeChoice = form.getValues("logging_only_scope_choice");
if (!directionalScopeSupported && (scopeChoice === "input" || scopeChoice === "output")) {
form.setValue("logging_only_scope_choice", "default");
}
}, [directionalScopeSupported, form]);
// Fetch guardrail UI settings + provider params on mount / accessToken change
useEffect(() => {
@ -690,7 +698,6 @@ const AddGuardrailForm: React.FC<AddGuardrailFormProps> = ({ visible, onClose, a
const providerLabels: Record<string, string> = getGuardrailProviders();
const providerKeys = Object.keys(providerLabels);
const supportedModes = getSupportedModesForProvider(guardrailSettings, selectedProvider) ?? DEFAULT_MODES;
const directionalScopeSupported = supportsDirectionalLoggingOnlyScope(guardrailSettings, selectedProvider);
return (
<FieldGroup>
<GuardrailField

View file

@ -204,6 +204,23 @@ describe("GuardrailInfoView update payload characterization", () => {
expect(lastPayload()).toEqual({ litellm_params: { logging_only_scope: null } });
});
it("clears a stored directional scope for a provider that does not support it", async () => {
vi.mocked(networking.getGuardrailUISettings).mockResolvedValue({
...uiSettings,
providers_without_directional_logging_only_scope: ["bedrock"],
});
vi.mocked(networking.getGuardrailInfo).mockResolvedValue(
guardrail({ guardrailIdentifier: "gr-abc", mode: "logging_only", logging_only_scope: "output" }),
);
const user = userEvent.setup({ delay: null });
renderView();
await openEditor(user);
await saveChanges(user);
await waitFor(() => expect(networking.updateGuardrailCall).toHaveBeenCalledTimes(1));
expect(lastPayload()).toEqual({ litellm_params: { logging_only_scope: null } });
});
it("parses the guardrail information textarea into an object", async () => {
const user = userEvent.setup({ delay: null });
renderView();

View file

@ -38,6 +38,7 @@ import {
getLoggingOnlyScopeUpdate,
getGuardrailLogoAndName,
guardrail_provider_map,
choiceToLoggingOnlyScope,
loggingOnlyScopeToChoice,
skipSystemMessageToChoice,
skipToolMessageToChoice,
@ -224,27 +225,27 @@ const GuardrailInfoView: React.FC<GuardrailInfoProps> = ({ guardrailId, onClose,
// binds are seeded: an unbound key would otherwise be submitted as if the user had set it.
useEffect(() => {
if (!guardrailData) return;
const litellmParams = guardrailData.litellm_params;
form.setValue("guardrail_name", guardrailData.guardrail_name);
form.setValue("default_on", guardrailData.litellm_params?.default_on);
const storedLoggingOnlyScope = guardrailData.litellm_params?.logging_only_scope;
form.setValue("logging_only_scope_choice", loggingOnlyScopeToChoice(storedLoggingOnlyScope));
form.setValue(
"skip_system_message_choice",
skipSystemMessageToChoice(guardrailData.litellm_params?.skip_system_message_in_guardrail),
);
form.setValue(
"skip_tool_message_choice",
skipToolMessageToChoice(guardrailData.litellm_params?.skip_tool_message_in_guardrail),
);
form.setValue(
"guardrail_info",
guardrailData.guardrail_info ? JSON.stringify(guardrailData.guardrail_info, null, 2) : "",
);
if (guardrailData.litellm_params?.optional_params) {
form.setValue("optional_params", guardrailData.litellm_params.optional_params);
form.setValue("default_on", litellmParams?.default_on);
form.setValue("logging_only_scope_choice", loggingOnlyScopeToChoice(litellmParams?.logging_only_scope));
const skipSystemMessageChoice = skipSystemMessageToChoice(litellmParams?.skip_system_message_in_guardrail);
form.setValue("skip_system_message_choice", skipSystemMessageChoice);
form.setValue("skip_tool_message_choice", skipToolMessageToChoice(litellmParams?.skip_tool_message_in_guardrail));
const guardrailInfo = guardrailData.guardrail_info ? JSON.stringify(guardrailData.guardrail_info, null, 2) : "";
form.setValue("guardrail_info", guardrailInfo);
if (litellmParams?.optional_params) {
form.setValue("optional_params", litellmParams.optional_params);
}
}, [guardrailData, guardrailProviderSpecificParams, form]);
useEffect(() => {
const scope = choiceToLoggingOnlyScope(form.getValues("logging_only_scope_choice"));
if (!directionalScopeSupported && scope !== null && scope !== "both") {
form.setValue("logging_only_scope_choice", "default");
}
}, [directionalScopeSupported, form, guardrailData]);
const resetToolPermissionEditor = useCallback(() => {
if (guardrailData?.litellm_params?.guardrail === "tool_permission") {
setToolPermissionConfig({