test(ui-models): cover reading a stored capacity back out of a row
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.
This commit is contained in:
@@ -177,8 +177,8 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** What a capacity field shows: the buffer while typing, else the stored count. */
|
/** What a capacity field shows: the buffer while typing, else the stored count. */
|
||||||
const capacityText = (index: number, field: CapacityField): string =>
|
const capacityText = (model: ModelDraft, index: number, field: CapacityField): string =>
|
||||||
editing.get(bufferKey(index, field)) ?? capacitySpelling(numberOf(models[index] ?? {}, field))
|
editing.get(bufferKey(index, field)) ?? capacitySpelling(numberOf(model, field))
|
||||||
|
|
||||||
/** Drop one row's entries and shift the rows after it down, in one pass. */
|
/** Drop one row's entries and shift the rows after it down, in one pass. */
|
||||||
const reindexOnRemove = (
|
const reindexOnRemove = (
|
||||||
@@ -388,7 +388,7 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode {
|
|||||||
className={styles['input']}
|
className={styles['input']}
|
||||||
type="text"
|
type="text"
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
value={capacityText(index, 'contextWindow')}
|
value={capacityText(model, index, 'contextWindow')}
|
||||||
placeholder={CAPACITY_HINT.contextWindow}
|
placeholder={CAPACITY_HINT.contextWindow}
|
||||||
aria-label={`${t('modelContextWindow')} ${index + 1}`}
|
aria-label={`${t('modelContextWindow')} ${index + 1}`}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -401,7 +401,7 @@ export function ModelListEditor(props: ModelListEditorProps): ReactNode {
|
|||||||
className={styles['input']}
|
className={styles['input']}
|
||||||
type="text"
|
type="text"
|
||||||
inputMode="numeric"
|
inputMode="numeric"
|
||||||
value={capacityText(index, 'maxTokens')}
|
value={capacityText(model, index, 'maxTokens')}
|
||||||
placeholder={CAPACITY_HINT.maxTokens}
|
placeholder={CAPACITY_HINT.maxTokens}
|
||||||
aria-label={`${t('modelMaxTokens')} ${index + 1}`}
|
aria-label={`${t('modelMaxTokens')} ${index + 1}`}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|||||||
@@ -202,7 +202,12 @@ export function ProviderEditor(props: ProviderEditorProps): ReactNode {
|
|||||||
? setPath(draft, ['apiKeyEnv'], keyRef)
|
? setPath(draft, ['apiKeyEnv'], keyRef)
|
||||||
: draft
|
: 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']))
|
const failure = validateDeepSeekModels(getPath(next, ['models']))
|
||||||
|
/* v8 ignore next 3 -- unreachable from the card: the same failure disables submit */
|
||||||
if (failure !== undefined) {
|
if (failure !== undefined) {
|
||||||
return `${t('model')} ${String(failure.index + 1)}: ${t(failure.key)}`
|
return `${t('model')} ${String(failure.index + 1)}: ${t(failure.key)}`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -263,6 +263,25 @@ describe('model list editing', () => {
|
|||||||
expect(mutate).not.toHaveBeenCalled()
|
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<HTMLInputElement>(`${en.modelContextWindow} 1`).value).toBe('1M')
|
||||||
|
expect(screen.getByLabelText<HTMLInputElement>(`${en.modelMaxTokens} 1`).value).toBe('256K')
|
||||||
|
})
|
||||||
|
|
||||||
it('edits one row of several and lets a cleared capacity leave the profile', async () => {
|
it('edits one row of several and lets a cleared capacity leave the profile', async () => {
|
||||||
const { mutate } = await mountSection({
|
const { mutate } = await mountSection({
|
||||||
providers: { openai: { baseURL: 'https://proxy.example/v1', models: [{ id: 'first' }, { id: 'second' }] } },
|
providers: { openai: { baseURL: 'https://proxy.example/v1', models: [{ id: 'first' }, { id: 'second' }] } },
|
||||||
|
|||||||
Reference in New Issue
Block a user