mirror of
https://github.com/langgenius/dify.git
synced 2026-05-04 01:18:05 +08:00
test(web): add comprehensive unit and integration tests for plugins and tools modules (#32220)
Co-authored-by: CodingOnStar <hanxujiang@dify.com>
This commit is contained in:
@ -0,0 +1,79 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import useCheckInstalled from '../use-check-installed'
|
||||
|
||||
const mockPlugins = [
|
||||
{
|
||||
plugin_id: 'plugin-1',
|
||||
id: 'installed-1',
|
||||
declaration: { version: '1.0.0' },
|
||||
plugin_unique_identifier: 'org/plugin-1',
|
||||
},
|
||||
{
|
||||
plugin_id: 'plugin-2',
|
||||
id: 'installed-2',
|
||||
declaration: { version: '2.0.0' },
|
||||
plugin_unique_identifier: 'org/plugin-2',
|
||||
},
|
||||
]
|
||||
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
useCheckInstalled: ({ pluginIds, enabled }: { pluginIds: string[], enabled: boolean }) => ({
|
||||
data: enabled && pluginIds.length > 0 ? { plugins: mockPlugins } : undefined,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('useCheckInstalled', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should return installed info when enabled and has plugin IDs', () => {
|
||||
const { result } = renderHook(() => useCheckInstalled({
|
||||
pluginIds: ['plugin-1', 'plugin-2'],
|
||||
enabled: true,
|
||||
}))
|
||||
|
||||
expect(result.current.installedInfo).toBeDefined()
|
||||
expect(result.current.installedInfo?.['plugin-1']).toEqual({
|
||||
installedId: 'installed-1',
|
||||
installedVersion: '1.0.0',
|
||||
uniqueIdentifier: 'org/plugin-1',
|
||||
})
|
||||
expect(result.current.installedInfo?.['plugin-2']).toEqual({
|
||||
installedId: 'installed-2',
|
||||
installedVersion: '2.0.0',
|
||||
uniqueIdentifier: 'org/plugin-2',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return undefined installedInfo when disabled', () => {
|
||||
const { result } = renderHook(() => useCheckInstalled({
|
||||
pluginIds: ['plugin-1'],
|
||||
enabled: false,
|
||||
}))
|
||||
|
||||
expect(result.current.installedInfo).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return undefined installedInfo with empty plugin IDs', () => {
|
||||
const { result } = renderHook(() => useCheckInstalled({
|
||||
pluginIds: [],
|
||||
enabled: true,
|
||||
}))
|
||||
|
||||
expect(result.current.installedInfo).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should return isLoading and error states', () => {
|
||||
const { result } = renderHook(() => useCheckInstalled({
|
||||
pluginIds: ['plugin-1'],
|
||||
enabled: true,
|
||||
}))
|
||||
|
||||
expect(result.current.isLoading).toBe(false)
|
||||
expect(result.current.error).toBeNull()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,76 @@
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import useHideLogic from '../use-hide-logic'
|
||||
|
||||
const mockFoldAnimInto = vi.fn()
|
||||
const mockClearCountDown = vi.fn()
|
||||
const mockCountDownFoldIntoAnim = vi.fn()
|
||||
|
||||
vi.mock('../use-fold-anim-into', () => ({
|
||||
default: () => ({
|
||||
modalClassName: 'test-modal-class',
|
||||
foldIntoAnim: mockFoldAnimInto,
|
||||
clearCountDown: mockClearCountDown,
|
||||
countDownFoldIntoAnim: mockCountDownFoldIntoAnim,
|
||||
}),
|
||||
}))
|
||||
|
||||
describe('useHideLogic', () => {
|
||||
const mockOnClose = vi.fn()
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should return initial state with modalClassName', () => {
|
||||
const { result } = renderHook(() => useHideLogic(mockOnClose))
|
||||
|
||||
expect(result.current.modalClassName).toBe('test-modal-class')
|
||||
})
|
||||
|
||||
it('should call onClose directly when not installing', () => {
|
||||
const { result } = renderHook(() => useHideLogic(mockOnClose))
|
||||
|
||||
act(() => {
|
||||
result.current.foldAnimInto()
|
||||
})
|
||||
|
||||
expect(mockOnClose).toHaveBeenCalled()
|
||||
expect(mockFoldAnimInto).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should call doFoldAnimInto when installing', () => {
|
||||
const { result } = renderHook(() => useHideLogic(mockOnClose))
|
||||
|
||||
act(() => {
|
||||
result.current.handleStartToInstall()
|
||||
})
|
||||
|
||||
act(() => {
|
||||
result.current.foldAnimInto()
|
||||
})
|
||||
|
||||
expect(mockFoldAnimInto).toHaveBeenCalled()
|
||||
expect(mockOnClose).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should set installing and start countdown on handleStartToInstall', () => {
|
||||
const { result } = renderHook(() => useHideLogic(mockOnClose))
|
||||
|
||||
act(() => {
|
||||
result.current.handleStartToInstall()
|
||||
})
|
||||
|
||||
expect(mockCountDownFoldIntoAnim).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should clear countdown when setIsInstalling to false', () => {
|
||||
const { result } = renderHook(() => useHideLogic(mockOnClose))
|
||||
|
||||
act(() => {
|
||||
result.current.setIsInstalling(false)
|
||||
})
|
||||
|
||||
expect(mockClearCountDown).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,149 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { InstallationScope } from '@/types/feature'
|
||||
import { pluginInstallLimit } from '../use-install-plugin-limit'
|
||||
|
||||
const mockSystemFeatures = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: false,
|
||||
plugin_installation_scope: InstallationScope.ALL,
|
||||
},
|
||||
}
|
||||
|
||||
vi.mock('@/context/global-public-context', () => ({
|
||||
useGlobalPublicStore: (selector: (state: { systemFeatures: typeof mockSystemFeatures }) => unknown) =>
|
||||
selector({ systemFeatures: mockSystemFeatures }),
|
||||
}))
|
||||
|
||||
const basePlugin = {
|
||||
from: 'marketplace' as const,
|
||||
verification: { authorized_category: 'langgenius' },
|
||||
}
|
||||
|
||||
describe('pluginInstallLimit', () => {
|
||||
it('should allow all plugins when scope is ALL', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: false,
|
||||
plugin_installation_scope: InstallationScope.ALL,
|
||||
},
|
||||
}
|
||||
|
||||
expect(pluginInstallLimit(basePlugin as never, features as never).canInstall).toBe(true)
|
||||
})
|
||||
|
||||
it('should deny all plugins when scope is NONE', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: false,
|
||||
plugin_installation_scope: InstallationScope.NONE,
|
||||
},
|
||||
}
|
||||
|
||||
expect(pluginInstallLimit(basePlugin as never, features as never).canInstall).toBe(false)
|
||||
})
|
||||
|
||||
it('should allow langgenius plugins when scope is OFFICIAL_ONLY', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: false,
|
||||
plugin_installation_scope: InstallationScope.OFFICIAL_ONLY,
|
||||
},
|
||||
}
|
||||
|
||||
expect(pluginInstallLimit(basePlugin as never, features as never).canInstall).toBe(true)
|
||||
})
|
||||
|
||||
it('should deny non-official plugins when scope is OFFICIAL_ONLY', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: false,
|
||||
plugin_installation_scope: InstallationScope.OFFICIAL_ONLY,
|
||||
},
|
||||
}
|
||||
const plugin = { ...basePlugin, verification: { authorized_category: 'community' } }
|
||||
|
||||
expect(pluginInstallLimit(plugin as never, features as never).canInstall).toBe(false)
|
||||
})
|
||||
|
||||
it('should allow partner plugins when scope is OFFICIAL_AND_PARTNER', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: false,
|
||||
plugin_installation_scope: InstallationScope.OFFICIAL_AND_PARTNER,
|
||||
},
|
||||
}
|
||||
const plugin = { ...basePlugin, verification: { authorized_category: 'partner' } }
|
||||
|
||||
expect(pluginInstallLimit(plugin as never, features as never).canInstall).toBe(true)
|
||||
})
|
||||
|
||||
it('should deny github plugins when restrict_to_marketplace_only is true', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: true,
|
||||
plugin_installation_scope: InstallationScope.ALL,
|
||||
},
|
||||
}
|
||||
const plugin = { ...basePlugin, from: 'github' as const }
|
||||
|
||||
expect(pluginInstallLimit(plugin as never, features as never).canInstall).toBe(false)
|
||||
})
|
||||
|
||||
it('should deny package plugins when restrict_to_marketplace_only is true', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: true,
|
||||
plugin_installation_scope: InstallationScope.ALL,
|
||||
},
|
||||
}
|
||||
const plugin = { ...basePlugin, from: 'package' as const }
|
||||
|
||||
expect(pluginInstallLimit(plugin as never, features as never).canInstall).toBe(false)
|
||||
})
|
||||
|
||||
it('should allow marketplace plugins even when restrict_to_marketplace_only is true', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: true,
|
||||
plugin_installation_scope: InstallationScope.ALL,
|
||||
},
|
||||
}
|
||||
|
||||
expect(pluginInstallLimit(basePlugin as never, features as never).canInstall).toBe(true)
|
||||
})
|
||||
|
||||
it('should default to langgenius when no verification info', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: false,
|
||||
plugin_installation_scope: InstallationScope.OFFICIAL_ONLY,
|
||||
},
|
||||
}
|
||||
const plugin = { from: 'marketplace' as const }
|
||||
|
||||
expect(pluginInstallLimit(plugin as never, features as never).canInstall).toBe(true)
|
||||
})
|
||||
|
||||
it('should fallback to canInstall true for unrecognized scope', () => {
|
||||
const features = {
|
||||
plugin_installation_permission: {
|
||||
restrict_to_marketplace_only: false,
|
||||
plugin_installation_scope: 'unknown-scope' as InstallationScope,
|
||||
},
|
||||
}
|
||||
|
||||
expect(pluginInstallLimit(basePlugin as never, features as never).canInstall).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('usePluginInstallLimit', () => {
|
||||
it('should return canInstall from pluginInstallLimit using global store', async () => {
|
||||
const { default: usePluginInstallLimit } = await import('../use-install-plugin-limit')
|
||||
const plugin = { from: 'marketplace' as const, verification: { authorized_category: 'langgenius' } }
|
||||
|
||||
const { result } = renderHook(() => usePluginInstallLimit(plugin as never))
|
||||
|
||||
expect(result.current.canInstall).toBe(true)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,168 @@
|
||||
import { renderHook } from '@testing-library/react'
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { PluginCategoryEnum } from '../../../types'
|
||||
|
||||
// Mock invalidation / refresh functions
|
||||
const mockInvalidateInstalledPluginList = vi.fn()
|
||||
const mockRefetchLLMModelList = vi.fn()
|
||||
const mockRefetchEmbeddingModelList = vi.fn()
|
||||
const mockRefetchRerankModelList = vi.fn()
|
||||
const mockRefreshModelProviders = vi.fn()
|
||||
const mockInvalidateAllToolProviders = vi.fn()
|
||||
const mockInvalidateAllBuiltInTools = vi.fn()
|
||||
const mockInvalidateAllDataSources = vi.fn()
|
||||
const mockInvalidateDataSourceListAuth = vi.fn()
|
||||
const mockInvalidateStrategyProviders = vi.fn()
|
||||
const mockInvalidateAllTriggerPlugins = vi.fn()
|
||||
const mockInvalidateRAGRecommendedPlugins = vi.fn()
|
||||
|
||||
vi.mock('@/service/use-plugins', () => ({
|
||||
useInvalidateInstalledPluginList: () => mockInvalidateInstalledPluginList,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/declarations', () => ({
|
||||
ModelTypeEnum: { textGeneration: 'text-generation', textEmbedding: 'text-embedding', rerank: 'rerank' },
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
||||
useModelList: (type: string) => {
|
||||
const map: Record<string, { mutate: ReturnType<typeof vi.fn> }> = {
|
||||
'text-generation': { mutate: mockRefetchLLMModelList },
|
||||
'text-embedding': { mutate: mockRefetchEmbeddingModelList },
|
||||
'rerank': { mutate: mockRefetchRerankModelList },
|
||||
}
|
||||
return map[type] ?? { mutate: vi.fn() }
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContext: () => ({ refreshModelProviders: mockRefreshModelProviders }),
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-tools', () => ({
|
||||
useInvalidateAllToolProviders: () => mockInvalidateAllToolProviders,
|
||||
useInvalidateAllBuiltInTools: () => mockInvalidateAllBuiltInTools,
|
||||
useInvalidateRAGRecommendedPlugins: () => mockInvalidateRAGRecommendedPlugins,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-pipeline', () => ({
|
||||
useInvalidDataSourceList: () => mockInvalidateAllDataSources,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-datasource', () => ({
|
||||
useInvalidDataSourceListAuth: () => mockInvalidateDataSourceListAuth,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-strategy', () => ({
|
||||
useInvalidateStrategyProviders: () => mockInvalidateStrategyProviders,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/use-triggers', () => ({
|
||||
useInvalidateAllTriggerPlugins: () => mockInvalidateAllTriggerPlugins,
|
||||
}))
|
||||
|
||||
const { default: useRefreshPluginList } = await import('../use-refresh-plugin-list')
|
||||
|
||||
describe('useRefreshPluginList', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('should always invalidate installed plugin list', () => {
|
||||
const { result } = renderHook(() => useRefreshPluginList())
|
||||
|
||||
result.current.refreshPluginList()
|
||||
|
||||
expect(mockInvalidateInstalledPluginList).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should refresh tool providers for tool category manifest', () => {
|
||||
const { result } = renderHook(() => useRefreshPluginList())
|
||||
|
||||
result.current.refreshPluginList({ category: PluginCategoryEnum.tool } as never)
|
||||
|
||||
expect(mockInvalidateAllToolProviders).toHaveBeenCalledTimes(1)
|
||||
expect(mockInvalidateAllBuiltInTools).toHaveBeenCalledTimes(1)
|
||||
expect(mockInvalidateRAGRecommendedPlugins).toHaveBeenCalledWith('tool')
|
||||
})
|
||||
|
||||
it('should refresh model lists for model category manifest', () => {
|
||||
const { result } = renderHook(() => useRefreshPluginList())
|
||||
|
||||
result.current.refreshPluginList({ category: PluginCategoryEnum.model } as never)
|
||||
|
||||
expect(mockRefreshModelProviders).toHaveBeenCalledTimes(1)
|
||||
expect(mockRefetchLLMModelList).toHaveBeenCalledTimes(1)
|
||||
expect(mockRefetchEmbeddingModelList).toHaveBeenCalledTimes(1)
|
||||
expect(mockRefetchRerankModelList).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should refresh datasource lists for datasource category manifest', () => {
|
||||
const { result } = renderHook(() => useRefreshPluginList())
|
||||
|
||||
result.current.refreshPluginList({ category: PluginCategoryEnum.datasource } as never)
|
||||
|
||||
expect(mockInvalidateAllDataSources).toHaveBeenCalledTimes(1)
|
||||
expect(mockInvalidateDataSourceListAuth).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should refresh trigger plugins for trigger category manifest', () => {
|
||||
const { result } = renderHook(() => useRefreshPluginList())
|
||||
|
||||
result.current.refreshPluginList({ category: PluginCategoryEnum.trigger } as never)
|
||||
|
||||
expect(mockInvalidateAllTriggerPlugins).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should refresh strategy providers for agent category manifest', () => {
|
||||
const { result } = renderHook(() => useRefreshPluginList())
|
||||
|
||||
result.current.refreshPluginList({ category: PluginCategoryEnum.agent } as never)
|
||||
|
||||
expect(mockInvalidateStrategyProviders).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should refresh all types when refreshAllType is true', () => {
|
||||
const { result } = renderHook(() => useRefreshPluginList())
|
||||
|
||||
result.current.refreshPluginList(undefined, true)
|
||||
|
||||
expect(mockInvalidateInstalledPluginList).toHaveBeenCalledTimes(1)
|
||||
expect(mockInvalidateAllToolProviders).toHaveBeenCalledTimes(1)
|
||||
expect(mockInvalidateAllBuiltInTools).toHaveBeenCalledTimes(1)
|
||||
expect(mockInvalidateRAGRecommendedPlugins).toHaveBeenCalledWith('tool')
|
||||
expect(mockInvalidateAllTriggerPlugins).toHaveBeenCalledTimes(1)
|
||||
expect(mockInvalidateAllDataSources).toHaveBeenCalledTimes(1)
|
||||
expect(mockInvalidateDataSourceListAuth).toHaveBeenCalledTimes(1)
|
||||
expect(mockRefreshModelProviders).toHaveBeenCalledTimes(1)
|
||||
expect(mockRefetchLLMModelList).toHaveBeenCalledTimes(1)
|
||||
expect(mockRefetchEmbeddingModelList).toHaveBeenCalledTimes(1)
|
||||
expect(mockRefetchRerankModelList).toHaveBeenCalledTimes(1)
|
||||
expect(mockInvalidateStrategyProviders).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should not refresh category-specific lists when manifest is null', () => {
|
||||
const { result } = renderHook(() => useRefreshPluginList())
|
||||
|
||||
result.current.refreshPluginList(null)
|
||||
|
||||
expect(mockInvalidateInstalledPluginList).toHaveBeenCalledTimes(1)
|
||||
expect(mockInvalidateAllToolProviders).not.toHaveBeenCalled()
|
||||
expect(mockRefreshModelProviders).not.toHaveBeenCalled()
|
||||
expect(mockInvalidateAllDataSources).not.toHaveBeenCalled()
|
||||
expect(mockInvalidateAllTriggerPlugins).not.toHaveBeenCalled()
|
||||
expect(mockInvalidateStrategyProviders).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should not refresh unrelated categories for a specific manifest', () => {
|
||||
const { result } = renderHook(() => useRefreshPluginList())
|
||||
|
||||
result.current.refreshPluginList({ category: PluginCategoryEnum.tool } as never)
|
||||
|
||||
expect(mockInvalidateAllToolProviders).toHaveBeenCalledTimes(1)
|
||||
expect(mockRefreshModelProviders).not.toHaveBeenCalled()
|
||||
expect(mockInvalidateAllDataSources).not.toHaveBeenCalled()
|
||||
expect(mockInvalidateAllTriggerPlugins).not.toHaveBeenCalled()
|
||||
expect(mockInvalidateStrategyProviders).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user