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:
Yichen Jiang
2026-07-30 20:36:28 +08:00
parent af7dd4e340
commit f5d21af60b
5 changed files with 48 additions and 4 deletions
@@ -12,6 +12,7 @@ import { useState } from 'react'
import type { ReactNode } from 'react'
import type { IApiClient, SettingsNamespaceView } from '@deepseek-ai/dsh-client-connection/client'
import type { SnapshotSelectorHook } from '@deepseek-ai/dsh-client-web-react'
import { messageOf } from './store.ts'
import type { ModelsSettingsState, ModelsSettingsStore, ProviderRow } from './store.ts'
import { ProviderEditor } from './ProviderEditor.tsx'
import type { en } from './locales.ts'
@@ -68,7 +69,7 @@ export async function removeProviderProfile(
} catch (error) {
// The transport rejected rather than answering; the caller must be able
// to say so instead of the row silently staying put.
return error instanceof Error ? error.message : String(error)
return messageOf(error)
}
if (!response.result.ok) return response.result.error.message
await controller.load()
@@ -18,7 +18,7 @@ import type { CredentialView, IApiClient, SettingsNamespaceView, SettingsPathOpV
import {
deletePath, getPath, nodeAtPath, rehydrateSchema, setPath, validateDraft,
} from '@deepseek-ai/dsh-client-schema-form'
import { deriveKeyRef } from './store.ts'
import { deriveKeyRef, messageOf } from './store.ts'
import type { en } from './locales.ts'
import styles from './ModelsSection.module.css'
@@ -215,7 +215,7 @@ export function ProviderEditor(props: ProviderEditorProps): ReactNode {
// A transport failure (disconnect, a request the host refuses) rejects
// rather than answering; without this the card would stay busy forever
// with no error shown.
setFailure(error instanceof Error ? error.message : String(error))
setFailure(messageOf(error))
} finally {
setBusy(false)
}
@@ -40,6 +40,17 @@ export interface ModelsSettingsState {
namespaces: ReadonlyMap<string, SettingsNamespaceView>
}
/**
* Human text for a rejected wire call. A transport failure rejects with an
* Error; a host or a runtime can reject with anything, and the page still has
* to say something.
* @param error - the rejection value.
* @returns the message to show.
*/
export function messageOf(error: unknown): string {
return error instanceof Error ? error.message : String(error)
}
/**
* Derive the conventional credential reference for a provider route: the v1
* page never asks for an environment-variable name, so a typed key stores