fix(ui): gate the pass-through guardrail field inputs when the section is disabled (#37435)

PassThroughGuardrailsSection threads its `disabled` prop to the guardrail
selector and to all three quick-add buttons, but not to the two `TagsInput`
controls for request and response field targeting. Every other prop crossed,
so the section greys out while a user can still type field names straight
into both tag inputs.

Latent rather than live: neither call site passes `disabled` today, so the
prop is currently dead across the tree. This arms the gate for whoever passes
it first.

`TagsInput` already honours `disabled` end to end, measured rather than read:
with it set, the input carries a real disabled attribute and all three commit
paths, token separator, Enter and blur, are inert. So the fix belongs at the
call sites and the primitive needs no change.

The new test drives the control rather than asserting an attribute, since a
Base UI control can paint a disabled state while still accepting input. The
enabled cases are the liveness gate: they type the same text through the same
gesture and assert it commits, so the disabled cases cannot pass vacuously.
This commit is contained in:
yuneng-jiang 2026-08-19 00:00:33 -07:00 • committed by GitHub
parent 0700b1e54e
commit c696fdfb05
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 63 additions and 0 deletions

View file

@ -0,0 +1,61 @@
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { beforeEach, describe, expect, it, vi } from "vitest";
import PassThroughGuardrailsSection from "./PassThroughGuardrailsSection";
vi.mock("../networking", async (importOriginal) => ({
...(await importOriginal<typeof import("../networking")>()),
getGuardrailsList: vi.fn(async () => ({ guardrails: [{ guardrail_name: "pii-guard" }] })),
}));
const FIELDS = [
{ label: "Request Fields", matcher: /Request Fields/, payloadKey: "request_fields", typed: "query" },
{ label: "Response Fields", matcher: /Response Fields/, payloadKey: "response_fields", typed: "choices.content" },
] as const;
const renderSection = (disabled: boolean) => {
const onChange = vi.fn();
render(
<PassThroughGuardrailsSection
accessToken="test-token"
value={{ "pii-guard": null }}
onChange={onChange}
disabled={disabled}
/>,
);
return onChange;
};
const fieldInput = (matcher: RegExp) => screen.getByLabelText(matcher) as HTMLInputElement;
describe("PassThroughGuardrailsSection field targeting", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it.each(FIELDS)(
"commits a typed $label entry while the section is enabled",
async ({ matcher, payloadKey, typed }) => {
const user = userEvent.setup();
const onChange = renderSection(false);
await user.type(fieldInput(matcher), `${typed},`);
expect(onChange.mock.calls.at(-1)?.[0]).toStrictEqual({ "pii-guard": { [payloadKey]: [typed] } });
},
);
it.each(FIELDS)("refuses typed $label input while the section is disabled", async ({ matcher, typed }) => {
const user = userEvent.setup();
const onChange = renderSection(true);
const input = fieldInput(matcher);
await user.type(input, `${typed},`);
await user.type(input, "sneaked-in{Enter}");
await user.tab();
expect(input.value).toBe("");
expect(onChange).not.toHaveBeenCalled();
});
});

View file

@ -174,6 +174,7 @@ const PassThroughGuardrailsSection: React.FC<PassThroughGuardrailsSectionProps>
value={value[guardrailName]?.request_fields ?? []}
onValueChange={(fields) => handleFieldChange(guardrailName, "request_fields", fields)}
tokenSeparators={[","]}
disabled={disabled}
/>
</Field>
<Field>
@ -212,6 +213,7 @@ const PassThroughGuardrailsSection: React.FC<PassThroughGuardrailsSectionProps>
value={value[guardrailName]?.response_fields ?? []}
onValueChange={(fields) => handleFieldChange(guardrailName, "response_fields", fields)}
tokenSeparators={[","]}
disabled={disabled}
/>
</Field>
</div>