From 135064c8314dc7875bb1d1a17bb2c85a7ab1448e Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Fri, 7 Aug 2026 17:02:05 +0800 Subject: [PATCH] fix(ui-models): stop the shared hint contradicting a filled-in field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../src/client/CustomProviderCard.tsx | 7 +++++-- .../ui-models/tests/provider-form.spec.tsx | 20 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/packages/client/ui-models/src/client/CustomProviderCard.tsx b/packages/client/ui-models/src/client/CustomProviderCard.tsx index f8dd6ca3f8..e27bd3c6bd 100644 --- a/packages/client/ui-models/src/client/CustomProviderCard.tsx +++ b/packages/client/ui-models/src/client/CustomProviderCard.tsx @@ -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') diff --git a/packages/client/ui-models/tests/provider-form.spec.tsx b/packages/client/ui-models/tests/provider-form.spec.tsx index 831353bc5c..7d8f2efe27 100644 --- a/packages/client/ui-models/tests/provider-form.spec.tsx +++ b/packages/client/ui-models/tests/provider-form.spec.tsx @@ -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)