mirror of
https://github.com/langgenius/dify.git
synced 2026-05-05 09:58:04 +08:00
test: strengthen model provider header coverage
This commit is contained in:
@ -0,0 +1,208 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { ModelProvider } from '../declarations'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { act, renderHook, waitFor } from '@testing-library/react'
|
||||
import * as React from 'react'
|
||||
import { ConfigurationMethodEnum, ModelTypeEnum, PreferredProviderTypeEnum } from '../declarations'
|
||||
import { useChangeProviderPriority } from './use-change-provider-priority'
|
||||
|
||||
const mockUpdateModelList = vi.fn()
|
||||
const mockUpdateModelProviders = vi.fn()
|
||||
const mockNotify = vi.fn()
|
||||
const mockQueryKey = vi.fn(({ input }: { input: { params: { provider: string } } }) => ['model-providers', 'models', input.params.provider])
|
||||
const mockChangePreferredProviderType = vi.fn()
|
||||
const mockMutationOptions = vi.fn((options: Record<string, unknown>) => ({
|
||||
mutationFn: (variables: unknown) => mockChangePreferredProviderType(variables),
|
||||
...options,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/toast', () => ({
|
||||
default: {
|
||||
notify: (...args: unknown[]) => mockNotify(...args),
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleQuery: {
|
||||
modelProviders: {
|
||||
models: {
|
||||
queryKey: (options: { input: { params: { provider: string } } }) => mockQueryKey(options),
|
||||
},
|
||||
changePreferredProviderType: {
|
||||
mutationOptions: (options: Record<string, unknown>) => mockMutationOptions(options),
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('../hooks', () => ({
|
||||
useUpdateModelList: () => mockUpdateModelList,
|
||||
useUpdateModelProviders: () => mockUpdateModelProviders,
|
||||
}))
|
||||
|
||||
const createProvider = (overrides: Partial<ModelProvider> = {}): ModelProvider => ({
|
||||
provider: 'langgenius/openai/openai',
|
||||
configurate_methods: [
|
||||
ConfigurationMethodEnum.customizableModel,
|
||||
ConfigurationMethodEnum.predefinedModel,
|
||||
],
|
||||
supported_model_types: [ModelTypeEnum.textGeneration, ModelTypeEnum.textEmbedding],
|
||||
label: { en_US: 'OpenAI' },
|
||||
icon_small: { en_US: 'https://example.com/icon.png' },
|
||||
provider_credential_schema: { credential_form_schemas: [] },
|
||||
model_credential_schema: {
|
||||
model: {
|
||||
label: { en_US: 'Model' },
|
||||
placeholder: { en_US: 'Select model' },
|
||||
},
|
||||
credential_form_schemas: [],
|
||||
},
|
||||
...overrides,
|
||||
} as ModelProvider)
|
||||
|
||||
const createTestQueryClient = () => new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, gcTime: 0 },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
})
|
||||
|
||||
const createWrapper = (queryClient: QueryClient) => {
|
||||
return ({ children }: { children: ReactNode }) => (
|
||||
React.createElement(QueryClientProvider, { client: queryClient }, children)
|
||||
)
|
||||
}
|
||||
|
||||
describe('useChangeProviderPriority', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockChangePreferredProviderType.mockResolvedValue(undefined)
|
||||
})
|
||||
|
||||
describe('when changing provider priority', () => {
|
||||
it('should submit the selected preferred provider type for the current provider', async () => {
|
||||
const queryClient = createTestQueryClient()
|
||||
const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries').mockResolvedValue(undefined)
|
||||
const provider = createProvider()
|
||||
const { result } = renderHook(() => useChangeProviderPriority(provider), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.handleChangePriority(PreferredProviderTypeEnum.custom)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockChangePreferredProviderType).toHaveBeenCalledWith({
|
||||
params: { provider: 'langgenius/openai/openai' },
|
||||
body: { preferred_provider_type: PreferredProviderTypeEnum.custom },
|
||||
})
|
||||
})
|
||||
|
||||
expect(mockQueryKey).toHaveBeenCalledWith({
|
||||
input: {
|
||||
params: {
|
||||
provider: 'langgenius/openai/openai',
|
||||
},
|
||||
},
|
||||
})
|
||||
expect(mockMutationOptions).toHaveBeenCalled()
|
||||
expect(invalidateQueries).toHaveBeenCalledWith({
|
||||
queryKey: ['model-providers', 'models', 'langgenius/openai/openai'],
|
||||
exact: true,
|
||||
refetchType: 'none',
|
||||
})
|
||||
expect(mockUpdateModelProviders).toHaveBeenCalledTimes(1)
|
||||
expect(mockUpdateModelList).toHaveBeenCalledTimes(2)
|
||||
expect(mockUpdateModelList).toHaveBeenNthCalledWith(1, ModelTypeEnum.textGeneration)
|
||||
expect(mockUpdateModelList).toHaveBeenNthCalledWith(2, ModelTypeEnum.textEmbedding)
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'success',
|
||||
message: 'common.actionMsg.modifiedSuccessfully',
|
||||
})
|
||||
expect(result.current.isChangingPriority).toBe(false)
|
||||
})
|
||||
|
||||
it('should tolerate an undefined provider and still submit a request without refreshing model lists', async () => {
|
||||
const queryClient = createTestQueryClient()
|
||||
const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries').mockResolvedValue(undefined)
|
||||
const { result } = renderHook(() => useChangeProviderPriority(undefined), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.handleChangePriority(PreferredProviderTypeEnum.system)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockChangePreferredProviderType).toHaveBeenCalledWith({
|
||||
params: { provider: '' },
|
||||
body: { preferred_provider_type: PreferredProviderTypeEnum.system },
|
||||
})
|
||||
})
|
||||
|
||||
expect(invalidateQueries).toHaveBeenCalledWith({
|
||||
queryKey: ['model-providers', 'models', ''],
|
||||
exact: true,
|
||||
refetchType: 'none',
|
||||
})
|
||||
expect(mockUpdateModelProviders).toHaveBeenCalledTimes(1)
|
||||
expect(mockUpdateModelList).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('when the mutation is not successful immediately', () => {
|
||||
it('should show an error toast when the mutation fails', async () => {
|
||||
const queryClient = createTestQueryClient()
|
||||
const invalidateQueries = vi.spyOn(queryClient, 'invalidateQueries').mockResolvedValue(undefined)
|
||||
mockChangePreferredProviderType.mockRejectedValueOnce(new Error('network error'))
|
||||
const { result } = renderHook(() => useChangeProviderPriority(createProvider()), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.handleChangePriority(PreferredProviderTypeEnum.custom)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockNotify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'common.actionMsg.modifiedUnsuccessfully',
|
||||
})
|
||||
})
|
||||
|
||||
expect(invalidateQueries).not.toHaveBeenCalled()
|
||||
expect(mockUpdateModelProviders).not.toHaveBeenCalled()
|
||||
expect(mockUpdateModelList).not.toHaveBeenCalled()
|
||||
expect(result.current.isChangingPriority).toBe(false)
|
||||
})
|
||||
|
||||
it('should expose the pending mutation state while the request is in flight', async () => {
|
||||
let resolveMutation: (() => void) | undefined
|
||||
mockChangePreferredProviderType.mockImplementationOnce(() => new Promise<void>((resolve) => {
|
||||
resolveMutation = resolve
|
||||
}))
|
||||
|
||||
const queryClient = createTestQueryClient()
|
||||
const { result } = renderHook(() => useChangeProviderPriority(createProvider()), {
|
||||
wrapper: createWrapper(queryClient),
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.handleChangePriority(PreferredProviderTypeEnum.custom)
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isChangingPriority).toBe(true)
|
||||
})
|
||||
|
||||
await act(async () => {
|
||||
resolveMutation?.()
|
||||
})
|
||||
|
||||
await waitFor(() => {
|
||||
expect(result.current.isChangingPriority).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,88 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { useTrialCredits } from './use-trial-credits'
|
||||
|
||||
const mockUseCurrentWorkspace = vi.fn()
|
||||
|
||||
vi.mock('@/service/use-common', () => ({
|
||||
useCurrentWorkspace: () => mockUseCurrentWorkspace(),
|
||||
}))
|
||||
|
||||
describe('useTrialCredits', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseCurrentWorkspace.mockReturnValue({
|
||||
data: {
|
||||
trial_credits: 100,
|
||||
trial_credits_used: 40,
|
||||
next_credit_reset_date: '2026-04-01',
|
||||
},
|
||||
isPending: false,
|
||||
})
|
||||
})
|
||||
|
||||
describe('when workspace data is available', () => {
|
||||
it('should return the remaining credits and reset date', () => {
|
||||
const { result } = renderHook(() => useTrialCredits())
|
||||
|
||||
expect(result.current).toEqual({
|
||||
credits: 60,
|
||||
totalCredits: 100,
|
||||
isExhausted: false,
|
||||
isLoading: false,
|
||||
nextCreditResetDate: '2026-04-01',
|
||||
})
|
||||
})
|
||||
|
||||
it('should keep the hook out of loading state during a background refetch', () => {
|
||||
mockUseCurrentWorkspace.mockReturnValue({
|
||||
data: {
|
||||
trial_credits: 80,
|
||||
trial_credits_used: 20,
|
||||
next_credit_reset_date: '2026-05-01',
|
||||
},
|
||||
isPending: true,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useTrialCredits())
|
||||
|
||||
expect(result.current.isLoading).toBe(false)
|
||||
expect(result.current.credits).toBe(60)
|
||||
expect(result.current.isExhausted).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('when workspace data is missing or exhausted', () => {
|
||||
it('should report loading while the first workspace request is pending', () => {
|
||||
mockUseCurrentWorkspace.mockReturnValue({
|
||||
data: undefined,
|
||||
isPending: true,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useTrialCredits())
|
||||
|
||||
expect(result.current).toEqual({
|
||||
credits: 0,
|
||||
totalCredits: 0,
|
||||
isExhausted: true,
|
||||
isLoading: true,
|
||||
nextCreditResetDate: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it('should clamp negative remaining credits to zero', () => {
|
||||
mockUseCurrentWorkspace.mockReturnValue({
|
||||
data: {
|
||||
trial_credits: 10,
|
||||
trial_credits_used: 99,
|
||||
next_credit_reset_date: undefined,
|
||||
},
|
||||
isPending: false,
|
||||
})
|
||||
|
||||
const { result } = renderHook(() => useTrialCredits())
|
||||
|
||||
expect(result.current.credits).toBe(0)
|
||||
expect(result.current.isExhausted).toBe(true)
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -16,6 +16,7 @@ import {
|
||||
genModelNameFormSchema,
|
||||
genModelTypeFormSchema,
|
||||
modelTypeFormat,
|
||||
providerToPluginId,
|
||||
removeCredentials,
|
||||
saveCredentials,
|
||||
savePredefinedLoadBalancingConfig,
|
||||
@ -47,6 +48,16 @@ describe('utils', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('providerToPluginId', () => {
|
||||
it('should return the plugin id prefix when the provider key contains a provider segment', () => {
|
||||
expect(providerToPluginId('langgenius/openai/openai')).toBe('langgenius/openai')
|
||||
})
|
||||
|
||||
it('should return an empty string when the provider key has no plugin prefix', () => {
|
||||
expect(providerToPluginId('openai')).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('modelTypeFormat', () => {
|
||||
it('should format text embedding type', () => {
|
||||
expect(modelTypeFormat(ModelTypeEnum.textEmbedding)).toBe('TEXT EMBEDDING')
|
||||
|
||||
Reference in New Issue
Block a user