The step and the Models page both asked one question of a join that describes every provider: is deepseek-official's credential stored? A user who configured some other route was taken over on every blank session, and the DeepSeek setup card opened over them on every visit to Models with a Cancel that could not close it — while clearing the add card's draft, because it shared the row-editor close handler. providerUsable(row) now answers what both surfaces need: the route is registered and whatever credential its profile names is stored. Readiness (renamed onboardingReadiness) ends on any usable row, needsSetup takes the same fact, and each card kind owns its own close handler. Fixes #2325
131 lines
4.8 KiB
TypeScript
131 lines
4.8 KiB
TypeScript
/** Pure first-run readiness projection over the shared Models join. */
|
|
import { describe, expect, it } from 'vitest'
|
|
import type { CredentialView } from '@deepseek-ai/dsh-api-remotes/client'
|
|
import type { ModelsSettingsState, ProviderRow } from '../src/client/store.ts'
|
|
import { onboardingReadiness, providerUsable } from '../src/client/store.ts'
|
|
|
|
const missingCredential: CredentialView = { configured: false, writable: true }
|
|
|
|
function row(overrides: Partial<ProviderRow> = {}): ProviderRow {
|
|
return {
|
|
entry: {
|
|
provider: 'deepseek-official',
|
|
displayName: 'DeepSeek',
|
|
settingsNs: 'llm-deepseek',
|
|
settingsPath: [],
|
|
active: true,
|
|
},
|
|
configured: true,
|
|
removable: false,
|
|
apiKeyEnv: 'DEEPSEEK_API_KEY',
|
|
credential: missingCredential,
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
/** A second provider the user configured themselves. */
|
|
function otherRow(overrides: Partial<ProviderRow> = {}): ProviderRow {
|
|
return {
|
|
entry: {
|
|
provider: 'hfai',
|
|
displayName: 'HFAI',
|
|
settingsNs: 'llm-pi-ai',
|
|
settingsPath: ['providers', 'hfai'],
|
|
active: true,
|
|
},
|
|
configured: true,
|
|
removable: true,
|
|
apiKeyEnv: 'HFAI_API_KEY',
|
|
credential: { configured: true, source: 'file', writable: true },
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
function state(overrides: Partial<ModelsSettingsState> = {}): ModelsSettingsState {
|
|
return {
|
|
status: 'ready',
|
|
error: null,
|
|
credentialError: null,
|
|
writable: true,
|
|
rows: [row()],
|
|
namespaces: new Map(),
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
describe('providerUsable', () => {
|
|
it('requires a registered route and a stored key for every named reference', () => {
|
|
expect(providerUsable(otherRow())).toBe(true)
|
|
expect(providerUsable(otherRow({ entry: { ...otherRow().entry, active: false } }))).toBe(false)
|
|
expect(providerUsable(otherRow({ credential: missingCredential }))).toBe(false)
|
|
expect(providerUsable(otherRow({ credential: undefined }))).toBe(false)
|
|
})
|
|
|
|
it('treats a reference-free registered route as provider-native authentication', () => {
|
|
expect(providerUsable(otherRow({ apiKeyEnv: undefined, credential: undefined }))).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('onboardingReadiness', () => {
|
|
it('waits for the first join and skips onboarding when the adapter directory entry is absent', () => {
|
|
expect(onboardingReadiness(state({ status: 'idle', rows: [] }))).toEqual({ kind: 'loading' })
|
|
expect(onboardingReadiness(state({ status: 'loading', rows: [] }))).toEqual({ kind: 'loading' })
|
|
expect(onboardingReadiness(state({ rows: [] }))).toEqual({ kind: 'adapter-absent' })
|
|
expect(onboardingReadiness(state({
|
|
rows: [row({
|
|
entry: {
|
|
...row().entry,
|
|
settingsNs: '',
|
|
},
|
|
})],
|
|
}))).toEqual({ kind: 'adapter-absent' })
|
|
})
|
|
|
|
it('reports a missing writable effective credential', () => {
|
|
expect(onboardingReadiness(state())).toEqual({ kind: 'credential-missing' })
|
|
})
|
|
|
|
it('ends onboarding once any other registered provider can serve requests', () => {
|
|
expect(onboardingReadiness(state({ rows: [row(), otherRow()] }))).toEqual({ kind: 'provider-ready' })
|
|
// A provider the user cannot reach yet leaves the prompt in place.
|
|
expect(onboardingReadiness(state({
|
|
rows: [row(), otherRow({ credential: missingCredential })],
|
|
}))).toEqual({ kind: 'credential-missing' })
|
|
})
|
|
|
|
it('accepts file and process-environment credentials without prompting', () => {
|
|
expect(onboardingReadiness(state({
|
|
rows: [row({ credential: { configured: true, source: 'file', writable: true } })],
|
|
}))).toEqual({ kind: 'provider-ready' })
|
|
expect(onboardingReadiness(state({
|
|
rows: [row({ credential: { configured: true, source: 'env', writable: false } })],
|
|
}))).toEqual({ kind: 'provider-ready' })
|
|
})
|
|
|
|
it('turns missing capabilities into diagnostics that never block the product', () => {
|
|
expect(onboardingReadiness(state({ status: 'error', error: 'settings down' }))).toEqual({
|
|
kind: 'unavailable',
|
|
reason: 'load-failed',
|
|
})
|
|
expect(onboardingReadiness(state({
|
|
rows: [row({ entry: { ...row().entry, active: false } })],
|
|
}))).toEqual({ kind: 'unavailable', reason: 'provider-inactive' })
|
|
expect(onboardingReadiness(state({
|
|
credentialError: 'credentials service is absent',
|
|
}))).toEqual({
|
|
kind: 'unavailable',
|
|
reason: 'credentials-unavailable',
|
|
})
|
|
expect(onboardingReadiness(state({
|
|
rows: [row({ credential: undefined })],
|
|
}))).toEqual({ kind: 'unavailable', reason: 'credentials-unavailable' })
|
|
expect(onboardingReadiness(state({
|
|
rows: [row({ credential: { configured: false, writable: false } })],
|
|
}))).toEqual({ kind: 'unavailable', reason: 'credential-read-only' })
|
|
expect(onboardingReadiness(state({ writable: false }))).toEqual({
|
|
kind: 'unavailable',
|
|
reason: 'settings-read-only',
|
|
})
|
|
})
|
|
})
|