test(ui-models): cover the failure paths, and share the one message reader
The per-file coverage gate caught three uncovered paths in the error handling this round added: the page banner for a failed row removal, the editor card's transport-rejection catch, and `store.fail` itself. Two of them are one click each — Remove with a rejecting write, Apply with a rejecting write — so they are covered through the UI rather than by calling the helpers directly. The third was a duplicated `error instanceof Error ? error.message : String(error)` in two files; it becomes one exported `messageOf`, which removes the branch from both call sites and gives the fallback arm a home a direct unit test can reach (the lint rule forbids rejecting a promise with a non-Error, so a rejection cannot exercise it).
This commit is contained in:
@@ -409,6 +409,19 @@ describe('ModelsSection', () => {
|
||||
expect(set).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps the card usable when the write rejects instead of answering', async () => {
|
||||
// A transport failure (disconnect, or the 403 a non-loopback browser now
|
||||
// gets on the whole configuration plane) rejects rather than returning a
|
||||
// failed envelope: without a catch the card would stay busy forever.
|
||||
await mountSection({ mutate: vi.fn(() => Promise.reject(new Error('connection lost'))) })
|
||||
fireEvent.click(screen.getByText(en.customized))
|
||||
fireEvent.change(screen.getByLabelText<HTMLInputElement>(en.baseUrl), { target: { value: 'https://next' } })
|
||||
fireEvent.click(screen.getByText(en.apply))
|
||||
await screen.findByText('connection lost')
|
||||
// Not stuck in `applying…`: the finally cleared busy, so Apply is live again.
|
||||
expect(screen.getByText(en.apply)).toBeTruthy()
|
||||
})
|
||||
|
||||
it('surfaces a shadowed credential write on the card', async () => {
|
||||
await mountSection({
|
||||
set: vi.fn(() => Promise.resolve(fail('credentials: DEEPSEEK_API_KEY is shadowed by the read-only environment', 'credential-rejected'))),
|
||||
@@ -557,6 +570,15 @@ describe('ModelsSection', () => {
|
||||
expect(controller.store.getSnapshot().rows).toBe(before)
|
||||
})
|
||||
|
||||
it('shows a failed removal on the page banner, including a non-Error rejection', async () => {
|
||||
// The whole click path: the row's Remove button, the transport rejecting
|
||||
// with a non-Error value, and the store surfacing it where a load failure
|
||||
// would appear — rather than the row silently staying put.
|
||||
await mountSection({ mutate: vi.fn(() => Promise.reject(new Error('the host refused'))) })
|
||||
fireEvent.click(screen.getAllByText(en.remove)[0] as HTMLElement)
|
||||
await screen.findByText(`${en.loadFailed}: the host refused`)
|
||||
})
|
||||
|
||||
it('reports a transport rejection instead of failing the removal silently', async () => {
|
||||
const { face, controller } = await mountSection({
|
||||
mutate: vi.fn(() => Promise.reject(new Error('connection lost'))),
|
||||
|
||||
Reference in New Issue
Block a user