fix(web): silence the stale gate hint, clear whitespace fields, narrow the paste heuristic

This commit is contained in:
Yichen Jiang
2026-08-07 11:35:23 +08:00
parent 91e9d5afa2
commit 5a422337f8
8 changed files with 55 additions and 11 deletions

View File

@@ -1092,6 +1092,8 @@ describe('apiKeyFailure', () => {
['a padded key, which the caller trims', ' sk-abc '],
['the printable-ASCII boundary characters', '!~'],
['a hyphenated key carrying an equals sign', 'sk-ABC=xyz'],
['an all-upper-case key ending in base64 padding', 'ABCD=='],
['an all-upper-case key ending in one padding character', 'MNOPQRST='],
])('accepts %s', (_label, draft) => {
expect(apiKeyFailure(draft)).toBeUndefined()
})

View File

@@ -880,6 +880,22 @@ describe('hand-declared providers', () => {
expect(set).not.toHaveBeenCalled()
})
it('stays silent about the other gates when only the key is refused', () => {
mountCard()
fireEvent.change(screen.getByLabelText(en.customRoute), { target: { value: 'acme-gateway' } })
fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: 'https://gateway.acme.example/v1' } })
fireEvent.click(screen.getByRole('button', { name: en.addModel }))
fireEvent.change(screen.getByLabelText(`${en.modelId} 1`), { target: { value: 'acme-large' } })
fireEvent.change(screen.getByLabelText(en.keyInput), { target: { value: 'sk-\u{1F600}' } })
// Route, endpoint, and models are all satisfied, so answering with the
// next unmet gate would print a second, false fault beside the real one.
expect(screen.getByText(en.keyIllegalCharacters)).toBeTruthy()
expect(screen.queryByText(en.customNeedsModels)).toBeNull()
expect(screen.queryByText(en.customNeedsBaseUrl)).toBeNull()
})
it('tells a whitespace-only key what a blank field means on a create card', () => {
const { mutate } = mountCard()
@@ -927,6 +943,21 @@ describe('API key field', () => {
expect(set).not.toHaveBeenCalled()
})
it('clears a whitespace-only base URL instead of writing the spaces', async () => {
const { mutate } = await mountSection()
openEditor('openai')
// The field renders this as empty, so the draft must agree: storing the
// spaces would hand both adapters a non-empty string they accept as a URL.
fireEvent.change(screen.getByLabelText(en.baseUrl), { target: { value: ' ' } })
fireEvent.click(screen.getByText(en.apply))
await waitFor(() => { expect(mutate).toHaveBeenCalled() })
const ops = firstMutate(mutate).ops
expect(ops.some(op => op.op === 'set' && op.path.includes('baseURL'))).toBe(false)
expect(ops.some(op => op.op === 'unset' && op.path.includes('baseURL'))).toBe(true)
})
it('blocks submit and names the field when the key holds only whitespace', async () => {
const { mutate, set } = await mountSection()
openEditor('openai')