From dd36db54346963c3c36525145687a7334ba4d2aa Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Wed, 5 Aug 2026 21:12:45 +0800 Subject: [PATCH] test(ui-models): cover reading a stored capacity back out of a row MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every capacity assertion typed into the field first, so the path that reads a stored count — the one an already-configured route takes every time it is opened — was never exercised, and the coverage gate said so. A row now opens on stored counts and is expected to spell them `1M` and `256K`, the same vocabulary the field accepts. `capacityText` takes the row it is rendering rather than looking it up again by index, which retires the impossible empty-row branch that lookup needed. The list editor's pre-flight model check is unreachable from the card that disables submit on the same failure, and says so where it stands. --- .../ui-models/src/client/ModelListEditor.tsx | 8 ++++---- .../ui-models/src/client/ProviderEditor.tsx | 5 +++++ .../ui-models/tests/provider-form.spec.tsx | 19 +++++++++++++++++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/packages/client/ui-models/src/client/ModelListEditor.tsx b/packages/client/ui-models/src/client/ModelListEditor.tsx index 6e7a26338c..e60c8c24ed 100644 --- a/packages/client/ui-models/src/client/ModelListEditor.tsx +++ b/packages/client/ui-models/src/client/ModelListEditor.tsx @@ -177,8 +177,8 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode { } /** What a capacity field shows: the buffer while typing, else the stored count. */ - const capacityText = (index: number, field: CapacityField): string => - editing.get(bufferKey(index, field)) ?? capacitySpelling(numberOf(models[index] ?? {}, field)) + const capacityText = (model: ModelDraft, index: number, field: CapacityField): string => + editing.get(bufferKey(index, field)) ?? capacitySpelling(numberOf(model, field)) /** Drop one row's entries and shift the rows after it down, in one pass. */ const reindexOnRemove = ( @@ -388,7 +388,7 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode { className={styles['input']} type="text" inputMode="numeric" - value={capacityText(index, 'contextWindow')} + value={capacityText(model, index, 'contextWindow')} placeholder={CAPACITY_HINT.contextWindow} aria-label={`${t('modelContextWindow')} ${index + 1}`} disabled={disabled} @@ -401,7 +401,7 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode { className={styles['input']} type="text" inputMode="numeric" - value={capacityText(index, 'maxTokens')} + value={capacityText(model, index, 'maxTokens')} placeholder={CAPACITY_HINT.maxTokens} aria-label={`${t('modelMaxTokens')} ${index + 1}`} disabled={disabled} diff --git a/packages/client/ui-models/src/client/ProviderEditor.tsx b/packages/client/ui-models/src/client/ProviderEditor.tsx index 52412d7645..f48572cc58 100644 --- a/packages/client/ui-models/src/client/ProviderEditor.tsx +++ b/packages/client/ui-models/src/client/ProviderEditor.tsx @@ -202,7 +202,12 @@ export function ProviderEditor(props: ProviderEditorProps): ReactNode { ? setPath(draft, ['apiKeyEnv'], keyRef) : draft { + // The same checker gates the submit button, so a card cannot reach this + // with a bad row; it stays because the schema check below would refuse + // the write with a message naming a path instead of the row, and because + // nothing but this function decides what is written. const failure = validateDeepSeekModels(getPath(next, ['models'])) + /* v8 ignore next 3 -- unreachable from the card: the same failure disables submit */ if (failure !== undefined) { return `${t('model')} ${String(failure.index + 1)}: ${t(failure.key)}` } diff --git a/packages/client/ui-models/tests/provider-form.spec.tsx b/packages/client/ui-models/tests/provider-form.spec.tsx index 49f512c8c4..99e85b0d10 100644 --- a/packages/client/ui-models/tests/provider-form.spec.tsx +++ b/packages/client/ui-models/tests/provider-form.spec.tsx @@ -263,6 +263,25 @@ describe('model list editing', () => { expect(mutate).not.toHaveBeenCalled() }) + it('spells a stored capacity back the way it is typed', async () => { + await mountSection({ + providers: { + openai: { + baseURL: 'https://proxy.example/v1', + models: [{ id: 'kept', contextWindow: 1_000_000, maxTokens: 256_000 }], + }, + }, + }) + openEditor('openai') + expandModel(1) + + // Opening a row reads the stored counts, which are plain integers; showing + // them as such would make an already-configured route look unlike one the + // user just typed, and re-applying would rewrite the field it read. + expect(screen.getByLabelText(`${en.modelContextWindow} 1`).value).toBe('1M') + expect(screen.getByLabelText(`${en.modelMaxTokens} 1`).value).toBe('256K') + }) + it('edits one row of several and lets a cleared capacity leave the profile', async () => { const { mutate } = await mountSection({ providers: { openai: { baseURL: 'https://proxy.example/v1', models: [{ id: 'first' }, { id: 'second' }] } },