From b4e90c426d525abbe41f27774080b34183e1af8f Mon Sep 17 00:00:00 2001 From: andreizdrali-uipath Date: Wed, 16 Sep 2026 12:10:33 +0300 Subject: [PATCH] feat(apollo-react): shared guardrail definitions layer and useGuardrailDefinitions [AL-574] Turns the `GET /api/execution/guardrails/definitions` payload into the `GuardrailDefinition`s `GuardrailBuilder` renders. Flow and Agents each carry their own copy of this today, and the two have drifted. - `definitions-wire.ts` mirrors the payload by hand, admitting both products' nullability variants, and reuses `GuardrailScope`/`GuardrailDefinitionStatus` so wire and display cannot drift. - `definitions-parse.ts` validates with zod and never throws: a non-array sets `inputError`, one bad definition is dropped whole into `invalid`. Blank display strings are the single normalization, since one would beat curated copy and render an empty label; blank identifiers fail the entry instead of silently changing a definition's identity. zod stays private, pinned to the hand-written mirror by a bidirectional assignability check on the hot path plus a key-set test and a source-level import guard, so no schema type reaches the emitted `.d.ts`. - `definitions-copy.ts` carries the six built-in validators' display copy as 63 lingui messages in the shared canvas catalog, replacing Agents' `OOB_GUARDRAILS_I8N` and Flow's `buildValidatorDisplayInfo`. Ids use raw wire values, never a transcribed slug, which is how the two products ended up keying the same entity as `finNationalId` and `fiNationalId`. English only: the l10n sync owns the other catalogs, as it does for every other string here. - `definitions-enrich.ts` resolves copy onto validated wire definitions. Pure and React-free, so Flow's vsix bridge calls it directly. Curated wins at definition level, wire wins at parameter level, BYO takes no curated copy. - `use-guardrail-definitions.ts` composes the three. `options.definitions` skips the request entirely, so each product keeps its own transport. The context is compared by content rather than identity, unlike `useDiscoveryModels`, where an inline object refetches every render and never settles. Results carry the request key that produced them, so a tenant switch cannot keep serving the previous tenant's guardrails. The 17 places the two products' English differs are each declared with a reason in `definitions-parity.test.ts` and asserted against both products' transcribed copy, so the suite fails on an undeclared difference or a wording we invented. Also lands two things the leaf PRs were each carrying their own copy of, since #1140, #1147 and #1161 all branch from here. `GuardrailStatusChip` is a read-only pill for a guardrail row: deliberately not `GuardrailChip`, which wraps a Radix `Toggle` and would put fake buttons in the tab order, and a `` composed from wind's `badgeVariants` rather than `Badge`, which renders a `
` that is invalid inside the palette entry's ` + ), +}; diff --git a/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-status-chip.test.tsx b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-status-chip.test.tsx new file mode 100644 index 000000000..82c93c836 --- /dev/null +++ b/packages/apollo-react/src/canvas/components/Guardrails/components/guardrail-status-chip.test.tsx @@ -0,0 +1,82 @@ +import { render, screen } from '@testing-library/react'; +import { axe } from 'jest-axe'; +import { createRef } from 'react'; +import { describe, expect, it } from 'vitest'; +import { GuardrailStatusChip } from './guardrail-status-chip'; + +// The label lives in an inner span so it can truncate, so `getByText` returns that span rather +// than the chip. Everything asserted here is on the chip itself. +const chip = () => document.querySelector('[data-slot="guardrail-status-chip"]'); + +describe('GuardrailStatusChip', () => { + it('renders a label, not a control', () => { + render(Governance managed); + + expect(screen.getByText('Governance managed')).toBeInTheDocument(); + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + }); + + it('carries the chip family geometry', () => { + render(Disabled); + + expect(chip()).toHaveClass('rounded-full'); + }); + + it('renders a span carrying the badge classes, not a div', () => { + // wind's `Badge` renders a `
`, and the palette entry puts these chips inside its + // `