test: strengthen model provider header coverage

This commit is contained in:
yyh
2026-03-13 18:40:29 +08:00
parent fcc8e79733
commit 03c58d151a
3 changed files with 307 additions and 0 deletions

View File

@ -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)
})
})
})
})

View File

@ -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)
})
})
})

View File

@ -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')