fix(models): wait for current snapshot when retrying setup
This commit is contained in:
1 parent
bd20fc56bb
commit
c880291a0b
5 files changed
+56
-6
No files matched your search
@@ -15,8 +15,8 @@
|
||||
en: d152484eb41ac6b4
|
||||
zh: 09388d293f9be9cb
|
||||
/deepseek-ai-dsh-client-ui-settings-models/use-this-package:
|
||||
en: 4f46d6c1b70b2456
|
||||
zh: 9d2affd4a6b5d3a6
|
||||
en: db5aaf7f702e1f0c
|
||||
zh: 409a78c14fb2b6e3
|
||||
/deepseek-ai-dsh-client-ui-settings-models/use-this-package/api-keys:
|
||||
en: c2f2a5c06ef20d5b
|
||||
zh: 49a7eb0b66b4ed53
|
||||
|
||||
@@ -35,7 +35,7 @@ Open the Models page from the Settings navigation to see every configured provid
|
||||
|
||||
A provider with a stored catalog error remains visible with its diagnostic and edit/delete actions. Add actions are offered only for registered settings namespaces, so an unavailable namespace cannot leave a button that opens no editor. A rejected save leaves the editor open and displays the Host diagnostic.
|
||||
|
||||
After Apply closes an editor, its saved status appears when the current provider, settings, and credential snapshot is ready. Opening any row's editor, adding a provider, and opening a removal confirmation wait for that refresh. A newly opened Models section waits for its first ready snapshot before mounting editors. A background refresh preserves an open draft while its editor remains mounted; a failed page load or provider removal can close it.
|
||||
After Apply closes an editor, its saved status appears when the current provider, settings, and credential snapshot is ready. Opening any row's editor, adding a provider, and opening a removal confirmation wait for that refresh. A newly opened Models section, or one retrying after a page-load failure, waits for a ready snapshot before mounting editors. A background refresh preserves an open draft while its editor remains mounted; a failed page load or provider removal can close it.
|
||||
|
||||
Host configuration `credentialOnboarding` defaults to `true`. The Electron preload marker suppresses automatic credential onboarding and the Web welcome notice; Models settings and explicit API-key editing remain available. The [account plugin](../ui-settings-account/README.md#desktop-onboarding) owns the Desktop introduction. Other native shells can disable only the credential step with `credentialOnboarding: false`. Host publishes this public boolean through `webserver/index-inject`, and Client validates it before registering dialogs. It is page initialization data, not a durable completion marker.
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ kind: "package-reference"
|
||||
|
||||
存在已存储目录错误的提供商仍显示诊断以及编辑、删除入口。添加操作只面向已注册的 settings 命名空间,因此不可用的命名空间不会留下无法打开编辑器的按钮。保存被拒绝时,编辑器保持打开并展示 Host 诊断。
|
||||
|
||||
应用设置并关闭编辑器后,当前提供商、settings 与凭据快照就绪时才显示保存状态。打开任一行的编辑器、添加提供商和打开移除确认都会等待该刷新。新打开的 Models 区域会等到首个 ready 快照后才挂载编辑器。后台刷新期间,只要编辑器仍挂载,已打开的草稿就会保留用户的修改;页面加载失败或提供商被移除可能关闭编辑器。
|
||||
应用设置并关闭编辑器后,当前提供商、settings 与凭据快照就绪时才显示保存状态。打开任一行的编辑器、添加提供商和打开移除确认都会等待该刷新。新打开的 Models 区域或页面加载失败后的重试会等到 ready 快照后才挂载编辑器。后台刷新期间,只要编辑器仍挂载,已打开的草稿就会保留用户的修改;页面加载失败或提供商被移除可能关闭编辑器。
|
||||
|
||||
Host 配置 `credentialOnboarding` 默认为 `true`。Electron preload 标记会抑制自动凭证引导和 Web 欢迎须知;模型设置页与显式 API Key 编辑仍然可用。[账号插件](../ui-settings-account/README.zh.md#desktop-onboarding)负责 Desktop 引导。其他原生壳可以通过 `credentialOnboarding: false` 仅禁用凭证步骤。Host 通过 `webserver/index-inject` 发布这个公开的布尔值,Client 在注册弹窗前校验它。它是页面初始化数据,不是持久化的完成标记。
|
||||
|
||||
|
||||
@@ -234,9 +234,10 @@ function Loaded({ injected, renderSlot }: { injected: ModelsSectionFace; renderS
|
||||
const snapshot = injected.useSnapshot(value => value)
|
||||
const state = { ...snapshot, rows: snapshot.rows.map(row => row.entry.provider === 'deepseek-account'
|
||||
? { ...row, entry: { ...row.entry, displayName: t('deepSeekAccount') } } : row) }
|
||||
/** A new surface waits for a current snapshot; later refreshes preserve its drafts. */
|
||||
/** A new or failed surface waits for a current snapshot; background refreshes preserve its drafts. */
|
||||
const [hasReadySnapshot, setHasReadySnapshot] = useState(state.status === 'ready')
|
||||
if (state.status === 'ready' && !hasReadySnapshot) setHasReadySnapshot(true)
|
||||
if (state.status === 'error' && hasReadySnapshot) setHasReadySnapshot(false)
|
||||
const [editing, setEditing] = useState<EditorTarget | undefined>(undefined)
|
||||
const [addOpen, setAddOpen] = useState(false)
|
||||
const [addMode, setAddMode] = useState<AddMode>('catalog')
|
||||
|
||||
@@ -192,7 +192,7 @@ function scriptedFace(overrides: {
|
||||
const unset = overrides.unset ?? vi.fn(() => Promise.resolve(remoteOk(undefined)))
|
||||
const face = {
|
||||
llm: {
|
||||
listProviders: vi.fn(() => Promise.resolve(remoteOk([
|
||||
listProviders: vi.fn((): ReturnType<PageContext['remote']['llm']['listProviders']> => Promise.resolve(remoteOk([
|
||||
{ id: 'deepseek-official', name: 'DeepSeek' },
|
||||
{ id: 'openai', name: 'openai' },
|
||||
]))),
|
||||
@@ -374,6 +374,55 @@ describe('ModelsSection', () => {
|
||||
await screen.findByRole('status')
|
||||
})
|
||||
|
||||
it('waits for the current snapshot before remounting setup after a failed page load', async () => {
|
||||
const { controller, face, mirror, mutate } = await mountFirstRun()
|
||||
const oldKey = screen.getByLabelText<HTMLInputElement>(en.keyInput)
|
||||
fireEvent.change(oldKey, { target: { value: 'sk-discarded-on-error' } })
|
||||
face.llm.listProviders.mockResolvedValueOnce(remoteFail('directory unavailable', 'gateway/internal'))
|
||||
await act(async () => { await controller.load() })
|
||||
expect(screen.queryByLabelText(en.keyInput)).toBeNull()
|
||||
expect(screen.getByText(`${en.loadFailed}: directory unavailable`)).toBeTruthy()
|
||||
const before = controller.store.getSnapshot().namespaces.get('llm-deepseek')!
|
||||
mirror.acceptView({
|
||||
...before,
|
||||
value: { apiKeyEnv: 'DEEPSEEK_API_KEY', baseURL: 'https://recovered.example', models: DEFAULT_DEEPSEEK_MODELS },
|
||||
user: { baseURL: 'https://recovered.example' },
|
||||
revision: 2,
|
||||
})
|
||||
const read = Promise.withResolvers<RemoteResult<Record<string, CredentialInfo>>>()
|
||||
const started = Promise.withResolvers<undefined>()
|
||||
const describe = face.credentials.describe.getMockImplementation()!
|
||||
face.credentials.describe.mockImplementation((refs) => {
|
||||
if (refs.length > 1) { started.resolve(undefined); return read.promise }
|
||||
return describe(refs)
|
||||
})
|
||||
const load = vi.spyOn(controller, 'load')
|
||||
try {
|
||||
fireEvent.click(screen.getByRole('button', { name: en.retry }))
|
||||
await act(async () => { await started.promise })
|
||||
expect(load).toHaveBeenCalledOnce()
|
||||
expect(controller.store.getSnapshot()).toMatchObject({ status: 'loading' })
|
||||
expect(controller.store.getSnapshot().namespaces.get('llm-deepseek')?.revision).toBe(0)
|
||||
expect(screen.getByRole('heading', { name: en.title })).toBeTruthy()
|
||||
expect(screen.queryByLabelText(en.keyInput)).toBeNull()
|
||||
} finally {
|
||||
await act(async () => { read.resolve(remoteOk({})); await load.mock.results[0]?.value })
|
||||
load.mockRestore()
|
||||
}
|
||||
const key = await screen.findByLabelText<HTMLInputElement>(en.keyInput)
|
||||
expect(key).not.toBe(oldKey)
|
||||
expect(key.value).toBe('')
|
||||
fireEvent.click(screen.getByText(en.customized))
|
||||
const url = screen.getByLabelText<HTMLInputElement>(en.baseUrl)
|
||||
expect(url.value).toBe('https://recovered.example')
|
||||
fireEvent.change(url, { target: { value: 'https://after-retry.example' } })
|
||||
fireEvent.click(screen.getByRole('button', { name: en.apply }))
|
||||
await waitFor(() => { expect(mutate).toHaveBeenCalledWith(
|
||||
'llm-deepseek', [{ op: 'set', path: ['baseURL'], value: 'https://after-retry.example' }], 2,
|
||||
) })
|
||||
await screen.findByRole('status')
|
||||
})
|
||||
|
||||
it('preserves a mounted setup draft while its current refresh is pending', async () => {
|
||||
const { controller, face } = await mountFirstRun()
|
||||
const key = screen.getByLabelText<HTMLInputElement>(en.keyInput)
|
||||
|
||||
Reference in new issue
Block a user