/ tests / chat-page-settings-modal.test.ts
chat-page-settings-modal.test.ts
  1  import { describe, expect, it, vi } from 'vitest'
  2  
  3  import type {
  4    ProviderCatalogItem,
  5    SettingsResponsePayload,
  6    SettingsUpdatePayload,
  7  } from '@/lib/shared/chat'
  8  import type { ProviderWithSettings } from '@/lib/shared/providers'
  9  import type { SettingsFormState } from '@/features/chat/components/chat-page/types'
 10  import { saveSettingsFromModal } from '@/features/chat/chat-page/settings-modal'
 11  import { updateChatSettings } from '@/features/chat/api'
 12  
 13  vi.mock('@/features/chat/api', () => ({
 14    updateChatSettings: vi.fn(),
 15  }))
 16  
 17  function makeForm(overrides: Partial<SettingsFormState> = {}): SettingsFormState {
 18    const base: SettingsFormState = {
 19      sessionChatModelOverride: '',
 20      sessionThinkingLevelOverride: '',
 21      sessionReasoningLevelOverride: '',
 22      sessionProviderPreset: 'inherit',
 23      sessionInheritedProviderPreset: 'openai',
 24      sessionProviderBaseUrl: '',
 25      sessionProviderEndpointMode: 'auto',
 26      sessionProviderApiKey: '',
 27      defaultsChatModel: '',
 28      defaultsProviderModelById: {},
 29      defaultsProviderModelOpenAi: '',
 30      defaultsProviderModelRouter: '',
 31      defaultsProviderModelCustom: '',
 32      defaultsEmbeddingModel: '',
 33      defaultsTranscriptionModel: '',
 34      defaultsEmbeddingProviderId: '',
 35      defaultsTranscriptionProviderId: '',
 36      defaultsThinkingLevel: '',
 37      defaultsReasoningLevel: '',
 38      defaultsProviderPreset: 'openai',
 39      defaultsOpenAiBaseUrl: 'https://api.openai.com/v1',
 40      defaultsOpenAiApiKey: '',
 41      defaultsRouterBaseUrl: '',
 42      defaultsRouterApiKey: '',
 43      defaultsProviderBaseUrl: '',
 44      defaultsProviderEndpointMode: '',
 45      defaultsProviderApiKey: '',
 46      defaultsToolCallLogRetentionDays: '30',
 47    }
 48  
 49    return {
 50      ...base,
 51      ...overrides,
 52      defaultsProviderModelById: {
 53        ...base.defaultsProviderModelById,
 54        ...(overrides.defaultsProviderModelById ?? {}),
 55      },
 56    }
 57  }
 58  
 59  function makeCatalog(): ProviderCatalogItem[] {
 60    return [
 61      {
 62        id: 'openai',
 63        label: 'OpenAI',
 64        available: true,
 65        baseUrl: 'https://api.openai.com/v1',
 66        description: null,
 67        defaultChatModel: null,
 68        models: [],
 69        modelCapabilities: {},
 70      },
 71      {
 72        id: 'example',
 73        label: 'Example',
 74        available: true,
 75        baseUrl: 'https://example.com/v1',
 76        description: null,
 77        defaultChatModel: null,
 78        models: ['minimax-m2'],
 79        modelCapabilities: {
 80          'minimax-m2': ['reasoning'],
 81        },
 82      },
 83    ]
 84  }
 85  
 86  function makeProviders(): ProviderWithSettings[] {
 87    return [
 88      {
 89        id: 'openai',
 90        name: 'OpenAI',
 91        type: 'openai',
 92        isCustom: false,
 93        settings: {
 94          apiHost: 'https://api.openai.com/v1',
 95          apiPath: '/responses',
 96        },
 97      },
 98      {
 99        id: 'example',
100        name: 'Example',
101        type: 'openai',
102        isCustom: true,
103        settings: {
104          apiHost: 'https://example.com/v1',
105          apiPath: '/chat/completions',
106          models: [{ modelId: 'minimax-m2', type: 'chat', capabilities: ['reasoning'] }],
107        },
108      },
109    ] as ProviderWithSettings[]
110  }
111  
112  function makeSnapshot(): SettingsResponsePayload {
113    return {
114      ok: true,
115      session: {
116        sessionId: 'session-1',
117        chatModelOverride: null,
118        activeChatModel: 'minimax-m2',
119        activeProviderModelDefault: null,
120        thinkingLevelOverride: null,
121        activeThinkingLevel: null,
122        reasoningLevelOverride: null,
123        activeReasoningLevel: 'off',
124        providerOverrideActive: false,
125        providerProfileId: 'openai',
126        providerEndpointMode: 'auto',
127        providerApiKeyConfigured: true,
128        providerLabel: 'OpenAI',
129        providerSourceLabel: 'global default (OpenAI)',
130      },
131      defaults: {
132        chatModel: null,
133        embeddingModel: null,
134        transcriptionModel: null,
135        embeddingProviderId: null,
136        transcriptionProviderId: null,
137        providerModelDefaults: {},
138        thinkingLevel: null,
139        reasoningLevel: null,
140        defaultProviderId: 'openai',
141        providerEndpointMode: 'auto',
142        webSearchProvider: null,
143        webSearchMaxResults: null,
144        webFetchMaxBytes: null,
145        toolCallLogRetentionDays: 30,
146        updatedAt: null,
147      },
148      providerCatalog: makeCatalog(),
149      providers: makeProviders(),
150    }
151  }
152  
153  describe('saveSettingsFromModal', () => {
154    it('persists session/default provider selection by provider id', async () => {
155      const updateMock = vi.mocked(updateChatSettings)
156      updateMock.mockResolvedValue(makeSnapshot())
157  
158      let payloadSent: SettingsUpdatePayload | undefined
159      updateMock.mockImplementation(async (payload) => {
160        payloadSent = payload
161        return makeSnapshot()
162      })
163  
164      await saveSettingsFromModal({
165        sessionId: 'session-1',
166        settingsForm: makeForm({
167          sessionProviderPreset: 'example',
168          defaultsProviderPreset: 'example',
169          defaultsProviderModelById: {
170            example: 'minimax-m2',
171          },
172        }),
173        providerCatalog: makeCatalog(),
174        providers: makeProviders(),
175        settingsSnapshot: null,
176        setIsSavingSettings: () => undefined,
177        setSettingsMutationError: () => undefined,
178        setSettingsSnapshot: () => undefined,
179        setSettingsForm: () => undefined,
180        setIsSettingsModalOpen: () => undefined,
181        setStatusLine: () => undefined,
182      })
183  
184      expect(updateMock).toHaveBeenCalledTimes(1)
185      const sentPayload = payloadSent as SettingsUpdatePayload
186      expect(sentPayload.session?.provider).toMatchObject({
187        providerId: 'example',
188      })
189      expect(sentPayload.defaults?.defaultProviderId).toBe('example')
190      expect(sentPayload.defaults?.providerModelDefaults?.example).toBe('minimax-m2')
191      expect(sentPayload.defaults?.providers?.find((provider) => provider.id === 'example'))
192        .toBeTruthy()
193    })
194  })