Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@ const enableProviderWhenModelsAvailableMock = vi.fn()
const updateProviderMock = vi.fn()
const useModelsMock = vi.fn()
const useProviderMock = vi.fn()
const moveMock = vi.fn()

vi.mock('@renderer/pages/settings/ProviderSettings/hooks/useMoveProviderToFirst', () => ({
useMoveProviderToFirst: () => ({
assertCanMoveProviderToFirst: vi.fn(),
moveProviderToFirst: moveMock
})
}))

vi.mock('@renderer/hooks/useModel', () => ({
useModels: (...args: any[]) => useModelsMock(...args)
Expand Down Expand Up @@ -51,6 +59,7 @@ describe('useProviderModelPullReconcile', () => {
vi.clearAllMocks()
enableProviderWhenModelsAvailableMock.mockResolvedValue(false)
useModelsMock.mockReturnValue({ models: [{ id: 'cherryin::model-1' }] })
moveMock.mockResolvedValue(undefined)
useProviderMock.mockReturnValue({
provider: { id: 'cherryin', isEnabled: false },
updateProvider: updateProviderMock
Expand All @@ -73,6 +82,7 @@ describe('useProviderModelPullReconcile', () => {
expect(enableProviderWhenModelsAvailableMock).toHaveBeenCalledWith(
{ id: 'cherryin', isEnabled: false },
updateProviderMock,
expect.objectContaining({ moveProviderToFirst: expect.any(Function) }),
2,
'pull_reconcile_up_to_date'
)
Expand All @@ -93,6 +103,7 @@ describe('useProviderModelPullReconcile', () => {
expect(enableProviderWhenModelsAvailableMock).toHaveBeenCalledWith(
{ id: 'cherryin', isEnabled: false },
updateProviderMock,
expect.objectContaining({ moveProviderToFirst: expect.any(Function) }),
0,
'pull_reconcile_up_to_date'
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ const { reconcileTriggerMock } = vi.hoisted(() => ({
}))
const useProviderMock = vi.fn()
const updateProviderMock = vi.fn()
const moveMock = vi.fn()

vi.mock('@renderer/pages/settings/ProviderSettings/hooks/useMoveProviderToFirst', () => ({
useMoveProviderToFirst: () => ({
assertCanMoveProviderToFirst: vi.fn(),
moveProviderToFirst: moveMock
})
}))

vi.mock('@data/hooks/useDataApi', () => ({
useMutation: () => ({
Expand Down Expand Up @@ -43,7 +51,9 @@ describe('usePullReconcileSubmit', () => {
reconcileTriggerMock.mockResolvedValue([])
useProviderMock.mockReset()
updateProviderMock.mockReset()
moveMock.mockReset()
updateProviderMock.mockResolvedValue(undefined)
moveMock.mockResolvedValue(undefined)
useProviderMock.mockReturnValue({
provider: { id: 'cherryin', isEnabled: true },
updateProvider: updateProviderMock
Expand Down Expand Up @@ -123,6 +133,7 @@ describe('usePullReconcileSubmit', () => {
})

expect(updateProviderMock).toHaveBeenCalledWith({ isEnabled: true })
expect(moveMock).toHaveBeenCalledWith('cherryin')
})

it('keeps a disabled provider disabled when pull reconcile leaves zero models', async () => {
Expand All @@ -142,6 +153,7 @@ describe('usePullReconcileSubmit', () => {
})

expect(updateProviderMock).not.toHaveBeenCalled()
expect(moveMock).not.toHaveBeenCalled()
})

it('surfaces reconcile failure without committing the drawer', async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useModels } from '@renderer/hooks/useModel'
import { useProvider } from '@renderer/hooks/useProvider'
import { useMoveProviderToFirst } from '@renderer/pages/settings/ProviderSettings/hooks/useMoveProviderToFirst'
import { useProviderPullReconcile as usePullPreview } from '@renderer/pages/settings/ProviderSettings/hooks/useProviderPullReconcile'
import { enableProviderWhenModelsAvailable } from '@renderer/pages/settings/ProviderSettings/utils/providerEnablement'
import { useCallback, useState } from 'react'
Expand All @@ -16,6 +17,7 @@ export function useProviderModelPullReconcile(providerId: string) {
const [pullReconcileDrawerOpen, setPullReconcileDrawerOpen] = useState(false)
const { provider, updateProvider } = useProvider(providerId)
const { models } = useModels({ providerId })
const providerReorder = useMoveProviderToFirst()

const closePullReconcile = useCallback(() => {
setPullReconcileDrawerOpen(false)
Expand All @@ -36,7 +38,13 @@ export function useProviderModelPullReconcile(providerId: string) {
if (!hasDiff) {
// Up to date: no diff to apply, but the existing local models are valid
// for the new key/host β€” enable the provider if it is currently disabled.
await enableProviderWhenModelsAvailable(provider, updateProvider, models.length, 'pull_reconcile_up_to_date')
await enableProviderWhenModelsAvailable(
provider,
updateProvider,
providerReorder,
models.length,
'pull_reconcile_up_to_date'
)
window.toast.success(
`${t('settings.models.manage.fetch_up_to_date')} ${t('settings.models.manage.fetch_up_to_date_hint')}`
)
Expand All @@ -47,7 +55,7 @@ export function useProviderModelPullReconcile(providerId: string) {
} catch {
/* toast + throw inside fetchPreview */
}
}, [models.length, provider, pullPreview, t, updateProvider])
}, [models.length, providerReorder, provider, pullPreview, t, updateProvider])

return {
openPullReconcile,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useMutation } from '@data/hooks/useDataApi'
import { loggerService } from '@logger'
import { useProvider } from '@renderer/hooks/useProvider'
import { useMoveProviderToFirst } from '@renderer/pages/settings/ProviderSettings/hooks/useMoveProviderToFirst'
import { enableProviderWhenModelsAvailable } from '@renderer/pages/settings/ProviderSettings/utils/providerEnablement'
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
Expand All @@ -24,6 +25,7 @@ type UsePullReconcileSubmitOptions = {
export function usePullReconcileSubmit({ providerId, onApplyCommitted }: UsePullReconcileSubmitOptions) {
const { t } = useTranslation()
const { provider, updateProvider } = useProvider(providerId)
const providerReorder = useMoveProviderToFirst()
const { trigger: reconcileTrigger, isLoading: applyBusy } = useMutation(
'POST',
'/providers/:providerId/models:reconcile',
Expand All @@ -44,6 +46,7 @@ export function usePullReconcileSubmit({ providerId, onApplyCommitted }: UsePull
await enableProviderWhenModelsAvailable(
provider,
updateProvider,
providerReorder,
reconciledModels.length,
'pull_reconcile_apply'
)
Expand Down Expand Up @@ -71,7 +74,7 @@ export function usePullReconcileSubmit({ providerId, onApplyCommitted }: UsePull
window.toast.error(t('settings.models.manage.sync_pull_failed'))
}
},
[onApplyCommitted, provider, providerId, reconcileTrigger, t, updateProvider]
[onApplyCommitted, providerReorder, provider, providerId, reconcileTrigger, t, updateProvider]
)

return {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { renderHook } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'

import { ProviderListNotReadyForReorderError, useMoveProviderToFirst } from '../useMoveProviderToFirst'

const readCacheMock = vi.fn()
const moveMock = vi.fn()

vi.mock('@data/hooks/useDataApi', () => ({
useReadCache: () => readCacheMock
}))

vi.mock('@data/hooks/useReorder', () => ({
useReorder: () => ({
move: moveMock,
applyReorderedList: vi.fn(),
isPending: false
})
}))

describe('useMoveProviderToFirst', () => {
beforeEach(() => {
vi.clearAllMocks()
moveMock.mockResolvedValue(undefined)
})

it('throws before moving when the provider list cache is not ready', async () => {
readCacheMock.mockReturnValue(undefined)

const { result } = renderHook(() => useMoveProviderToFirst())

expect(() => result.current.assertCanMoveProviderToFirst()).toThrow(ProviderListNotReadyForReorderError)
await expect(result.current.moveProviderToFirst('cherryin')).rejects.toThrow(ProviderListNotReadyForReorderError)
expect(moveMock).not.toHaveBeenCalled()
})

it('moves a provider to the first position when the provider list cache is ready', async () => {
readCacheMock.mockReturnValue([{ id: 'openai' }, { id: 'cherryin' }])

const { result } = renderHook(() => useMoveProviderToFirst())

expect(() => result.current.assertCanMoveProviderToFirst()).not.toThrow()
await result.current.moveProviderToFirst('cherryin')

expect(moveMock).toHaveBeenCalledWith('cherryin', { position: 'first' })
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ const useModelsMock = vi.fn()
const useProviderModelSyncMock = vi.fn()
const syncProviderModelsMock = vi.fn()
const updateProviderMock = vi.fn()
const moveMock = vi.fn()

vi.mock('@renderer/pages/settings/ProviderSettings/hooks/useMoveProviderToFirst', () => ({
useMoveProviderToFirst: () => ({
assertCanMoveProviderToFirst: vi.fn(),
moveProviderToFirst: moveMock
})
}))

vi.mock('@logger', () => ({
loggerService: {
Expand Down Expand Up @@ -45,6 +53,7 @@ describe('useProviderAutoModelSync', () => {
vi.clearAllMocks()
syncProviderModelsMock.mockResolvedValue([])
updateProviderMock.mockResolvedValue(undefined)
moveMock.mockResolvedValue(undefined)

useProviderMock.mockReturnValue({
provider: {
Expand Down Expand Up @@ -113,6 +122,7 @@ describe('useProviderAutoModelSync', () => {
renderHook(() => useProviderAutoModelSync('ollama'))

await waitFor(() => expect(updateProviderMock).toHaveBeenCalledWith({ isEnabled: true }))
expect(moveMock).toHaveBeenCalledWith('ollama')
})

it('syncs only once for the same initial eligible configuration (non-key provider)', async () => {
Expand Down Expand Up @@ -222,6 +232,7 @@ describe('useProviderAutoModelSync', () => {

await waitFor(() => expect(syncProviderModelsMock).toHaveBeenCalledTimes(1))
await waitFor(() => expect(updateProviderMock).toHaveBeenCalledWith({ isEnabled: true }))
expect(moveMock).toHaveBeenCalledWith('claude-code')
})

it('does not sync or enable a login provider until it is signed in', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,20 @@ const useAuthenticationApiKeyMock = vi.fn()
const useProviderEndpointsMock = vi.fn()
const checkApiMock = vi.fn()
const updateProviderMock = vi.fn()
const moveMock = vi.fn()
const commitInputApiKeyNowMock = vi.fn()
const showErrorDetailPopupMock = vi.fn()
const { loggerErrorMock } = vi.hoisted(() => ({
loggerErrorMock: vi.fn()
}))

vi.mock('@renderer/pages/settings/ProviderSettings/hooks/useMoveProviderToFirst', () => ({
useMoveProviderToFirst: () => ({
assertCanMoveProviderToFirst: vi.fn(),
moveProviderToFirst: moveMock
})
}))

vi.mock('react-i18next', async (importOriginal) => {
const actual = await importOriginal<object>()

Expand Down Expand Up @@ -76,6 +84,7 @@ describe('useProviderConnectionCheck', () => {
success: vi.fn()
}

moveMock.mockResolvedValue(undefined)
useProviderMock.mockReturnValue({
provider: { id: 'cherryin', name: 'CherryIN', isEnabled: false },
updateProvider: updateProviderMock
Expand Down Expand Up @@ -158,6 +167,7 @@ describe('useProviderConnectionCheck', () => {
})

expect(updateProviderMock).toHaveBeenCalledWith({ isEnabled: true })
expect(moveMock).toHaveBeenCalledWith('cherryin')
})

it('persists the pending API key before running the check and before enabling the provider', async () => {
Expand All @@ -172,6 +182,7 @@ describe('useProviderConnectionCheck', () => {

expect(commitInputApiKeyNowMock).toHaveBeenCalledTimes(1)
expect(updateProviderMock).toHaveBeenCalledWith({ isEnabled: true })
expect(moveMock).toHaveBeenCalledWith('cherryin')
// commit must run before the check (so the check validates the pending key,
// not a stale saved one) and before enabling the provider.
expect(commitInputApiKeyNowMock.mock.invocationCallOrder[0]).toBeLessThan(checkApiMock.mock.invocationCallOrder[0])
Expand Down Expand Up @@ -208,6 +219,7 @@ describe('useProviderConnectionCheck', () => {
})

it('does not patch an already enabled provider after a successful model connection check', async () => {
moveMock.mockResolvedValue(undefined)
useProviderMock.mockReturnValue({
provider: { id: 'cherryin', name: 'CherryIN', isEnabled: true },
updateProvider: updateProviderMock
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ import { useProviderEnable } from '../useProviderEnable'

const useProviderMock = vi.fn()
const useProviderMutationsMock = vi.fn()
const useReorderMock = vi.fn()
const updateProviderMock = vi.fn().mockResolvedValue(undefined)
const moveMock = vi.fn().mockResolvedValue(undefined)
const assertCanMoveProviderToFirstMock = vi.fn()

vi.mock('@renderer/hooks/useProvider', () => ({
useProvider: (...args: any[]) => useProviderMock(...args),
useProviderMutations: (...args: any[]) => useProviderMutationsMock(...args)
}))

vi.mock('@data/hooks/useReorder', () => ({
useReorder: (...args: any[]) => useReorderMock(...args)
vi.mock('@renderer/pages/settings/ProviderSettings/hooks/useMoveProviderToFirst', () => ({
useMoveProviderToFirst: () => ({
assertCanMoveProviderToFirst: assertCanMoveProviderToFirstMock,
moveProviderToFirst: moveMock
})
}))

describe('useProviderEnable', () => {
Expand All @@ -27,9 +30,7 @@ describe('useProviderEnable', () => {
useProviderMutationsMock.mockReturnValue({
updateProvider: updateProviderMock
})
useReorderMock.mockReturnValue({
move: moveMock
})
assertCanMoveProviderToFirstMock.mockImplementation(() => undefined)
})

it('updates only isEnabled when disabling a provider', async () => {
Expand All @@ -51,7 +52,7 @@ describe('useProviderEnable', () => {
})

expect(updateProviderMock).toHaveBeenCalledWith({ isEnabled: true })
expect(moveMock).toHaveBeenCalledWith('openai', { position: 'first' })
expect(moveMock).toHaveBeenCalledWith('openai')
})

it('does nothing when the provider is missing', async () => {
Expand Down Expand Up @@ -90,7 +91,32 @@ describe('useProviderEnable', () => {
expect(thrown).toBe(moveError)
expect(updateProviderMock).toHaveBeenCalledTimes(2)
expect(updateProviderMock).toHaveBeenNthCalledWith(1, { isEnabled: true })
expect(moveMock).toHaveBeenCalledWith('openai', { position: 'first' })
expect(moveMock).toHaveBeenCalledWith('openai')
expect(updateProviderMock).toHaveBeenNthCalledWith(2, { isEnabled: false })
})

it('does not enable when pin-to-top preconditions are not ready', async () => {
useProviderMock.mockReturnValue({
provider: { id: 'openai', isEnabled: false }
})
const reorderError = new Error('provider list cache is not ready')
assertCanMoveProviderToFirstMock.mockImplementationOnce(() => {
throw reorderError
})

const { result } = renderHook(() => useProviderEnable('openai'))

let thrown: unknown = null
await act(async () => {
try {
await result.current.toggleProviderEnabled(true)
} catch (error) {
thrown = error
}
})

expect(thrown).toBe(reorderError)
expect(updateProviderMock).not.toHaveBeenCalled()
expect(moveMock).not.toHaveBeenCalled()
})
})
Loading
Loading