* test(ui): cover the two modals no test would catch breaking
Both files sit in the antd Modal migration's blind spot. EditSSOSettingsModal's
test replaced antd wholesale with a stub Modal and asserted the stub's own
data-testid markup, so it proved nothing about the modal a user sees and would
have stayed green through any regression. routing_groups had no test at all.
Rewrite the first against the real antd Modal, querying by dialog role and
accessible name so the assertions hold under either library, and add an
integration test for the second that drives the row menu and the delete
confirmation end to end.
Modal width drops out of the SSO assertions: antd carries it as an inline style
and shadcn as a max-width class, so either form couples the test to the library
rather than to anything a user perceives.
* refactor(ui): move the straightforward antd Modals onto the shared Dialog
Twenty files whose Modal only used title, open, width, footer, className and
onCancel, so each one maps onto Dialog without judgement calls. Width becomes a
max-width class, the body gets the house scroll cap so tall content stays
reachable, and destroyOnHidden goes away because Base UI unmounts a closed
dialog on its own.
EditMembership needed a real fix rather than a translation. Clearing the form
after a submit resolved only ever worked by accident: the reset set every field
to undefined, which react-hook-form does not push out to a subscribed
Controller, and the fields looked cleared only because antd's Modal happened to
re-render the subtree afterwards. Dialog does not, so the stale values showed
through. emptyMemberFormValues now returns the empty value each control
actually understands, an empty string, null or an empty list, and the reset
lands whatever renders around it. Its test asserted the undefined shape while
describing the behaviour it was missing, so it now checks the values instead.
* refactor(ui): migrate the antd Modals that needed a judgement call
Sixteen more files. Most carried a prop that does not translate literally:
maskClosable becomes disablePointerDismissal, afterOpenChange becomes
onOpenChangeComplete, and closable={false} becomes showCloseButton={false}.
The styles prop went away everywhere it appeared. All but one instance set the
body to 24px and the header to 24px with no border, which is what DialogContent
already renders, so keeping it would have meant writing the default back by
hand.
Several Modals passed onOk alongside footer={null}, so antd rendered no OK
button and the handler could never fire. Each of those handlers was a
character-for-character copy of the neighbouring onCancel, so they are gone
rather than translated.
Rich titles now sit inside DialogHeader with DialogTitle carrying the heading
text, instead of the whole header block being nested inside DialogTitle. That
had put an h2 inside another h2, which is invalid and gave one dialog two
headings.
UserEnvVarsModal loses its formGeneration counter. Remounting the form when the
modal finished opening only mattered because antd kept a closed modal's
children mounted; Base UI unmounts them, so reopening is blank on its own. Its
test helper had encoded that remount as a timing assumption, so the file now
states the requirement outright and checks that reopening shows an empty field.
CreateMCPServer's cancel test read the tool list while the modal was closed,
which only worked because forceRender kept it mounted. It now asserts what a
user can actually observe: the panel is gone while closed, and reopening brings
back an empty URL and no tools.
Unmounting an open Base UI dialog leaves its scroll lock on <html> and <body>,
which survives cleanup() and makes every later test in the file see a locked
page where popups compute pointer-events: none and clicks quietly do nothing.
The shared setup now releases it.
* refactor(ui): finish the antd Modal migration onto the shared Dialog
Fifteen files whose Modal relied on antd's built-in footer. okText, cancelText,
onOk, okButtonProps, cancelButtonProps and confirmLoading collapse into two
explicit buttons in a DialogFooter, with danger becoming the destructive
variant and the various loading flags becoming disabled plus aria-busy. The one
okButtonProps that also hand-set a red background drops it, since the variant
already carries that.
add_guardrail_form keeps its own chrome, so its DialogContent turns off the
built-in close button and the padding, and its heading becomes the DialogTitle.
Under antd it passed title={null} and had no accessible name at all.
Modals that positioned themselves near the top of the viewport needed
translate-y-0 alongside top-8, because DialogContent centres itself with a
transform that top alone does not undo.
TeamGuardrailsTab's test reached its Mode select by index into every combobox on
the page. A modal dialog hides the rest of the page from assistive technology,
which antd never did, so the count changed and the index pointed at the wrong
control. It asks for the field by label now.
The mask-dismissal test drove antd's .ant-modal-wrap class directly; it uses the
overlay slot our own component exposes, and still fails if
disablePointerDismissal is dropped.
CreateUserButton stays on antd. Its Modal converts cleanly, but the colocated
test file then fails a varying handful of cases, and the cause sits in the test
file rather than the component, so it wants its own change.
Pruning suppressions for the touched files also cleared four
react-hooks/set-state-in-effect entries on CreateMCPServer that were already
stale before this branch.
* test(ui): pick Base UI select options through the shared helper
React 19's flush timing loses the race this test was relying on: the option
lands in the DOM one render before its positioner drops pointer-events: none,
so user-event refused the click. tests/test-utils already exports
chooseSelectOption for exactly this, added alongside the React 19 upgrade.
* chore(ui): upgrade the dashboard to React 19
Bumps react and react-dom from 18.3.1 to 19.2.8 with matching @types. Next 16 already required a React 19 peer, so this aligns the dashboard with what the framework expects and unblocks Base UI and shadcn work that assumes the React 19 ref model.
React 19 passes ref through as a regular prop, so the setup file's forwardRef tripwire and the ref-forwarding test's forwardRef case no longer describe real behavior; both now assert the React 19 contract instead. useRef<T>(null) now yields RefObject<T | null>, which is the one prop type MessageList had to widen.
* test(ui): wait for a Base UI select popup to open before clicking an option
The option lands in the DOM one render before the popup finishes entering, while its positioner still carries pointer-events: none, so clicking it throws. Waiting on the option's text alone was a race that React 19's flush timing loses, which is why four ToolPolicies cases went red on the bump.
chooseSelectOption in test-utils opens the trigger, finds the option by role, waits for it to stop being pointer-blocked, then clicks. It also replaces the last-match-by-text hack, which only worked because the popup happens to portal after the table.
The last tremor component import left the dashboard when the primitive
sweep merged, so the package, its v3 compatibility shim, its @theme token
block and the palette safelist it needed at runtime all have no consumer.
Removing the safelist is what shrinks the shipped stylesheet: tremor built
class names at runtime, so Tailwind had to emit every bg/text/border/ring/
stroke/fill utility across 22 palettes and 11 shades in case one was used.
Nothing in the app constructs a class name that way any more, so the
scanner finds every utility on its own.
The date-fns overrides pin also goes. It only existed because tremor and
react-day-picker@8 peered on date-fns 3 while Base UI wanted 4, and the
lockfile still resolves a single hoisted 4.4.0 without it.
* refactor(ui): move the model info view and pass-through endpoint forms off tremor
The pass-through settings form's Save button relied on tremor's implicit
submit, so it now carries an explicit type="submit" because the Base UI
button defaults to type="button". The include-subpath switch inside the
antd Form.Item is wired through onCheckedChange plus form.setFieldsValue,
since the Base UI switch does not read the onChange antd injects. Both
tab strips keep every panel mounted so the edit forms survive a tab
switch, pinned by a new mount-contract test on the model info view. Also
prunes the six tremor no-restricted-imports suppressions these files no
longer need.
* test(ui): pin the model info overview panel to its own dom node across tab switches
* fix(ui): keep the line tab strip on the model info and pass-through views
Both tab strips were bare tremor TabLists, which defaulted to the line
variant, so the straight rename turned them into filled segmented pills.
They now use the same full-width line strip the agent, guardrail and
prompt info views ship.
MessageManager and NotificationManager were thin facades over lib/toast since #37207. This
rewrites their ~750 call sites (226 files) to import { toast } from @/lib/toast directly:
success/info/warning/error keep their names, fromBackend becomes fromError, destroy/clear
become dismiss. The one config-object caller (CreateMCPServer's admin-review branch) becomes
an explicit toast.success(message, { description }). Behaviour is unchanged: no production
caller passed a duration, so every toast keeps the same kind, title and default duration.
Tests: the global vitest mock now targets @/lib/toast (toast.test.ts opts back out with
vi.unmock), so the per-file vi.mock boilerplate for the facades is deleted and assertions read
toast.success / toast.fromError. The two facade files, their test and their filename-case
suppressions are removed, along with the commented-out facade calls left in networking.tsx
api.ts read globalThis.location when the module loaded, which froze the base
URL at import and pinned its test file to jsdom. The creation-time baseUrl and
the middleware's runtime rebase were also two mechanisms doing overlapping
work, and the rebase hand-copied eleven RequestInit fields on every call.
Pass openapi-fetch's Request option instead, so the constructor applies
whatever getRequestBaseUrl() returns at the moment the request is built.
registerBaseUrlGetter is now the single source of the base URL, rebaseUrl and
rebaseRequest are deleted, and the request is constructed once, so the init
openapi-fetch assembled reaches the platform Request untouched. The abort
signal is no longer copied by hand.
This preserves behaviour rather than approximating it: getProxyBaseUrl() falls
back to location.origin, so the runtime base was never empty in a browser and
the old middleware already rebased every request, discarding the creation-time
value each time.
setupTests.ts gates its DOM-only tail behind a window check; setup files run
for every environment, so that tail previously stopped any node-environment
test file from loading.
api.test.ts now runs under @vitest-environment node with its assertions intact
and no location stub, plus regressions for per-call base resolution and abort
forwarding. api.sameOrigin.test.ts covers the browser fallback to the page
origin, which needs a DOM environment.
* fix(ui): forward refs through ui primitives and fail tests on swallowed refs
Under React 18 a ref passed to a plain function component is dropped
with only a dev console warning, so Base UI render-prop triggers
composed over our shadcn-style primitives silently stop working (the
tooltip just never opens; ui/badge.tsx hit exactly this on the shared
DataTable branch). Label, Separator, Skeleton, UiLoadingSpinner and the
Table family now use React.forwardRef like Button and Input already
did, a contract test pins ref delivery for each, and setupTests turns
React's ref warning into a test failure so the next primitive that
swallows a ref fails CI instead of shipping a dead tooltip
* fix(ui): include captured ref warnings in the tripwire error
The afterEach tripwire threw a fixed message and discarded the collected
React warnings, so a failure never said which component swallowed the ref.
Append the captured warnings (component name + stack) to the thrown error.
Local vi.mock("@tremor/react") overrides in router_settings tests were
clobbering the global setupTests.ts mock, re-introducing the real Tooltip
component which schedules a setTimeout. When jsdom tears down after each
test file, the pending timer fires and hits window is not defined, which
Vitest flags as an unhandled error that can cause false positive failures
in subsequent tests (including the create_mcp_server timeout in CI).
Fix: add Switch to the global @tremor/react mock in setupTests.ts (the
only reason the local overrides existed), then remove the three local
vi.mock("@tremor/react") blocks so all test files inherit the global mock
with properly stubbed Button, Tooltip, and Switch.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>