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:
Coding On Star
2026-02-12 10:04:56 +08:00
committed by GitHub
parent 10f85074e8
commit d6b025e91e
195 changed files with 12219 additions and 7840 deletions

View File

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

View File

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

View File

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

View File

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