fix(ui-models): stop the shared hint contradicting a filled-in field

The line under the create form names the one blocked gate worth naming,
and its fallback arm reads "no models yet". An unmet Provider ID gate fell
through to that arm, so a card with two models listed right above it was
told it needed one. The key gate was already excluded for this reason; the
route gate was assumed excluded because its field explains itself, and was
not.

Tightening the route rule in the previous commit is what made this easy to
hit — a digit-leading id now fails the gate — but the fallthrough predates
it and fires for an empty or taken id just the same.
This commit is contained in:
Yichen Jiang
2026-08-07 17:02:05 +08:00
parent 5a90eb41fb
commit 135064c831
2 changed files with 25 additions and 2 deletions

View File

@@ -112,14 +112,17 @@ export function CustomProviderCard(props: CustomProviderCardProps): ReactNode {
const ready = route.length > 0 && !routeInvalid && !routeTaken
&& baseURL.length > 0 && models.length > 0 && modelFailure === undefined
&& keyFailure === undefined
// The one blocked gate worth a line under the form. The route id is omitted
// because its own field already explains itself, and a satisfied card says
// The one blocked gate worth a line under the form. A satisfied card says
// nothing at all rather than printing an empty paragraph.
const hint = failure !== undefined || ready
// The key field prints its own failure directly beneath itself, so a card
// blocked only by the key stays silent here rather than answering with the
// next unmet gate — which is satisfied, and reads as a second, false fault.
|| keyFailure !== undefined
// Same for the route id, and it must be tested rather than assumed: the
// fallback arm below reads "no models yet", so an unmet route gate used to
// fall through to it and contradict the filled-in list right above.
|| route.length === 0 || routeInvalid || routeTaken
? undefined
: baseURL.length === 0
? t('customNeedsBaseUrl')

View File

@@ -790,6 +790,26 @@ describe('hand-declared providers', () => {
expect(onClose).toHaveBeenCalledWith(true)
})
it('never contradicts a filled-in field with the next gate\u2019s copy', () => {
mountCard()
const routeField = screen.getByLabelText(en.customRoute)
fireEvent.change(routeField, { target: { value: '2' } })
fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://acme.test/v1' } })
fireEvent.click(screen.getByRole('button', { name: en.addModel }))
fireEvent.change(screen.getByLabelText(`${en.modelId} 1`), { target: { value: 'm' } })
// The route field explains itself right under the input; the shared line
// must stay silent rather than falling through to "no models yet" while
// the list above plainly has one.
expect(screen.getByText(en.customRouteInvalid)).toBeTruthy()
expect(screen.queryByText(en.customNeedsModels)).toBeNull()
// Fixing the route hands the line back to the gate that is actually unmet.
fireEvent.change(routeField, { target: { value: 'acme' } })
expect(screen.queryByText(en.customNeedsModels)).toBeNull()
expect(buttonNamed(en.create).disabled).toBe(false)
})
it('refuses a route id whose derived credential reference would be illegal', () => {
mountCard()
const routeField = screen.getByLabelText(en.customRoute)