readiness.client.spec.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. /** Pure first-run readiness projection over the shared Models join. */
  2. import { describe, expect, it } from 'vitest'
  3. import type { CredentialInfo } from '@deepseek-ai/dsh-api-remotes/client'
  4. import type { ModelsSettingsState, ProviderRow } from '../src/client/store.ts'
  5. import { onboardingReadiness, providerUsable } from '../src/client/store.ts'
  6. const missingCredential: CredentialInfo = { configured: false, writable: true }
  7. function row(overrides: Partial<ProviderRow> = {}): ProviderRow {
  8. return {
  9. entry: {
  10. provider: 'deepseek-official',
  11. displayName: 'DeepSeek',
  12. settingsNs: 'llm-deepseek',
  13. settingsPath: [],
  14. active: true,
  15. },
  16. configured: true,
  17. removable: false,
  18. apiKeyEnv: 'DEEPSEEK_API_KEY',
  19. credential: missingCredential,
  20. ...overrides,
  21. }
  22. }
  23. /** A second provider the user configured themselves. */
  24. function otherRow(overrides: Partial<ProviderRow> = {}): ProviderRow {
  25. return {
  26. entry: {
  27. provider: 'hfai',
  28. displayName: 'HFAI',
  29. settingsNs: 'llm-pi-ai',
  30. settingsPath: ['providers', 'hfai'],
  31. active: true,
  32. },
  33. configured: true,
  34. removable: true,
  35. apiKeyEnv: 'HFAI_API_KEY',
  36. credential: { configured: true, source: 'file', writable: true },
  37. ...overrides,
  38. }
  39. }
  40. function state(overrides: Partial<ModelsSettingsState> = {}): ModelsSettingsState {
  41. return {
  42. status: 'ready',
  43. error: null,
  44. credentialError: null,
  45. writable: true,
  46. rows: [row()],
  47. namespaces: new Map(),
  48. ...overrides,
  49. }
  50. }
  51. describe('providerUsable', () => {
  52. it('requires a registered route and a stored key for every named reference', () => {
  53. expect(providerUsable(otherRow())).toBe(true)
  54. expect(providerUsable(otherRow({ entry: { ...otherRow().entry, active: false } }))).toBe(false)
  55. expect(providerUsable(otherRow({ credential: missingCredential }))).toBe(false)
  56. expect(providerUsable(otherRow({ credential: undefined }))).toBe(false)
  57. })
  58. it('treats a reference-free registered route as provider-native authentication', () => {
  59. expect(providerUsable(otherRow({ apiKeyEnv: undefined, credential: undefined }))).toBe(true)
  60. })
  61. })
  62. describe('onboardingReadiness', () => {
  63. it('waits for the first join and skips onboarding when the adapter directory entry is absent', () => {
  64. expect(onboardingReadiness(state({ status: 'idle', rows: [] }))).toEqual({ kind: 'loading' })
  65. expect(onboardingReadiness(state({ status: 'loading', rows: [] }))).toEqual({ kind: 'loading' })
  66. expect(onboardingReadiness(state({ rows: [] }))).toEqual({ kind: 'adapter-absent' })
  67. expect(onboardingReadiness(state({
  68. rows: [row({
  69. entry: {
  70. ...row().entry,
  71. settingsNs: '',
  72. },
  73. })],
  74. }))).toEqual({ kind: 'adapter-absent' })
  75. })
  76. it('reports a missing writable effective credential', () => {
  77. expect(onboardingReadiness(state())).toEqual({ kind: 'credential-missing' })
  78. })
  79. it('ends onboarding once any other registered provider can serve requests', () => {
  80. expect(onboardingReadiness(state({ rows: [row(), otherRow()] }))).toEqual({ kind: 'provider-ready' })
  81. // A provider the user cannot reach yet leaves the prompt in place.
  82. expect(onboardingReadiness(state({
  83. rows: [row(), otherRow({ credential: missingCredential })],
  84. }))).toEqual({ kind: 'credential-missing' })
  85. })
  86. it('accepts file and process-environment credentials without prompting', () => {
  87. expect(onboardingReadiness(state({
  88. rows: [row({ credential: { configured: true, source: 'file', writable: true } })],
  89. }))).toEqual({ kind: 'provider-ready' })
  90. expect(onboardingReadiness(state({
  91. rows: [row({ credential: { configured: true, source: 'env', writable: false } })],
  92. }))).toEqual({ kind: 'provider-ready' })
  93. })
  94. it('turns missing capabilities into diagnostics that never block the product', () => {
  95. expect(onboardingReadiness(state({ status: 'error', error: 'settings down' }))).toEqual({
  96. kind: 'unavailable',
  97. reason: 'load-failed',
  98. })
  99. expect(onboardingReadiness(state({
  100. rows: [row({ entry: { ...row().entry, active: false } })],
  101. }))).toEqual({ kind: 'unavailable', reason: 'provider-inactive' })
  102. expect(onboardingReadiness(state({
  103. credentialError: 'credentials service is absent',
  104. }))).toEqual({
  105. kind: 'unavailable',
  106. reason: 'credentials-unavailable',
  107. })
  108. expect(onboardingReadiness(state({
  109. rows: [row({ credential: undefined })],
  110. }))).toEqual({ kind: 'unavailable', reason: 'credentials-unavailable' })
  111. expect(onboardingReadiness(state({
  112. rows: [row({ credential: { configured: false, writable: false } })],
  113. }))).toEqual({ kind: 'unavailable', reason: 'credential-read-only' })
  114. expect(onboardingReadiness(state({ writable: false }))).toEqual({
  115. kind: 'unavailable',
  116. reason: 'settings-read-only',
  117. })
  118. })
  119. })