mirror of
https://github.com/langgenius/dify.git
synced 2026-03-10 19:21:50 +08:00
test(web): increase test coverage for model-provider-page folder (#32374)
This commit is contained in:
parent
84533cbfe0
commit
ad3a195734
@ -1,10 +1,6 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import SyncButton from './sync-button'
|
||||
|
||||
vi.mock('ahooks', () => ({
|
||||
useBoolean: () => [false, { setTrue: vi.fn(), setFalse: vi.fn() }],
|
||||
}))
|
||||
|
||||
describe('SyncButton', () => {
|
||||
describe('Rendering', () => {
|
||||
it('should render without crashing', () => {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { act, render, screen, waitFor } from '@testing-library/react'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { audioToText } from '@/service/share'
|
||||
@ -8,7 +8,6 @@ const { mockState, MockRecorder } = vi.hoisted(() => {
|
||||
const state = {
|
||||
params: {} as Record<string, string>,
|
||||
pathname: '/test',
|
||||
rafCallback: undefined as (() => void) | undefined,
|
||||
recorderInstances: [] as unknown[],
|
||||
startOverride: null as (() => Promise<void>) | null,
|
||||
analyseData: new Uint8Array(1024).fill(150) as Uint8Array,
|
||||
@ -55,13 +54,6 @@ vi.mock('./utils', () => ({
|
||||
convertToMp3: vi.fn(() => new Blob(['test'], { type: 'audio/mp3' })),
|
||||
}))
|
||||
|
||||
vi.mock('ahooks', () => ({
|
||||
useRafInterval: vi.fn((fn: () => void) => {
|
||||
mockState.rafCallback = fn
|
||||
return vi.fn()
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('VoiceInput', () => {
|
||||
const onConverted = vi.fn()
|
||||
const onCancel = vi.fn()
|
||||
@ -70,7 +62,6 @@ describe('VoiceInput', () => {
|
||||
vi.clearAllMocks()
|
||||
mockState.params = {}
|
||||
mockState.pathname = '/test'
|
||||
mockState.rafCallback = undefined
|
||||
mockState.recorderInstances = []
|
||||
mockState.startOverride = null
|
||||
|
||||
@ -101,21 +92,6 @@ describe('VoiceInput', () => {
|
||||
expect(screen.getByTestId('voice-input-timer')).toHaveTextContent('00:00')
|
||||
})
|
||||
|
||||
it('should increment timer via useRafInterval callback', async () => {
|
||||
render(<VoiceInput onConverted={onConverted} onCancel={onCancel} />)
|
||||
await screen.findByText('common.voiceInput.speaking')
|
||||
|
||||
act(() => {
|
||||
mockState.rafCallback?.()
|
||||
})
|
||||
expect(screen.getByTestId('voice-input-timer')).toHaveTextContent('00:01')
|
||||
|
||||
act(() => {
|
||||
mockState.rafCallback?.()
|
||||
})
|
||||
expect(screen.getByTestId('voice-input-timer')).toHaveTextContent('00:02')
|
||||
})
|
||||
|
||||
it('should call onCancel when recording start fails', async () => {
|
||||
mockState.startOverride = () => Promise.reject(new Error('Permission denied'))
|
||||
|
||||
@ -177,32 +153,6 @@ describe('VoiceInput', () => {
|
||||
expect(onCancel).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should automatically stop recording after 600 seconds', async () => {
|
||||
vi.mocked(audioToText).mockResolvedValueOnce({ text: 'auto stopped' })
|
||||
mockState.params = { token: 'abc' }
|
||||
|
||||
render(<VoiceInput onConverted={onConverted} onCancel={onCancel} />)
|
||||
await screen.findByTestId('voice-input-stop')
|
||||
|
||||
for (let i = 0; i < 600; i++)
|
||||
act(() => { mockState.rafCallback?.() })
|
||||
|
||||
await waitFor(() => {
|
||||
expect(onConverted).toHaveBeenCalledWith('auto stopped')
|
||||
})
|
||||
})
|
||||
|
||||
it('should show red timer text after 500 seconds', async () => {
|
||||
render(<VoiceInput onConverted={onConverted} onCancel={onCancel} />)
|
||||
await screen.findByTestId('voice-input-stop')
|
||||
|
||||
for (let i = 0; i < 501; i++)
|
||||
act(() => { mockState.rafCallback?.() })
|
||||
|
||||
const timer = screen.getByTestId('voice-input-timer')
|
||||
expect(timer.className).toContain('text-[#F04438]')
|
||||
})
|
||||
|
||||
it('should draw on canvas with low data values triggering v < 128 clamp', async () => {
|
||||
mockState.analyseData = new Uint8Array(1024).fill(50)
|
||||
|
||||
|
||||
@ -1,8 +1,22 @@
|
||||
import type { Mock } from 'vitest'
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import type {
|
||||
DefaultModelResponse,
|
||||
Model,
|
||||
} from './declarations'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { useLocale } from '@/context/i18n'
|
||||
import { useLanguage } from './hooks'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
ModelTypeEnum,
|
||||
} from './declarations'
|
||||
import {
|
||||
useLanguage,
|
||||
useModelList,
|
||||
useProviderCredentialsAndLoadBalancing,
|
||||
useSystemDefaultModelAndModelList,
|
||||
} from './hooks'
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@tanstack/react-query', () => ({
|
||||
useQuery: vi.fn(),
|
||||
useQueryClient: vi.fn(() => ({
|
||||
@ -10,17 +24,6 @@ vi.mock('@tanstack/react-query', () => ({
|
||||
})),
|
||||
}))
|
||||
|
||||
// mock use-context-selector
|
||||
vi.mock('use-context-selector', () => ({
|
||||
useContext: vi.fn(),
|
||||
createContext: () => ({
|
||||
Provider: ({ children }: any) => children,
|
||||
Consumer: ({ children }: any) => children(null),
|
||||
}),
|
||||
useContextSelector: vi.fn(),
|
||||
}))
|
||||
|
||||
// mock service/common functions
|
||||
vi.mock('@/service/common', () => ({
|
||||
fetchDefaultModal: vi.fn(),
|
||||
fetchModelList: vi.fn(),
|
||||
@ -30,63 +33,129 @@ vi.mock('@/service/common', () => ({
|
||||
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
commonQueryKeys: {
|
||||
modelProviders: ['common', 'model-providers'],
|
||||
modelList: (type: string) => ['model-list', type],
|
||||
modelProviders: ['model-providers'],
|
||||
defaultModel: (type: string) => ['default-model', type],
|
||||
},
|
||||
}))
|
||||
|
||||
// mock context hooks
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useLocale: vi.fn(() => 'en-US'),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: vi.fn(),
|
||||
}))
|
||||
const { useQuery } = await import('@tanstack/react-query')
|
||||
const { fetchModelList, fetchModelProviderCredentials } = await import('@/service/common')
|
||||
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContextSelector: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/event-emitter', () => ({
|
||||
useEventEmitterContextContext: vi.fn(),
|
||||
}))
|
||||
|
||||
// mock plugins
|
||||
vi.mock('@/app/components/plugins/marketplace/hooks', () => ({
|
||||
useMarketplacePlugins: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/plugins/marketplace/utils', () => ({
|
||||
getMarketplacePluginsByCollectionId: vi.fn(),
|
||||
}))
|
||||
|
||||
vi.mock('./provider-added-card', () => ({
|
||||
default: vi.fn(),
|
||||
}))
|
||||
|
||||
afterAll(() => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('useLanguage', () => {
|
||||
it('should replace hyphen with underscore in locale', () => {
|
||||
;(useLocale as Mock).mockReturnValue('en-US')
|
||||
const { result } = renderHook(() => useLanguage())
|
||||
expect(result.current).toBe('en_US')
|
||||
describe('hooks', () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should return locale as is if no hyphen exists', () => {
|
||||
;(useLocale as Mock).mockReturnValue('enUS')
|
||||
describe('useLanguage', () => {
|
||||
it('should replace hyphen with underscore in locale', () => {
|
||||
;(useLocale as Mock).mockReturnValue('en-US')
|
||||
const { result } = renderHook(() => useLanguage())
|
||||
expect(result.current).toBe('en_US')
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useLanguage())
|
||||
expect(result.current).toBe('enUS')
|
||||
it('should return locale as is if no hyphen exists', () => {
|
||||
;(useLocale as Mock).mockReturnValue('enUS')
|
||||
const { result } = renderHook(() => useLanguage())
|
||||
expect(result.current).toBe('enUS')
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle multiple hyphens', () => {
|
||||
;(useLocale as Mock).mockReturnValue('zh-Hans-CN')
|
||||
describe('useSystemDefaultModelAndModelList', () => {
|
||||
it('should return default model state', () => {
|
||||
const defaultModel = {
|
||||
provider: {
|
||||
provider: 'openai',
|
||||
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
|
||||
},
|
||||
model: 'gpt-3.5',
|
||||
model_type: ModelTypeEnum.textGeneration,
|
||||
} as unknown as DefaultModelResponse
|
||||
const modelList = [{ provider: 'openai', models: [{ model: 'gpt-3.5' }] }] as unknown as Model[]
|
||||
const { result } = renderHook(() => useSystemDefaultModelAndModelList(defaultModel, modelList))
|
||||
|
||||
const { result } = renderHook(() => useLanguage())
|
||||
expect(result.current).toBe('zh_Hans-CN')
|
||||
expect(result.current[0]).toEqual({ model: 'gpt-3.5', provider: 'openai' })
|
||||
})
|
||||
|
||||
it('should update default model state', () => {
|
||||
const defaultModel = {
|
||||
provider: {
|
||||
provider: 'openai',
|
||||
icon_small: { en_US: 'icon', zh_Hans: 'icon' },
|
||||
},
|
||||
model: 'gpt-3.5',
|
||||
model_type: ModelTypeEnum.textGeneration,
|
||||
} as any
|
||||
const modelList = [{ provider: 'openai', models: [{ model: 'gpt-3.5' }] }] as any
|
||||
const { result } = renderHook(() => useSystemDefaultModelAndModelList(defaultModel, modelList))
|
||||
|
||||
const newModel = { model: 'gpt-4', provider: 'openai' }
|
||||
act(() => {
|
||||
result.current[1](newModel)
|
||||
})
|
||||
|
||||
expect(result.current[0]).toEqual(newModel)
|
||||
})
|
||||
})
|
||||
|
||||
describe('useProviderCredentialsAndLoadBalancing', () => {
|
||||
it('should fetch predefined credentials', async () => {
|
||||
(useQuery as Mock).mockReturnValue({
|
||||
data: { credentials: { key: 'value' }, load_balancing: { enabled: true } },
|
||||
isPending: false,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useProviderCredentialsAndLoadBalancing(
|
||||
'openai',
|
||||
ConfigurationMethodEnum.predefinedModel,
|
||||
true,
|
||||
undefined,
|
||||
'cred-id',
|
||||
))
|
||||
|
||||
expect(result.current.credentials).toEqual({ key: 'value' })
|
||||
expect(result.current.loadBalancing).toEqual({ enabled: true })
|
||||
expect(fetchModelProviderCredentials).not.toHaveBeenCalled() // useQuery calls it, but we blocked it with mockReturnValue
|
||||
})
|
||||
|
||||
it('should fetch custom credentials', () => {
|
||||
(useQuery as Mock).mockReturnValue({
|
||||
data: { credentials: { key: 'value' }, load_balancing: { enabled: true } },
|
||||
isPending: false,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useProviderCredentialsAndLoadBalancing(
|
||||
'openai',
|
||||
ConfigurationMethodEnum.customizableModel,
|
||||
true,
|
||||
{ __model_name: 'gpt-4', __model_type: ModelTypeEnum.textGeneration },
|
||||
'cred-id',
|
||||
))
|
||||
|
||||
expect(result.current.credentials).toEqual({
|
||||
key: 'value',
|
||||
__model_name: 'gpt-4',
|
||||
__model_type: ModelTypeEnum.textGeneration,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('useModelList', () => {
|
||||
it('should fetch model list', () => {
|
||||
(useQuery as Mock).mockReturnValue({
|
||||
data: { data: [{ model: 'gpt-4' }] },
|
||||
isPending: false,
|
||||
refetch: vi.fn(),
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useModelList(ModelTypeEnum.textGeneration))
|
||||
|
||||
expect(result.current.data).toEqual([{ model: 'gpt-4' }])
|
||||
expect(fetchModelList).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -0,0 +1,199 @@
|
||||
import { act, render, screen } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
CurrentSystemQuotaTypeEnum,
|
||||
CustomConfigurationStatusEnum,
|
||||
QuotaUnitEnum,
|
||||
} from './declarations'
|
||||
import ModelProviderPage from './index'
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => ({
|
||||
mutateCurrentWorkspace: vi.fn(),
|
||||
isValidatingCurrentWorkspace: false,
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockGlobalState = {
|
||||
systemFeatures: { enable_marketplace: true },
|
||||
}
|
||||
|
||||
const mockQuotaConfig = {
|
||||
quota_type: CurrentSystemQuotaTypeEnum.free,
|
||||
quota_unit: QuotaUnitEnum.times,
|
||||
quota_limit: 100,
|
||||
quota_used: 1,
|
||||
last_used: 0,
|
||||
is_valid: true,
|
||||
}
|
||||
|
||||
vi.mock('@/context/global-public-context', () => ({
|
||||
useGlobalPublicStore: (selector: (s: { systemFeatures: { enable_marketplace: boolean } }) => unknown) => selector(mockGlobalState),
|
||||
}))
|
||||
|
||||
const mockProviders = [
|
||||
{
|
||||
provider: 'openai',
|
||||
label: { en_US: 'OpenAI' },
|
||||
custom_configuration: { status: CustomConfigurationStatusEnum.active },
|
||||
system_configuration: {
|
||||
enabled: false,
|
||||
current_quota_type: CurrentSystemQuotaTypeEnum.free,
|
||||
quota_configurations: [mockQuotaConfig],
|
||||
},
|
||||
},
|
||||
{
|
||||
provider: 'anthropic',
|
||||
label: { en_US: 'Anthropic' },
|
||||
custom_configuration: { status: CustomConfigurationStatusEnum.noConfigure },
|
||||
system_configuration: {
|
||||
enabled: false,
|
||||
current_quota_type: CurrentSystemQuotaTypeEnum.free,
|
||||
quota_configurations: [mockQuotaConfig],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => ({
|
||||
modelProviders: mockProviders,
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockDefaultModelState = {
|
||||
data: null,
|
||||
isLoading: false,
|
||||
}
|
||||
|
||||
vi.mock('./hooks', () => ({
|
||||
useDefaultModel: () => mockDefaultModelState,
|
||||
}))
|
||||
|
||||
vi.mock('./install-from-marketplace', () => ({
|
||||
default: () => <div data-testid="install-from-marketplace" />,
|
||||
}))
|
||||
|
||||
vi.mock('./provider-added-card', () => ({
|
||||
default: ({ provider }: { provider: { provider: string } }) => <div data-testid="provider-card">{provider.provider}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('./provider-added-card/quota-panel', () => ({
|
||||
default: () => <div data-testid="quota-panel" />,
|
||||
}))
|
||||
|
||||
vi.mock('./system-model-selector', () => ({
|
||||
default: () => <div data-testid="system-model-selector" />,
|
||||
}))
|
||||
|
||||
describe('ModelProviderPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.clearAllMocks()
|
||||
mockGlobalState.systemFeatures.enable_marketplace = true
|
||||
mockDefaultModelState.data = null
|
||||
mockDefaultModelState.isLoading = false
|
||||
mockProviders.splice(0, mockProviders.length, {
|
||||
provider: 'openai',
|
||||
label: { en_US: 'OpenAI' },
|
||||
custom_configuration: { status: CustomConfigurationStatusEnum.active },
|
||||
system_configuration: {
|
||||
enabled: false,
|
||||
current_quota_type: CurrentSystemQuotaTypeEnum.free,
|
||||
quota_configurations: [mockQuotaConfig],
|
||||
},
|
||||
}, {
|
||||
provider: 'anthropic',
|
||||
label: { en_US: 'Anthropic' },
|
||||
custom_configuration: { status: CustomConfigurationStatusEnum.noConfigure },
|
||||
system_configuration: {
|
||||
enabled: false,
|
||||
current_quota_type: CurrentSystemQuotaTypeEnum.free,
|
||||
quota_configurations: [mockQuotaConfig],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('should render main elements', () => {
|
||||
render(<ModelProviderPage searchText="" />)
|
||||
expect(screen.getByText('common.modelProvider.models')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('system-model-selector')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('install-from-marketplace')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render configured and not configured providers sections', () => {
|
||||
render(<ModelProviderPage searchText="" />)
|
||||
expect(screen.getByText('openai')).toBeInTheDocument()
|
||||
expect(screen.getByText('common.modelProvider.toBeConfigured')).toBeInTheDocument()
|
||||
expect(screen.getByText('anthropic')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should filter providers based on search text', () => {
|
||||
render(<ModelProviderPage searchText="open" />)
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(600)
|
||||
})
|
||||
expect(screen.getByText('openai')).toBeInTheDocument()
|
||||
expect(screen.queryByText('anthropic')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show empty state if no configured providers match', () => {
|
||||
render(<ModelProviderPage searchText="non-existent" />)
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(600)
|
||||
})
|
||||
expect(screen.getByText('common.modelProvider.emptyProviderTitle')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide marketplace section when marketplace feature is disabled', () => {
|
||||
mockGlobalState.systemFeatures.enable_marketplace = false
|
||||
|
||||
render(<ModelProviderPage searchText="" />)
|
||||
|
||||
expect(screen.queryByTestId('install-from-marketplace')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should prioritize fixed providers in visible order', () => {
|
||||
mockProviders.splice(0, mockProviders.length, {
|
||||
provider: 'zeta-provider',
|
||||
label: { en_US: 'Zeta Provider' },
|
||||
custom_configuration: { status: CustomConfigurationStatusEnum.active },
|
||||
system_configuration: {
|
||||
enabled: false,
|
||||
current_quota_type: CurrentSystemQuotaTypeEnum.free,
|
||||
quota_configurations: [mockQuotaConfig],
|
||||
},
|
||||
}, {
|
||||
provider: 'langgenius/anthropic/anthropic',
|
||||
label: { en_US: 'Anthropic Fixed' },
|
||||
custom_configuration: { status: CustomConfigurationStatusEnum.active },
|
||||
system_configuration: {
|
||||
enabled: false,
|
||||
current_quota_type: CurrentSystemQuotaTypeEnum.free,
|
||||
quota_configurations: [mockQuotaConfig],
|
||||
},
|
||||
}, {
|
||||
provider: 'langgenius/openai/openai',
|
||||
label: { en_US: 'OpenAI Fixed' },
|
||||
custom_configuration: { status: CustomConfigurationStatusEnum.noConfigure },
|
||||
system_configuration: {
|
||||
enabled: true,
|
||||
current_quota_type: CurrentSystemQuotaTypeEnum.free,
|
||||
quota_configurations: [mockQuotaConfig],
|
||||
},
|
||||
})
|
||||
|
||||
render(<ModelProviderPage searchText="" />)
|
||||
|
||||
const renderedProviders = screen.getAllByTestId('provider-card').map(item => item.textContent)
|
||||
expect(renderedProviders).toEqual([
|
||||
'langgenius/openai/openai',
|
||||
'langgenius/anthropic/anthropic',
|
||||
'zeta-provider',
|
||||
])
|
||||
expect(screen.queryByText('common.modelProvider.toBeConfigured')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,109 @@
|
||||
import type { Mock } from 'vitest'
|
||||
import type { ModelProvider } from './declarations'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { useMarketplaceAllPlugins } from './hooks'
|
||||
import InstallFromMarketplace from './install-from-marketplace'
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('next/link', () => ({
|
||||
default: ({ children, href }: { children: React.ReactNode, href: string }) => <a href={href}>{children}</a>,
|
||||
}))
|
||||
|
||||
vi.mock('next-themes', () => ({
|
||||
useTheme: () => ({ theme: 'light' }),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/divider', () => ({
|
||||
default: () => <div data-testid="divider" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/loading', () => ({
|
||||
default: () => <div data-testid="loading" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/plugins/marketplace/list', () => ({
|
||||
default: ({ plugins, cardRender }: { plugins: { plugin_id: string, name: string, type?: string }[], cardRender: (plugin: { plugin_id: string, name: string, type?: string }) => React.ReactNode }) => (
|
||||
<div data-testid="plugin-list">
|
||||
{plugins.map(p => (
|
||||
<div key={p.plugin_id} data-testid="plugin-item">
|
||||
{cardRender(p)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/plugins/provider-card', () => ({
|
||||
default: ({ payload }: { payload: { name: string } }) => <div>{payload.name}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('./hooks', () => ({
|
||||
useMarketplaceAllPlugins: vi.fn(() => ({
|
||||
plugins: [],
|
||||
isLoading: false,
|
||||
})),
|
||||
}))
|
||||
|
||||
describe('InstallFromMarketplace', () => {
|
||||
const mockProviders = [] as ModelProvider[]
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render expanded by default', () => {
|
||||
render(<InstallFromMarketplace providers={mockProviders} searchText="" />)
|
||||
expect(screen.getByText('common.modelProvider.installProvider')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('plugin-list')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should collapse when clicked', () => {
|
||||
render(<InstallFromMarketplace providers={mockProviders} searchText="" />)
|
||||
fireEvent.click(screen.getByText('common.modelProvider.installProvider'))
|
||||
expect(screen.queryByTestId('plugin-list')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show loading state', () => {
|
||||
(useMarketplaceAllPlugins as unknown as Mock).mockReturnValue({
|
||||
plugins: [],
|
||||
isLoading: true,
|
||||
})
|
||||
|
||||
render(<InstallFromMarketplace providers={mockProviders} searchText="" />)
|
||||
// It's expanded by default, so loading should show immediately
|
||||
expect(screen.getByTestId('loading')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should list plugins', () => {
|
||||
(useMarketplaceAllPlugins as unknown as Mock).mockReturnValue({
|
||||
plugins: [{ plugin_id: '1', name: 'Plugin 1' }],
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
render(<InstallFromMarketplace providers={mockProviders} searchText="" />)
|
||||
// Expanded by default
|
||||
expect(screen.getByText('Plugin 1')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide bundle plugins from the list', () => {
|
||||
(useMarketplaceAllPlugins as unknown as Mock).mockReturnValue({
|
||||
plugins: [
|
||||
{ plugin_id: '1', name: 'Plugin 1', type: 'plugin' },
|
||||
{ plugin_id: '2', name: 'Bundle 1', type: 'bundle' },
|
||||
],
|
||||
isLoading: false,
|
||||
})
|
||||
|
||||
render(<InstallFromMarketplace providers={mockProviders} searchText="" />)
|
||||
|
||||
expect(screen.getByText('Plugin 1')).toBeInTheDocument()
|
||||
expect(screen.queryByText('Bundle 1')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render discovery link', () => {
|
||||
render(<InstallFromMarketplace providers={mockProviders} searchText="" />)
|
||||
expect(screen.getByText('plugin.marketplace.difyMarketplace')).toHaveAttribute('href')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,61 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import DeprecatedModelTrigger from './deprecated-model-trigger'
|
||||
|
||||
vi.mock('../model-icon', () => ({
|
||||
default: ({ modelName }: { modelName: string }) => <span>{modelName}</span>,
|
||||
}))
|
||||
|
||||
const mockUseProviderContext = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: mockUseProviderContext,
|
||||
}))
|
||||
|
||||
describe('DeprecatedModelTrigger', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseProviderContext.mockReturnValue({
|
||||
modelProviders: [{ provider: 'someone-else' }, { provider: 'openai' }],
|
||||
})
|
||||
})
|
||||
|
||||
it('should render model name', () => {
|
||||
render(<DeprecatedModelTrigger modelName="gpt-deprecated" providerName="openai" />)
|
||||
expect(screen.getAllByText('gpt-deprecated').length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should show deprecated tooltip when warn icon is hovered', async () => {
|
||||
const { container } = render(
|
||||
<DeprecatedModelTrigger
|
||||
modelName="gpt-deprecated"
|
||||
providerName="openai"
|
||||
showWarnIcon
|
||||
/>,
|
||||
)
|
||||
|
||||
const tooltipTrigger = container.querySelector('[data-state]') as HTMLElement
|
||||
fireEvent.mouseEnter(tooltipTrigger)
|
||||
|
||||
expect(await screen.findByText('common.modelProvider.deprecated')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render when provider is not found', () => {
|
||||
mockUseProviderContext.mockReturnValue({
|
||||
modelProviders: [{ provider: 'someone-else' }],
|
||||
})
|
||||
|
||||
render(<DeprecatedModelTrigger modelName="gpt-deprecated" providerName="openai" />)
|
||||
expect(screen.getAllByText('gpt-deprecated').length).toBeGreaterThan(0)
|
||||
})
|
||||
|
||||
it('should not show deprecated tooltip when warn icon is disabled', async () => {
|
||||
render(
|
||||
<DeprecatedModelTrigger
|
||||
modelName="gpt-deprecated"
|
||||
providerName="openai"
|
||||
showWarnIcon={false}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByText('common.modelProvider.deprecated')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,13 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import EmptyTrigger from './empty-trigger'
|
||||
|
||||
describe('EmptyTrigger', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should render configure model text', () => {
|
||||
render(<EmptyTrigger open={false} />)
|
||||
expect(screen.getByText('plugin.detailPanel.configureModel')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,50 @@
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import {
|
||||
ModelFeatureEnum,
|
||||
ModelFeatureTextEnum,
|
||||
} from '../declarations'
|
||||
import FeatureIcon from './feature-icon'
|
||||
|
||||
describe('FeatureIcon', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should show feature label when showFeaturesLabel is true', () => {
|
||||
render(
|
||||
<>
|
||||
<FeatureIcon feature={ModelFeatureEnum.vision} showFeaturesLabel />
|
||||
<FeatureIcon feature={ModelFeatureEnum.document} showFeaturesLabel />
|
||||
<FeatureIcon feature={ModelFeatureEnum.audio} showFeaturesLabel />
|
||||
<FeatureIcon feature={ModelFeatureEnum.video} showFeaturesLabel />
|
||||
</>,
|
||||
)
|
||||
|
||||
expect(screen.getByText(ModelFeatureTextEnum.vision)).toBeInTheDocument()
|
||||
expect(screen.getByText(ModelFeatureTextEnum.document)).toBeInTheDocument()
|
||||
expect(screen.getByText(ModelFeatureTextEnum.audio)).toBeInTheDocument()
|
||||
expect(screen.getByText(ModelFeatureTextEnum.video)).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show tooltip content on hover when showFeaturesLabel is false', async () => {
|
||||
const cases: Array<{ feature: ModelFeatureEnum, text: string }> = [
|
||||
{ feature: ModelFeatureEnum.vision, text: ModelFeatureTextEnum.vision },
|
||||
{ feature: ModelFeatureEnum.document, text: ModelFeatureTextEnum.document },
|
||||
{ feature: ModelFeatureEnum.audio, text: ModelFeatureTextEnum.audio },
|
||||
{ feature: ModelFeatureEnum.video, text: ModelFeatureTextEnum.video },
|
||||
]
|
||||
|
||||
for (const { feature, text } of cases) {
|
||||
const { container, unmount } = render(<FeatureIcon feature={feature} />)
|
||||
fireEvent.mouseEnter(container.firstElementChild as HTMLElement)
|
||||
expect(await screen.findByText(`common.modelProvider.featureSupported:{"feature":"${text}"}`))
|
||||
.toBeInTheDocument()
|
||||
unmount()
|
||||
}
|
||||
})
|
||||
|
||||
it('should render nothing for unsupported feature', () => {
|
||||
const { container } = render(<FeatureIcon feature={ModelFeatureEnum.toolCall} />)
|
||||
expect(container).toBeEmptyDOMElement()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,126 @@
|
||||
import type { Model, ModelItem } from '../declarations'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
ModelStatusEnum,
|
||||
ModelTypeEnum,
|
||||
} from '../declarations'
|
||||
import ModelSelector from './index'
|
||||
|
||||
vi.mock('./model-trigger', () => ({
|
||||
default: () => <div>model-trigger</div>,
|
||||
}))
|
||||
|
||||
vi.mock('./deprecated-model-trigger', () => ({
|
||||
default: ({ modelName }: { modelName: string }) => <div>{`deprecated:${modelName}`}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('./empty-trigger', () => ({
|
||||
default: () => <div>empty-trigger</div>,
|
||||
}))
|
||||
|
||||
vi.mock('./popup', () => ({
|
||||
default: ({ onHide, onSelect }: { onHide: () => void, onSelect: (provider: string, model: ModelItem) => void }) => (
|
||||
<>
|
||||
<button type="button" onClick={() => onSelect('openai', { model: 'gpt-4' } as ModelItem)}>
|
||||
select
|
||||
</button>
|
||||
<button type="button" onClick={onHide}>
|
||||
hide
|
||||
</button>
|
||||
</>
|
||||
),
|
||||
}))
|
||||
|
||||
const makeModelItem = (overrides: Partial<ModelItem> = {}): ModelItem => ({
|
||||
model: 'gpt-4',
|
||||
label: { en_US: 'GPT-4', zh_Hans: 'GPT-4' },
|
||||
model_type: ModelTypeEnum.textGeneration,
|
||||
fetch_from: ConfigurationMethodEnum.predefinedModel,
|
||||
status: ModelStatusEnum.active,
|
||||
model_properties: {},
|
||||
load_balancing_enabled: false,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const makeModel = (overrides: Partial<Model> = {}): Model => ({
|
||||
provider: 'openai',
|
||||
icon_small: { en_US: '', zh_Hans: '' },
|
||||
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
|
||||
models: [makeModelItem()],
|
||||
status: ModelStatusEnum.active,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('ModelSelector', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should toggle popup and close it after selecting a model', () => {
|
||||
render(<ModelSelector modelList={[makeModel()]} />)
|
||||
|
||||
fireEvent.click(screen.getByText('empty-trigger'))
|
||||
expect(screen.getByText('select')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText('select'))
|
||||
expect(screen.queryByText('select')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should call onSelect when provided', () => {
|
||||
const onSelect = vi.fn()
|
||||
render(<ModelSelector modelList={[makeModel()]} onSelect={onSelect} />)
|
||||
|
||||
fireEvent.click(screen.getByText('empty-trigger'))
|
||||
fireEvent.click(screen.getByText('select'))
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith({ provider: 'openai', model: 'gpt-4' })
|
||||
})
|
||||
|
||||
it('should close popup when popup requests hide', () => {
|
||||
render(<ModelSelector modelList={[makeModel()]} />)
|
||||
|
||||
fireEvent.click(screen.getByText('empty-trigger'))
|
||||
expect(screen.getByText('hide')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByText('hide'))
|
||||
expect(screen.queryByText('hide')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not open popup when readonly', () => {
|
||||
render(<ModelSelector modelList={[makeModel()]} readonly />)
|
||||
|
||||
fireEvent.click(screen.getByText('empty-trigger'))
|
||||
expect(screen.queryByText('select')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render deprecated trigger when defaultModel is not in list', () => {
|
||||
const { rerender } = render(
|
||||
<ModelSelector
|
||||
defaultModel={{ provider: 'openai', model: 'missing-model' }}
|
||||
modelList={[makeModel()]}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('deprecated:missing-model')).toBeInTheDocument()
|
||||
|
||||
rerender(
|
||||
<ModelSelector
|
||||
defaultModel={{ provider: '', model: '' }}
|
||||
modelList={[makeModel()]}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByText('deprecated:')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render model trigger when defaultModel matches', () => {
|
||||
render(
|
||||
<ModelSelector
|
||||
defaultModel={{ provider: 'openai', model: 'gpt-4' }}
|
||||
modelList={[makeModel()]}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('model-trigger')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,91 @@
|
||||
import type { Model, ModelItem } from '../declarations'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
ModelStatusEnum,
|
||||
ModelTypeEnum,
|
||||
} from '../declarations'
|
||||
import ModelTrigger from './model-trigger'
|
||||
|
||||
vi.mock('../hooks', async () => {
|
||||
const actual = await vi.importActual<typeof import('../hooks')>('../hooks')
|
||||
return {
|
||||
...actual,
|
||||
useLanguage: () => 'en_US',
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../model-icon', () => ({
|
||||
default: ({ modelName }: { modelName: string }) => <span>{modelName}</span>,
|
||||
}))
|
||||
|
||||
vi.mock('../model-name', () => ({
|
||||
default: ({ modelItem }: { modelItem: ModelItem }) => <span>{modelItem.label.en_US}</span>,
|
||||
}))
|
||||
|
||||
const makeModelItem = (overrides: Partial<ModelItem> = {}): ModelItem => ({
|
||||
model: 'gpt-4',
|
||||
label: { en_US: 'GPT-4', zh_Hans: 'GPT-4' },
|
||||
model_type: ModelTypeEnum.textGeneration,
|
||||
fetch_from: ConfigurationMethodEnum.predefinedModel,
|
||||
status: ModelStatusEnum.active,
|
||||
model_properties: {},
|
||||
load_balancing_enabled: false,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const makeModel = (overrides: Partial<Model> = {}): Model => ({
|
||||
provider: 'openai',
|
||||
icon_small: { en_US: '', zh_Hans: '' },
|
||||
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
|
||||
models: [makeModelItem()],
|
||||
status: ModelStatusEnum.active,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('ModelTrigger', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should show model name', () => {
|
||||
render(
|
||||
<ModelTrigger
|
||||
open
|
||||
provider={makeModel()}
|
||||
model={makeModelItem()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('GPT-4')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show status tooltip content when model is not active', async () => {
|
||||
const { container } = render(
|
||||
<ModelTrigger
|
||||
open={false}
|
||||
provider={makeModel()}
|
||||
model={makeModelItem({ status: ModelStatusEnum.noConfigure })}
|
||||
/>,
|
||||
)
|
||||
|
||||
const tooltipTrigger = container.querySelector('[data-state]') as HTMLElement
|
||||
fireEvent.mouseEnter(tooltipTrigger)
|
||||
|
||||
expect(await screen.findByText('No Configure')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should not show status icon when readonly', () => {
|
||||
render(
|
||||
<ModelTrigger
|
||||
open={false}
|
||||
provider={makeModel()}
|
||||
model={makeModelItem({ status: ModelStatusEnum.noConfigure })}
|
||||
readonly
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('GPT-4')).toBeInTheDocument()
|
||||
expect(screen.queryByText('No Configure')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,147 @@
|
||||
import type { DefaultModel, Model, ModelItem } from '../declarations'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
ModelFeatureEnum,
|
||||
ModelStatusEnum,
|
||||
ModelTypeEnum,
|
||||
} from '../declarations'
|
||||
import PopupItem from './popup-item'
|
||||
|
||||
const mockUpdateModelList = vi.hoisted(() => vi.fn())
|
||||
const mockUpdateModelProviders = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('../hooks', async () => {
|
||||
const actual = await vi.importActual<typeof import('../hooks')>('../hooks')
|
||||
return {
|
||||
...actual,
|
||||
useLanguage: () => 'en_US',
|
||||
useUpdateModelList: () => mockUpdateModelList,
|
||||
useUpdateModelProviders: () => mockUpdateModelProviders,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('../model-badge', () => ({
|
||||
default: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
|
||||
}))
|
||||
|
||||
vi.mock('../model-icon', () => ({
|
||||
default: ({ modelName }: { modelName: string }) => <span>{modelName}</span>,
|
||||
}))
|
||||
|
||||
vi.mock('../model-name', () => ({
|
||||
default: ({ modelItem }: { modelItem: ModelItem }) => <span>{modelItem.label.en_US}</span>,
|
||||
}))
|
||||
|
||||
const mockSetShowModelModal = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: () => ({
|
||||
setShowModelModal: mockSetShowModelModal,
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockUseProviderContext = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: mockUseProviderContext,
|
||||
}))
|
||||
|
||||
const makeModelItem = (overrides: Partial<ModelItem> = {}): ModelItem => ({
|
||||
model: 'gpt-4',
|
||||
label: { en_US: 'GPT-4', zh_Hans: 'GPT-4' },
|
||||
model_type: ModelTypeEnum.textGeneration,
|
||||
features: [ModelFeatureEnum.vision],
|
||||
fetch_from: ConfigurationMethodEnum.predefinedModel,
|
||||
status: ModelStatusEnum.active,
|
||||
model_properties: { mode: 'chat', context_size: 4096 },
|
||||
load_balancing_enabled: false,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const makeModel = (overrides: Partial<Model> = {}): Model => ({
|
||||
provider: 'openai',
|
||||
icon_small: { en_US: '', zh_Hans: '' },
|
||||
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
|
||||
models: [makeModelItem()],
|
||||
status: ModelStatusEnum.active,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('PopupItem', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseProviderContext.mockReturnValue({
|
||||
modelProviders: [{ provider: 'openai' }],
|
||||
})
|
||||
})
|
||||
|
||||
it('should call onSelect when clicking an active model', () => {
|
||||
const onSelect = vi.fn()
|
||||
render(<PopupItem model={makeModel()} onSelect={onSelect} />)
|
||||
|
||||
fireEvent.click(screen.getByText('GPT-4'))
|
||||
|
||||
expect(onSelect).toHaveBeenCalledWith('openai', expect.objectContaining({ model: 'gpt-4' }))
|
||||
})
|
||||
|
||||
it('should not call onSelect when model is not active', () => {
|
||||
const onSelect = vi.fn()
|
||||
render(
|
||||
<PopupItem
|
||||
model={makeModel({ models: [makeModelItem({ status: ModelStatusEnum.disabled })] })}
|
||||
onSelect={onSelect}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('GPT-4'))
|
||||
|
||||
expect(onSelect).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should open model modal when clicking add on unconfigured model', () => {
|
||||
const { rerender } = render(
|
||||
<PopupItem
|
||||
model={makeModel({ models: [makeModelItem({ status: ModelStatusEnum.noConfigure })] })}
|
||||
onSelect={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('COMMON.OPERATION.ADD'))
|
||||
|
||||
expect(mockSetShowModelModal).toHaveBeenCalled()
|
||||
|
||||
const call = mockSetShowModelModal.mock.calls[0][0] as { onSaveCallback?: () => void }
|
||||
call.onSaveCallback?.()
|
||||
|
||||
expect(mockUpdateModelProviders).toHaveBeenCalled()
|
||||
expect(mockUpdateModelList).toHaveBeenCalledWith(ModelTypeEnum.textGeneration)
|
||||
|
||||
rerender(
|
||||
<PopupItem
|
||||
model={makeModel({
|
||||
models: [makeModelItem({ status: ModelStatusEnum.noConfigure, model_type: undefined as unknown as ModelTypeEnum })],
|
||||
})}
|
||||
onSelect={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('COMMON.OPERATION.ADD'))
|
||||
const call2 = mockSetShowModelModal.mock.calls.at(-1)?.[0] as { onSaveCallback?: () => void } | undefined
|
||||
call2?.onSaveCallback?.()
|
||||
|
||||
expect(mockUpdateModelProviders).toHaveBeenCalled()
|
||||
expect(mockUpdateModelList).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should show selected state when defaultModel matches', () => {
|
||||
const defaultModel: DefaultModel = { provider: 'openai', model: 'gpt-4' }
|
||||
render(
|
||||
<PopupItem
|
||||
defaultModel={defaultModel}
|
||||
model={makeModel()}
|
||||
onSelect={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('GPT-4')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,199 @@
|
||||
import type { Model, ModelItem } from '../declarations'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
ModelFeatureEnum,
|
||||
ModelStatusEnum,
|
||||
ModelTypeEnum,
|
||||
} from '../declarations'
|
||||
import Popup from './popup'
|
||||
|
||||
let mockLanguage = 'en_US'
|
||||
|
||||
const mockSetShowAccountSettingModal = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@/context/modal-context', () => ({
|
||||
useModalContext: () => ({
|
||||
setShowAccountSettingModal: mockSetShowAccountSettingModal,
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockSupportFunctionCall = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@/utils/tool-call', () => ({
|
||||
supportFunctionCall: mockSupportFunctionCall,
|
||||
}))
|
||||
|
||||
const mockCloseActiveTooltip = vi.hoisted(() => vi.fn())
|
||||
vi.mock('@/app/components/base/tooltip/TooltipManager', () => ({
|
||||
tooltipManager: {
|
||||
closeActiveTooltip: mockCloseActiveTooltip,
|
||||
register: vi.fn(),
|
||||
clear: vi.fn(),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/icons/src/vender/solid/general', () => ({
|
||||
XCircle: ({ onClick }: { onClick?: () => void }) => (
|
||||
<button type="button" aria-label="clear-search" onClick={onClick} />
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('../hooks', async () => {
|
||||
const actual = await vi.importActual<typeof import('../hooks')>('../hooks')
|
||||
return {
|
||||
...actual,
|
||||
useLanguage: () => mockLanguage,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('./popup-item', () => ({
|
||||
default: ({ model }: { model: Model }) => <div>{model.provider}</div>,
|
||||
}))
|
||||
|
||||
const makeModelItem = (overrides: Partial<ModelItem> = {}): ModelItem => ({
|
||||
model: 'gpt-4',
|
||||
label: { en_US: 'GPT-4', zh_Hans: 'GPT-4' },
|
||||
model_type: ModelTypeEnum.textGeneration,
|
||||
fetch_from: ConfigurationMethodEnum.predefinedModel,
|
||||
status: ModelStatusEnum.active,
|
||||
model_properties: {},
|
||||
load_balancing_enabled: false,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const makeModel = (overrides: Partial<Model> = {}): Model => ({
|
||||
provider: 'openai',
|
||||
icon_small: { en_US: '', zh_Hans: '' },
|
||||
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
|
||||
models: [makeModelItem()],
|
||||
status: ModelStatusEnum.active,
|
||||
...overrides,
|
||||
})
|
||||
|
||||
describe('Popup', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockLanguage = 'en_US'
|
||||
mockSupportFunctionCall.mockReturnValue(true)
|
||||
})
|
||||
|
||||
it('should filter models by search and allow clearing search', () => {
|
||||
render(
|
||||
<Popup
|
||||
modelList={[makeModel()]}
|
||||
onSelect={vi.fn()}
|
||||
onHide={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByText('openai')).toBeInTheDocument()
|
||||
|
||||
const input = screen.getByPlaceholderText('datasetSettings.form.searchModel')
|
||||
fireEvent.change(input, { target: { value: 'not-found' } })
|
||||
expect(screen.getByText('No model found for “not-found”')).toBeInTheDocument()
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'clear-search' }))
|
||||
expect((input as HTMLInputElement).value).toBe('')
|
||||
})
|
||||
|
||||
it('should filter by scope features including toolCall and non-toolCall checks', () => {
|
||||
const modelList = [
|
||||
makeModel({ models: [makeModelItem({ features: [ModelFeatureEnum.toolCall, ModelFeatureEnum.vision] })] }),
|
||||
]
|
||||
|
||||
// When tool-call support is missing, it should be filtered out.
|
||||
mockSupportFunctionCall.mockReturnValue(false)
|
||||
const { unmount } = render(
|
||||
<Popup
|
||||
modelList={modelList}
|
||||
onSelect={vi.fn()}
|
||||
onHide={vi.fn()}
|
||||
scopeFeatures={[ModelFeatureEnum.toolCall, ModelFeatureEnum.vision]}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByText('No model found for “”')).toBeInTheDocument()
|
||||
|
||||
// When tool-call support exists, the non-toolCall feature check should also pass.
|
||||
unmount()
|
||||
mockSupportFunctionCall.mockReturnValue(true)
|
||||
const { unmount: unmount2 } = render(
|
||||
<Popup
|
||||
modelList={modelList}
|
||||
onSelect={vi.fn()}
|
||||
onHide={vi.fn()}
|
||||
scopeFeatures={[ModelFeatureEnum.toolCall, ModelFeatureEnum.vision]}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByText('openai')).toBeInTheDocument()
|
||||
|
||||
unmount2()
|
||||
const { unmount: unmount3 } = render(
|
||||
<Popup
|
||||
modelList={modelList}
|
||||
onSelect={vi.fn()}
|
||||
onHide={vi.fn()}
|
||||
scopeFeatures={[ModelFeatureEnum.vision]}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByText('openai')).toBeInTheDocument()
|
||||
|
||||
// When features are missing, non-toolCall feature checks should fail.
|
||||
unmount3()
|
||||
render(
|
||||
<Popup
|
||||
modelList={[makeModel({ models: [makeModelItem({ features: undefined })] })]}
|
||||
onSelect={vi.fn()}
|
||||
onHide={vi.fn()}
|
||||
scopeFeatures={[ModelFeatureEnum.vision]}
|
||||
/>,
|
||||
)
|
||||
expect(screen.getByText('No model found for “”')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should match labels from other languages when current language key is missing', () => {
|
||||
mockLanguage = 'fr_FR'
|
||||
|
||||
render(
|
||||
<Popup
|
||||
modelList={[makeModel()]}
|
||||
onSelect={vi.fn()}
|
||||
onHide={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.change(
|
||||
screen.getByPlaceholderText('datasetSettings.form.searchModel'),
|
||||
{ target: { value: 'gpt' } },
|
||||
)
|
||||
|
||||
expect(screen.getByText('openai')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should close tooltip on scroll', () => {
|
||||
const { container } = render(
|
||||
<Popup
|
||||
modelList={[makeModel()]}
|
||||
onSelect={vi.fn()}
|
||||
onHide={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.scroll(container.firstElementChild as HTMLElement)
|
||||
expect(mockCloseActiveTooltip).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should open provider settings when clicking footer link', () => {
|
||||
render(
|
||||
<Popup
|
||||
modelList={[makeModel()]}
|
||||
onSelect={vi.fn()}
|
||||
onHide={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByText('common.model.settingsLink'))
|
||||
|
||||
expect(mockSetShowAccountSettingModal).toHaveBeenCalledWith({
|
||||
payload: 'provider',
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,97 @@
|
||||
import type { ModelProvider } from '../declarations'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import useTheme from '@/hooks/use-theme'
|
||||
import { Theme } from '@/types/app'
|
||||
import { useLanguage } from '../hooks'
|
||||
import ProviderIcon from './index'
|
||||
|
||||
type UseThemeReturnType = ReturnType<typeof useTheme>
|
||||
|
||||
vi.mock('@/app/components/base/icons/src/public/llm', () => ({
|
||||
AnthropicDark: ({ className }: { className: string }) => <div data-testid="anthropic-dark" className={className} />,
|
||||
AnthropicLight: ({ className }: { className: string }) => <div data-testid="anthropic-light" className={className} />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/icons/src/vender/other', () => ({
|
||||
Openai: ({ className }: { className: string }) => <div data-testid="openai-icon" className={className} />,
|
||||
}))
|
||||
|
||||
vi.mock('@/i18n-config', () => ({
|
||||
renderI18nObject: (obj: Record<string, string> | string, lang: string) => {
|
||||
if (typeof obj === 'string')
|
||||
return obj
|
||||
return obj[lang] || obj.en_US || Object.values(obj)[0]
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-theme', () => {
|
||||
const mockFn = vi.fn(() => ({ theme: Theme.light }))
|
||||
return { default: mockFn }
|
||||
})
|
||||
|
||||
vi.mock('../hooks', () => ({
|
||||
useLanguage: vi.fn(() => 'en_US'),
|
||||
}))
|
||||
|
||||
const createProvider = (overrides: Partial<ModelProvider> = {}): ModelProvider => ({
|
||||
provider: 'some/provider',
|
||||
label: { en_US: 'Provider', zh_Hans: '提供商' },
|
||||
help: { title: { en_US: 'Help', zh_Hans: '帮助' }, url: { en_US: 'https://example.com', zh_Hans: 'https://example.com' } },
|
||||
icon_small: { en_US: 'https://example.com/icon.png', zh_Hans: 'https://example.com/icon.png' },
|
||||
supported_model_types: [],
|
||||
configurate_methods: [],
|
||||
provider_credential_schema: { credential_form_schemas: [] },
|
||||
model_credential_schema: { model: { label: { en_US: 'Model', zh_Hans: '模型' }, placeholder: { en_US: 'Select', zh_Hans: '选择' } }, credential_form_schemas: [] },
|
||||
preferred_provider_type: undefined,
|
||||
...overrides,
|
||||
} as ModelProvider)
|
||||
|
||||
describe('ProviderIcon', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
const mockTheme = vi.mocked(useTheme)
|
||||
const mockLang = vi.mocked(useLanguage)
|
||||
mockTheme.mockReturnValue({ theme: Theme.light, themes: [], setTheme: vi.fn() } as UseThemeReturnType)
|
||||
mockLang.mockReturnValue('en_US')
|
||||
})
|
||||
|
||||
it('should render Anthropic icon based on theme', () => {
|
||||
const mockTheme = vi.mocked(useTheme)
|
||||
mockTheme.mockReturnValue({ theme: Theme.dark, themes: [], setTheme: vi.fn() } as UseThemeReturnType)
|
||||
const provider = createProvider({ provider: 'langgenius/anthropic/anthropic' })
|
||||
|
||||
render(<ProviderIcon provider={provider} />)
|
||||
expect(screen.getByTestId('anthropic-light')).toBeInTheDocument()
|
||||
|
||||
mockTheme.mockReturnValue({ theme: Theme.light, themes: [], setTheme: vi.fn() } as UseThemeReturnType)
|
||||
render(<ProviderIcon provider={provider} />)
|
||||
expect(screen.getByTestId('anthropic-dark')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render OpenAI icon', () => {
|
||||
const provider = createProvider({ provider: 'langgenius/openai/openai' })
|
||||
render(<ProviderIcon provider={provider} />)
|
||||
expect(screen.getByTestId('openai-icon')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render generic provider with image and label', () => {
|
||||
const provider = createProvider({ label: { en_US: 'Custom', zh_Hans: '自定义' } })
|
||||
render(<ProviderIcon provider={provider} />)
|
||||
|
||||
const img = screen.getByAltText('provider-icon') as HTMLImageElement
|
||||
expect(img.src).toBe('https://example.com/icon.png')
|
||||
expect(screen.getByText('Custom')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should use dark icon in dark theme for generic provider', () => {
|
||||
const mockTheme = vi.mocked(useTheme)
|
||||
mockTheme.mockReturnValue({ theme: Theme.dark, themes: [], setTheme: vi.fn() } as UseThemeReturnType)
|
||||
const provider = createProvider({
|
||||
icon_small_dark: { en_US: 'https://example.com/dark.png', zh_Hans: 'https://example.com/dark.png' },
|
||||
})
|
||||
|
||||
render(<ProviderIcon provider={provider} />)
|
||||
const img = screen.getByAltText('provider-icon') as HTMLImageElement
|
||||
expect(img.src).toBe('https://example.com/dark.png')
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,160 @@
|
||||
import type { DefaultModelResponse } from '../declarations'
|
||||
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
|
||||
import { vi } from 'vitest'
|
||||
import { ModelTypeEnum } from '../declarations'
|
||||
import SystemModel from './index'
|
||||
|
||||
vi.mock('react-i18next', async () => {
|
||||
const { createReactI18nextMock } = await import('@/test/i18n-mock')
|
||||
return createReactI18nextMock({
|
||||
'modelProvider.systemModelSettings': 'System Model Settings',
|
||||
'modelProvider.systemReasoningModel.key': 'System Reasoning Model',
|
||||
'modelProvider.systemReasoningModel.tip': 'Reasoning model tip',
|
||||
'modelProvider.embeddingModel.key': 'Embedding Model',
|
||||
'modelProvider.embeddingModel.tip': 'Embedding model tip',
|
||||
'modelProvider.rerankModel.key': 'Rerank Model',
|
||||
'modelProvider.rerankModel.tip': 'Rerank model tip',
|
||||
'modelProvider.speechToTextModel.key': 'Speech to Text Model',
|
||||
'modelProvider.speechToTextModel.tip': 'Speech to text model tip',
|
||||
'modelProvider.ttsModel.key': 'TTS Model',
|
||||
'modelProvider.ttsModel.tip': 'TTS model tip',
|
||||
'operation.cancel': 'Cancel',
|
||||
'operation.save': 'Save',
|
||||
'actionMsg.modifiedSuccessfully': 'Modified successfully',
|
||||
})
|
||||
})
|
||||
|
||||
const mockNotify = vi.hoisted(() => vi.fn())
|
||||
const mockUpdateModelList = vi.hoisted(() => vi.fn())
|
||||
const mockUpdateDefaultModel = vi.hoisted(() => vi.fn(() => Promise.resolve({ result: 'success' })))
|
||||
|
||||
let mockIsCurrentWorkspaceManager = true
|
||||
|
||||
vi.mock('@/context/app-context', () => ({
|
||||
useAppContext: () => ({
|
||||
isCurrentWorkspaceManager: mockIsCurrentWorkspaceManager,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => ({
|
||||
textGenerationModelList: [],
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
useToastContext: () => ({
|
||||
notify: mockNotify,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../hooks', () => ({
|
||||
useModelList: () => ({
|
||||
data: [],
|
||||
}),
|
||||
useSystemDefaultModelAndModelList: (defaultModel: DefaultModelResponse | undefined) => [
|
||||
defaultModel || { model: '', provider: { provider: '', icon_small: { en_US: '', zh_Hans: '' } } },
|
||||
vi.fn(),
|
||||
],
|
||||
useUpdateModelList: () => mockUpdateModelList,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/common', () => ({
|
||||
updateDefaultModel: mockUpdateDefaultModel,
|
||||
}))
|
||||
|
||||
vi.mock('../model-selector', () => ({
|
||||
default: ({ onSelect }: { onSelect: (model: { model: string, provider: string }) => void }) => (
|
||||
<button onClick={() => onSelect({ model: 'test', provider: 'test' })}>Mock Model Selector</button>
|
||||
),
|
||||
}))
|
||||
|
||||
const mockModel: DefaultModelResponse = {
|
||||
model: 'gpt-4',
|
||||
model_type: ModelTypeEnum.textGeneration,
|
||||
provider: {
|
||||
provider: 'openai',
|
||||
icon_small: { en_US: '', zh_Hans: '' },
|
||||
},
|
||||
}
|
||||
|
||||
const defaultProps = {
|
||||
textGenerationDefaultModel: mockModel,
|
||||
embeddingsDefaultModel: undefined,
|
||||
rerankDefaultModel: undefined,
|
||||
speech2textDefaultModel: undefined,
|
||||
ttsDefaultModel: undefined,
|
||||
notConfigured: false,
|
||||
isLoading: false,
|
||||
}
|
||||
|
||||
describe('SystemModel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockIsCurrentWorkspaceManager = true
|
||||
})
|
||||
|
||||
it('should render settings button', () => {
|
||||
render(<SystemModel {...defaultProps} />)
|
||||
expect(screen.getByRole('button', { name: /system model settings/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should open modal when button is clicked', async () => {
|
||||
render(<SystemModel {...defaultProps} />)
|
||||
const button = screen.getByRole('button', { name: /system model settings/i })
|
||||
fireEvent.click(button)
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText(/system reasoning model/i)).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should disable button when loading', () => {
|
||||
render(<SystemModel {...defaultProps} isLoading />)
|
||||
expect(screen.getByRole('button', { name: /system model settings/i })).toBeDisabled()
|
||||
})
|
||||
|
||||
it('should close modal when cancel is clicked', async () => {
|
||||
render(<SystemModel {...defaultProps} />)
|
||||
fireEvent.click(screen.getByRole('button', { name: /system model settings/i }))
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /cancel/i })).toBeInTheDocument()
|
||||
})
|
||||
fireEvent.click(screen.getByRole('button', { name: /cancel/i }))
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByRole('button', { name: /cancel/i })).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('should save selected models and show success feedback', async () => {
|
||||
render(<SystemModel {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /system model settings/i }))
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /save/i })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
const selectorButtons = screen.getAllByRole('button', { name: 'Mock Model Selector' })
|
||||
selectorButtons.forEach(button => fireEvent.click(button))
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /save/i }))
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockUpdateDefaultModel).toHaveBeenCalledTimes(1)
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'success',
|
||||
message: 'Modified successfully',
|
||||
})
|
||||
expect(mockUpdateModelList).toHaveBeenCalledTimes(5)
|
||||
})
|
||||
})
|
||||
|
||||
it('should disable save when user is not workspace manager', async () => {
|
||||
mockIsCurrentWorkspaceManager = false
|
||||
render(<SystemModel {...defaultProps} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: /system model settings/i }))
|
||||
await waitFor(() => {
|
||||
expect(screen.getByRole('button', { name: /save/i })).toBeDisabled()
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,238 @@
|
||||
import type { Mock } from 'vitest'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import {
|
||||
deleteModelProvider,
|
||||
setModelProvider,
|
||||
validateModelLoadBalancingCredentials,
|
||||
validateModelProvider,
|
||||
} from '@/service/common'
|
||||
import { ValidatedStatus } from '../key-validator/declarations'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
FormTypeEnum,
|
||||
ModelTypeEnum,
|
||||
} from './declarations'
|
||||
import {
|
||||
genModelNameFormSchema,
|
||||
genModelTypeFormSchema,
|
||||
modelTypeFormat,
|
||||
removeCredentials,
|
||||
saveCredentials,
|
||||
savePredefinedLoadBalancingConfig,
|
||||
sizeFormat,
|
||||
validateCredentials,
|
||||
validateLoadBalancingCredentials,
|
||||
} from './utils'
|
||||
|
||||
// Mock service/common functions
|
||||
vi.mock('@/service/common', () => ({
|
||||
deleteModelProvider: vi.fn(),
|
||||
setModelProvider: vi.fn(),
|
||||
validateModelLoadBalancingCredentials: vi.fn(),
|
||||
validateModelProvider: vi.fn(),
|
||||
}))
|
||||
|
||||
describe('utils', () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('sizeFormat', () => {
|
||||
it('should format size less than 1000', () => {
|
||||
expect(sizeFormat(500)).toBe('500')
|
||||
})
|
||||
|
||||
it('should format size greater than 1000', () => {
|
||||
expect(sizeFormat(1500)).toBe('1K')
|
||||
})
|
||||
})
|
||||
|
||||
describe('modelTypeFormat', () => {
|
||||
it('should format text embedding type', () => {
|
||||
expect(modelTypeFormat(ModelTypeEnum.textEmbedding)).toBe('TEXT EMBEDDING')
|
||||
})
|
||||
|
||||
it('should format other types', () => {
|
||||
expect(modelTypeFormat(ModelTypeEnum.textGeneration)).toBe('LLM')
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateCredentials', () => {
|
||||
it('should validate predefined credentials successfully', async () => {
|
||||
(validateModelProvider as unknown as Mock).mockResolvedValue({ result: 'success' })
|
||||
const result = await validateCredentials(true, 'provider', { key: 'value' })
|
||||
expect(result).toEqual({ status: ValidatedStatus.Success })
|
||||
expect(validateModelProvider).toHaveBeenCalledWith({
|
||||
url: '/workspaces/current/model-providers/provider/credentials/validate',
|
||||
body: { credentials: { key: 'value' } },
|
||||
})
|
||||
})
|
||||
|
||||
it('should validate custom credentials successfully', async () => {
|
||||
(validateModelProvider as unknown as Mock).mockResolvedValue({ result: 'success' })
|
||||
const result = await validateCredentials(false, 'provider', {
|
||||
__model_name: 'model',
|
||||
__model_type: 'type',
|
||||
key: 'value',
|
||||
})
|
||||
expect(result).toEqual({ status: ValidatedStatus.Success })
|
||||
expect(validateModelProvider).toHaveBeenCalledWith({
|
||||
url: '/workspaces/current/model-providers/provider/models/credentials/validate',
|
||||
body: {
|
||||
model: 'model',
|
||||
model_type: 'type',
|
||||
credentials: { key: 'value' },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle validation failure', async () => {
|
||||
(validateModelProvider as unknown as Mock).mockResolvedValue({ result: 'error', error: 'failed' })
|
||||
const result = await validateCredentials(true, 'provider', {})
|
||||
expect(result).toEqual({ status: ValidatedStatus.Error, message: 'failed' })
|
||||
})
|
||||
|
||||
it('should handle exception', async () => {
|
||||
(validateModelProvider as unknown as Mock).mockRejectedValue(new Error('network error'))
|
||||
const result = await validateCredentials(true, 'provider', {})
|
||||
expect(result).toEqual({ status: ValidatedStatus.Error, message: 'network error' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateLoadBalancingCredentials', () => {
|
||||
it('should validate load balancing credentials successfully', async () => {
|
||||
(validateModelLoadBalancingCredentials as unknown as Mock).mockResolvedValue({ result: 'success' })
|
||||
const result = await validateLoadBalancingCredentials(true, 'provider', {
|
||||
__model_name: 'model',
|
||||
__model_type: 'type',
|
||||
key: 'value',
|
||||
})
|
||||
expect(result).toEqual({ status: ValidatedStatus.Success })
|
||||
expect(validateModelLoadBalancingCredentials).toHaveBeenCalledWith({
|
||||
url: '/workspaces/current/model-providers/provider/models/load-balancing-configs/credentials-validate',
|
||||
body: {
|
||||
model: 'model',
|
||||
model_type: 'type',
|
||||
credentials: { key: 'value' },
|
||||
},
|
||||
})
|
||||
})
|
||||
it('should validate load balancing credentials successfully with id', async () => {
|
||||
(validateModelLoadBalancingCredentials as unknown as Mock).mockResolvedValue({ result: 'success' })
|
||||
const result = await validateLoadBalancingCredentials(true, 'provider', {
|
||||
__model_name: 'model',
|
||||
__model_type: 'type',
|
||||
key: 'value',
|
||||
}, 'id')
|
||||
expect(result).toEqual({ status: ValidatedStatus.Success })
|
||||
expect(validateModelLoadBalancingCredentials).toHaveBeenCalledWith({
|
||||
url: '/workspaces/current/model-providers/provider/models/load-balancing-configs/id/credentials-validate',
|
||||
body: {
|
||||
model: 'model',
|
||||
model_type: 'type',
|
||||
credentials: { key: 'value' },
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should handle validation failure', async () => {
|
||||
(validateModelLoadBalancingCredentials as unknown as Mock).mockResolvedValue({ result: 'error', error: 'failed' })
|
||||
const result = await validateLoadBalancingCredentials(true, 'provider', {})
|
||||
expect(result).toEqual({ status: ValidatedStatus.Error, message: 'failed' })
|
||||
})
|
||||
})
|
||||
|
||||
describe('saveCredentials', () => {
|
||||
it('should save predefined credentials', async () => {
|
||||
await saveCredentials(true, 'provider', { __authorization_name__: 'name', key: 'value' })
|
||||
expect(setModelProvider).toHaveBeenCalledWith({
|
||||
url: '/workspaces/current/model-providers/provider/credentials',
|
||||
body: {
|
||||
config_from: ConfigurationMethodEnum.predefinedModel,
|
||||
credentials: { key: 'value' },
|
||||
load_balancing: undefined,
|
||||
name: 'name',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should save custom credentials', async () => {
|
||||
await saveCredentials(false, 'provider', {
|
||||
__model_name: 'model',
|
||||
__model_type: 'type',
|
||||
key: 'value',
|
||||
})
|
||||
expect(setModelProvider).toHaveBeenCalledWith({
|
||||
url: '/workspaces/current/model-providers/provider/models',
|
||||
body: {
|
||||
model: 'model',
|
||||
model_type: 'type',
|
||||
credentials: { key: 'value' },
|
||||
load_balancing: undefined,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('savePredefinedLoadBalancingConfig', () => {
|
||||
it('should save predefined load balancing config', async () => {
|
||||
await savePredefinedLoadBalancingConfig('provider', {
|
||||
__model_name: 'model',
|
||||
__model_type: 'type',
|
||||
key: 'value',
|
||||
})
|
||||
expect(setModelProvider).toHaveBeenCalledWith({
|
||||
url: '/workspaces/current/model-providers/provider/models',
|
||||
body: {
|
||||
config_from: ConfigurationMethodEnum.predefinedModel,
|
||||
model: 'model',
|
||||
model_type: 'type',
|
||||
credentials: { key: 'value' },
|
||||
load_balancing: undefined,
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('removeCredentials', () => {
|
||||
it('should remove predefined credentials', async () => {
|
||||
await removeCredentials(true, 'provider', {}, 'id')
|
||||
expect(deleteModelProvider).toHaveBeenCalledWith({
|
||||
url: '/workspaces/current/model-providers/provider/credentials',
|
||||
body: { credential_id: 'id' },
|
||||
})
|
||||
})
|
||||
|
||||
it('should remove custom credentials', async () => {
|
||||
await removeCredentials(false, 'provider', {
|
||||
__model_name: 'model',
|
||||
__model_type: 'type',
|
||||
})
|
||||
expect(deleteModelProvider).toHaveBeenCalledWith({
|
||||
url: '/workspaces/current/model-providers/provider/models',
|
||||
body: {
|
||||
model: 'model',
|
||||
model_type: 'type',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('genModelTypeFormSchema', () => {
|
||||
it('should generate form schema', () => {
|
||||
const schema = genModelTypeFormSchema([ModelTypeEnum.textGeneration])
|
||||
expect(schema.type).toBe(FormTypeEnum.select)
|
||||
expect(schema.variable).toBe('__model_type')
|
||||
expect(schema.options[0].value).toBe(ModelTypeEnum.textGeneration)
|
||||
})
|
||||
})
|
||||
|
||||
describe('genModelNameFormSchema', () => {
|
||||
it('should generate form schema', () => {
|
||||
const schema = genModelNameFormSchema()
|
||||
expect(schema.type).toBe(FormTypeEnum.textInput)
|
||||
expect(schema.variable).toBe('__model_name')
|
||||
expect(schema.required).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user