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 @@ -954,6 +954,11 @@ const [action, setAction] = useState<GuardrailAction>({
action outside the list never blanks the select. Both builders forward it, and start a new
guardrail on the first allowed type when log is not among them. `GuardrailBuilder`'s takes no
`filter`, which it has no field picker for.
- **`hideLogSeverity` drops the severity select** for a host whose log action has no severity
(Flow's Guardrail node, where log is relabelled Fail). The type select keeps its column, so
it does not move when the user switches to block. `severityLevel` is left as it is: a new
guardrail saves `'Info'`, an edited one its stored value, and the host overwrites it on save
if it needs another. `GuardrailBuilder` forwards it.
- **Asset recipients round-trip.** Types 4 and 6 display as their static siblings (3 and 5) in
the type select, so a value written by a host asset editor never blanks the selection.
- **Recipient types outside 1-6 pass through.** Agents' argument (7, 8) and
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,14 @@ export const AllowedActionTypes: Story = {
render: () => <ActionSectionHost initial={logAction} allowedActionTypes={['log', 'block']} />,
};

/** hideLogSeverity drops the severity select; the action keeps its severityLevel. */
export const LogWithoutSeverity: Story = {
args: { action: logAction, onActionChange: () => {} },
render: () => (
<ActionSectionHost initial={logAction} allowedActionTypes={['log', 'block']} hideLogSeverity />
),
};

/** Filter appears only with showFilter, and its field picker is host content. */
export const FilterWithHostFieldPicker: Story = {
args: { action: { $actionType: 'filter', fields: [] }, onActionChange: () => {} },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,52 @@ describe('GuardrailActionSection', () => {
expect(onActionChange).toHaveBeenCalledWith({ $actionType: 'log', severityLevel: 'Warning' });
});

describe('hideLogSeverity', () => {
it('shows the severity select by default', () => {
render(<GuardrailActionSection action={logAction} onActionChange={vi.fn()} />);

expect(screen.getByRole('combobox', { name: /severity level/i })).toBeInTheDocument();
});

it('hides the severity select for log actions', () => {
render(
<GuardrailActionSection action={logAction} onActionChange={vi.fn()} hideLogSeverity />
);

expect(screen.getByRole('combobox', { name: /action type/i })).toHaveTextContent('Log');
expect(screen.queryByRole('combobox', { name: /severity level/i })).not.toBeInTheDocument();
expect(screen.queryByText('Severity level')).not.toBeInTheDocument();
});

it('still emits a severity level when switching to log', async () => {
const onActionChange = vi.fn();
render(
<GuardrailActionSection
action={{ $actionType: 'block', reason: 'no PII' }}
onActionChange={onActionChange}
hideLogSeverity
/>
);

fireEvent.click(screen.getByRole('combobox', { name: /action type/i }));
fireEvent.click(await screen.findByRole('option', { name: 'Log' }));

expect(onActionChange).toHaveBeenCalledWith({ $actionType: 'log', severityLevel: 'Info' });
});

it('leaves the other action types alone', () => {
render(
<GuardrailActionSection
action={{ $actionType: 'block', reason: '' }}
onActionChange={vi.fn()}
hideLogSeverity
/>
);

expect(screen.getByLabelText(/blocking reason/i)).toBeInTheDocument();
});
});

it('renders the blocking reason input with its error for block actions', () => {
const onActionChange = vi.fn();
render(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ export interface GuardrailActionSectionProps {
* action outside the list never blanks the select.
*/
allowedActionTypes?: readonly GuardrailActionType[];
/**
* Hide the severity level select for a log action, for a host whose log action has no
* severity. The action's `severityLevel` is left as it is, for the host to set.
*/
hideLogSeverity?: boolean;
/** Content rendered as the second grid column when $actionType === 'filter' */
filterContent?: ReactNode;
/** Validation messages; each renders as soon as it is present. */
Expand Down Expand Up @@ -72,6 +77,7 @@ export function GuardrailActionSection({
onActionChange,
showFilter = false,
allowedActionTypes,
hideLogSeverity = false,
filterContent,
errors,
recipientTypeDisabled,
Expand Down Expand Up @@ -150,7 +156,7 @@ export function GuardrailActionSection({
<div className="grid grid-cols-1 @sm:grid-cols-2 gap-3 items-start">
{actionTypeSelect}

{action.$actionType === 'log' && (
{action.$actionType === 'log' && !hideLogSeverity && (
<FormField>
<Label htmlFor={`${uid}-severity-level`}>
{labels.severityLabel}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -381,6 +381,20 @@ export const FooterEvalsToggle: Story = {
args: { ...AddInline.args, evalsTogglePlacement: 'footer' },
};

/**
* A host whose log action has no severity: hideLogSeverity drops the severity select, here with
* log relabelled Fail and block as the only other action.
*/
export const LogWithoutSeverity: Story = {
args: {
...AddInline.args,
scope: 'Llm',
allowedActionTypes: ['log', 'block'],
hideLogSeverity: true,
labels: { actionLogLabel: 'Fail' },
},
};

/** Chrome strings resolved from the canvas lingui catalog (Japanese). */
export const Localized: Story = {
args: { ...AddInline.args },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1648,6 +1648,75 @@ describe('GuardrailBuilder', () => {
expect(screen.getByRole('combobox', { name: /action type/i })).toHaveTextContent('Log');
});

it('shows the log severity select by default', () => {
render(
<GuardrailBuilder
open
inline
hideHeader
definition={makeDef()}
scope="Llm"
defaultName="PII 1"
onSave={vi.fn()}
onCancel={vi.fn()}
/>
);

expect(screen.getByRole('combobox', { name: /severity level/i })).toBeInTheDocument();
});

it('hides the log severity select with hideLogSeverity and saves the default severity', () => {
const onSave = vi.fn();
render(
<GuardrailBuilder
open
inline
hideHeader
definition={makeDef()}
scope="Llm"
defaultName="PII 1"
hideLogSeverity
onSave={onSave}
onCancel={vi.fn()}
/>
);

expect(screen.getByRole('combobox', { name: /action type/i })).toHaveTextContent('Log');
expect(screen.queryByRole('combobox', { name: /severity level/i })).not.toBeInTheDocument();

fireEvent.click(screen.getByRole('button', { name: 'Save' }));
expect(onSave).toHaveBeenCalledWith(
expect.objectContaining({ action: { $actionType: 'log', severityLevel: 'Info' } })
);
});

it('saves an edited guardrail with its stored severity under hideLogSeverity', () => {
const onSave = vi.fn();
render(
<GuardrailBuilder
open
inline
hideHeader
definition={makeDef()}
scope="Llm"
guardrail={makeGuardrail({
selector: { scopes: ['Llm'] },
action: { $actionType: 'log', severityLevel: 'Error' },
})}
hideLogSeverity
onSave={onSave}
onCancel={vi.fn()}
/>
);

expect(screen.queryByRole('combobox', { name: /severity level/i })).not.toBeInTheDocument();

fireEvent.click(screen.getByRole('button', { name: 'Save' }));
expect(onSave).toHaveBeenCalledWith(
expect.objectContaining({ action: { $actionType: 'log', severityLevel: 'Error' } })
);
});

it('saves a recipient type it does not model unchanged', () => {
const onSave = vi.fn();
// Agents' ArgumentEmail, passed in with a cast as the host does.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ export interface GuardrailBuilderProps {
* builder has no field picker to edit it (`CustomGuardrailBuilder` does).
*/
allowedActionTypes?: ReadonlyArray<Exclude<GuardrailAction['$actionType'], 'filter'>>;
/**
* Hide the severity level select for a log action, for a host whose log action has no
* severity. A new guardrail still saves the default `'Info'` and an edited one its stored
* `severityLevel`; the host overwrites it on save if it needs another.
*/
hideLogSeverity?: boolean;
renderRecipientSearch?: (ctx: GuardrailRecipientSearchContext) => ReactNode;
/**
* Replace the editor for static/asset recipients (types 3/4/5/6). Return `undefined` to
Expand Down Expand Up @@ -200,6 +206,7 @@ export function GuardrailBuilder({
policyName,
evalsTogglePlacement = 'form',
allowedActionTypes,
hideLogSeverity,
renderRecipientSearch,
renderStaticRecipient,
renderAppPicker,
Expand Down Expand Up @@ -556,6 +563,7 @@ export function GuardrailBuilder({
action={formData.action}
onActionChange={(action) => updateField('action', action)}
allowedActionTypes={allowedActionTypes}
hideLogSeverity={hideLogSeverity}
errors={{
blockReason: displayErrors.blockReason,
filterFields: displayErrors.filterFields,
Expand Down
Loading