mirror of
https://github.com/langgenius/dify.git
synced 2026-05-03 00:48:04 +08:00
test(web): add coverage for workflow plugin install flows
This commit is contained in:
@ -40,6 +40,9 @@ vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', ()
|
||||
|
||||
type CheckValidFn = (data: CommonNodeType, t: unknown, extra?: unknown) => { errorMessage: string }
|
||||
const mockNodesMap: Record<string, { checkValid: CheckValidFn, metaData: { isStart: boolean, isRequired: boolean } }> = {}
|
||||
let mockModelProviders: Array<{ provider: string }> = []
|
||||
let mockUsedVars: string[][] = []
|
||||
const mockAvailableVarMap: Record<string, { availableVars: Array<{ nodeId: string, vars: Array<{ variable: string }> }> }> = {}
|
||||
|
||||
vi.mock('../use-nodes-meta-data', () => ({
|
||||
useNodesMetaData: () => ({
|
||||
@ -50,10 +53,10 @@ vi.mock('../use-nodes-meta-data', () => ({
|
||||
|
||||
vi.mock('../use-nodes-available-var-list', () => ({
|
||||
default: (nodes: Node[]) => {
|
||||
const map: Record<string, { availableVars: never[] }> = {}
|
||||
const map: Record<string, { availableVars: Array<{ nodeId: string, vars: Array<{ variable: string }> }> }> = {}
|
||||
if (nodes) {
|
||||
for (const n of nodes)
|
||||
map[n.id] = { availableVars: [] }
|
||||
map[n.id] = mockAvailableVarMap[n.id] ?? { availableVars: [] }
|
||||
}
|
||||
return map
|
||||
},
|
||||
@ -61,7 +64,7 @@ vi.mock('../use-nodes-available-var-list', () => ({
|
||||
}))
|
||||
|
||||
vi.mock('../../nodes/_base/components/variable/utils', () => ({
|
||||
getNodeUsedVars: () => [],
|
||||
getNodeUsedVars: () => mockUsedVars,
|
||||
isSpecialVar: () => false,
|
||||
}))
|
||||
|
||||
@ -91,6 +94,11 @@ vi.mock('@/context/i18n', () => ({
|
||||
useGetLanguage: () => 'en',
|
||||
}))
|
||||
|
||||
vi.mock('@/context/provider-context', () => ({
|
||||
useProviderContextSelector: (selector: (state: { modelProviders: Array<{ provider: string }> }) => unknown) =>
|
||||
selector({ modelProviders: mockModelProviders }),
|
||||
}))
|
||||
|
||||
// useWorkflowNodes reads from WorkflowContext (real store via renderWorkflowHook)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@ -125,6 +133,9 @@ beforeEach(() => {
|
||||
resetReactFlowMockState()
|
||||
resetFixtureCounters()
|
||||
Object.keys(mockNodesMap).forEach(k => delete mockNodesMap[k])
|
||||
Object.keys(mockAvailableVarMap).forEach(k => delete mockAvailableVarMap[k])
|
||||
mockModelProviders = []
|
||||
mockUsedVars = []
|
||||
setupNodesMap()
|
||||
})
|
||||
|
||||
@ -236,6 +247,9 @@ describe('useChecklist', () => {
|
||||
expect(warning).toBeDefined()
|
||||
expect(warning!.canNavigate).toBe(false)
|
||||
expect(warning!.disableGoTo).toBe(true)
|
||||
expect(warning!.isPluginMissing).toBe(true)
|
||||
expect(warning!.pluginUniqueIdentifier).toBe('plugin/tool@0.0.1')
|
||||
expect(warning!.errorMessages).toContain('workflow.nodes.common.pluginNotInstalled')
|
||||
})
|
||||
|
||||
it('should report required node types that are missing', () => {
|
||||
@ -282,6 +296,73 @@ describe('useChecklist', () => {
|
||||
const alienWarning = result.current.find((item: ChecklistItem) => item.id === 'alien')
|
||||
expect(alienWarning).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should report configure model errors when an llm model provider plugin is missing', () => {
|
||||
const startNode = createNode({ id: 'start', data: { type: BlockEnum.Start, title: 'Start' } })
|
||||
const llmNode = createNode({
|
||||
id: 'llm',
|
||||
data: {
|
||||
type: BlockEnum.LLM,
|
||||
title: 'LLM',
|
||||
model: {
|
||||
provider: 'langgenius/openai/openai',
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const edges = [
|
||||
createEdge({ source: 'start', target: 'llm' }),
|
||||
]
|
||||
|
||||
const { result } = renderWorkflowHook(
|
||||
() => useChecklist([startNode, llmNode], edges),
|
||||
)
|
||||
|
||||
const warning = result.current.find((item: ChecklistItem) => item.id === 'llm')
|
||||
expect(warning).toBeDefined()
|
||||
expect(warning!.errorMessages).toContain('workflow.errorMsg.configureModel')
|
||||
expect(warning!.canNavigate).toBe(true)
|
||||
})
|
||||
|
||||
it('should accumulate validation and invalid variable errors for the same node', () => {
|
||||
mockNodesMap[BlockEnum.LLM] = {
|
||||
checkValid: () => ({ errorMessage: 'Model not configured' }),
|
||||
metaData: { isStart: false, isRequired: false },
|
||||
}
|
||||
mockUsedVars = [['start', 'missingVar']]
|
||||
mockAvailableVarMap.llm = {
|
||||
availableVars: [
|
||||
{
|
||||
nodeId: 'start',
|
||||
vars: [{ variable: 'existingVar' }],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
const startNode = createNode({ id: 'start', data: { type: BlockEnum.Start, title: 'Start' } })
|
||||
const llmNode = createNode({
|
||||
id: 'llm',
|
||||
data: {
|
||||
type: BlockEnum.LLM,
|
||||
title: 'LLM',
|
||||
},
|
||||
})
|
||||
|
||||
const edges = [
|
||||
createEdge({ source: 'start', target: 'llm' }),
|
||||
]
|
||||
|
||||
const { result } = renderWorkflowHook(
|
||||
() => useChecklist([startNode, llmNode], edges),
|
||||
)
|
||||
|
||||
const warning = result.current.find((item: ChecklistItem) => item.id === 'llm')
|
||||
expect(warning).toBeDefined()
|
||||
expect(warning!.errorMessages).toEqual([
|
||||
'Model not configured',
|
||||
'workflow.errorMsg.invalidVariable',
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
171
web/app/components/workflow/utils/plugin-install-check.spec.ts
Normal file
171
web/app/components/workflow/utils/plugin-install-check.spec.ts
Normal file
@ -0,0 +1,171 @@
|
||||
import type { TriggerWithProvider } from '../block-selector/types'
|
||||
import type { CommonNodeType, ToolWithProvider } from '../types'
|
||||
import { CollectionType } from '@/app/components/tools/types'
|
||||
import { BlockEnum } from '../types'
|
||||
import {
|
||||
isNodePluginMissing,
|
||||
isPluginDependentNode,
|
||||
matchDataSource,
|
||||
matchToolInCollection,
|
||||
matchTriggerProvider,
|
||||
} from './plugin-install-check'
|
||||
|
||||
const createTool = (overrides: Partial<ToolWithProvider> = {}): ToolWithProvider => ({
|
||||
id: 'langgenius/search/search',
|
||||
name: 'search',
|
||||
plugin_id: 'plugin-search',
|
||||
provider: 'search-provider',
|
||||
plugin_unique_identifier: 'plugin-search@1.0.0',
|
||||
...overrides,
|
||||
} as ToolWithProvider)
|
||||
|
||||
const createTriggerProvider = (overrides: Partial<TriggerWithProvider> = {}): TriggerWithProvider => ({
|
||||
id: 'trigger-provider-id',
|
||||
name: 'trigger-provider',
|
||||
plugin_id: 'trigger-plugin',
|
||||
...overrides,
|
||||
} as TriggerWithProvider)
|
||||
|
||||
describe('plugin install check', () => {
|
||||
describe('isPluginDependentNode', () => {
|
||||
it('should return true for plugin dependent node types', () => {
|
||||
expect(isPluginDependentNode(BlockEnum.Tool)).toBe(true)
|
||||
expect(isPluginDependentNode(BlockEnum.DataSource)).toBe(true)
|
||||
expect(isPluginDependentNode(BlockEnum.TriggerPlugin)).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for non-plugin node types', () => {
|
||||
expect(isPluginDependentNode(BlockEnum.LLM)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('matchToolInCollection', () => {
|
||||
const collection = [createTool()]
|
||||
|
||||
it('should match a tool by plugin id', () => {
|
||||
expect(matchToolInCollection(collection, { plugin_id: 'plugin-search' })).toEqual(collection[0])
|
||||
})
|
||||
|
||||
it('should match a tool by legacy provider id', () => {
|
||||
expect(matchToolInCollection(collection, { provider_id: 'search' })).toEqual(collection[0])
|
||||
})
|
||||
|
||||
it('should match a tool by provider name', () => {
|
||||
expect(matchToolInCollection(collection, { provider_name: 'search' })).toEqual(collection[0])
|
||||
})
|
||||
|
||||
it('should return undefined when no tool matches', () => {
|
||||
expect(matchToolInCollection(collection, { plugin_id: 'missing-plugin' })).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('matchTriggerProvider', () => {
|
||||
const providers = [createTriggerProvider()]
|
||||
|
||||
it('should match a trigger provider by name', () => {
|
||||
expect(matchTriggerProvider(providers, { provider_name: 'trigger-provider' })).toEqual(providers[0])
|
||||
})
|
||||
|
||||
it('should match a trigger provider by id', () => {
|
||||
expect(matchTriggerProvider(providers, { provider_id: 'trigger-provider-id' })).toEqual(providers[0])
|
||||
})
|
||||
|
||||
it('should match a trigger provider by plugin id', () => {
|
||||
expect(matchTriggerProvider(providers, { plugin_id: 'trigger-plugin' })).toEqual(providers[0])
|
||||
})
|
||||
})
|
||||
|
||||
describe('matchDataSource', () => {
|
||||
const dataSources = [createTool({
|
||||
provider: 'knowledge-provider',
|
||||
plugin_id: 'knowledge-plugin',
|
||||
plugin_unique_identifier: 'knowledge-plugin@1.0.0',
|
||||
})]
|
||||
|
||||
it('should match a data source by unique identifier', () => {
|
||||
expect(matchDataSource(dataSources, { plugin_unique_identifier: 'knowledge-plugin@1.0.0' })).toEqual(dataSources[0])
|
||||
})
|
||||
|
||||
it('should match a data source by plugin id', () => {
|
||||
expect(matchDataSource(dataSources, { plugin_id: 'knowledge-plugin' })).toEqual(dataSources[0])
|
||||
})
|
||||
|
||||
it('should match a data source by provider name', () => {
|
||||
expect(matchDataSource(dataSources, { provider_name: 'knowledge-provider' })).toEqual(dataSources[0])
|
||||
})
|
||||
})
|
||||
|
||||
describe('isNodePluginMissing', () => {
|
||||
it('should report missing tool plugins when the collection is loaded but unmatched', () => {
|
||||
const node = {
|
||||
type: BlockEnum.Tool,
|
||||
title: 'Tool',
|
||||
desc: '',
|
||||
provider_type: CollectionType.builtIn,
|
||||
provider_id: 'missing-provider',
|
||||
plugin_unique_identifier: 'missing-plugin@1.0.0',
|
||||
} as CommonNodeType
|
||||
|
||||
expect(isNodePluginMissing(node, { builtInTools: [createTool()] })).toBe(true)
|
||||
})
|
||||
|
||||
it('should keep tool nodes installable when the collection has not loaded yet', () => {
|
||||
const node = {
|
||||
type: BlockEnum.Tool,
|
||||
title: 'Tool',
|
||||
desc: '',
|
||||
provider_type: CollectionType.builtIn,
|
||||
provider_id: 'missing-provider',
|
||||
plugin_unique_identifier: 'missing-plugin@1.0.0',
|
||||
} as CommonNodeType
|
||||
|
||||
expect(isNodePluginMissing(node, { builtInTools: undefined })).toBe(false)
|
||||
})
|
||||
|
||||
it('should ignore unmatched tool nodes without plugin identifiers', () => {
|
||||
const node = {
|
||||
type: BlockEnum.Tool,
|
||||
title: 'Tool',
|
||||
desc: '',
|
||||
provider_type: CollectionType.builtIn,
|
||||
provider_id: 'missing-provider',
|
||||
} as CommonNodeType
|
||||
|
||||
expect(isNodePluginMissing(node, { builtInTools: [createTool()] })).toBe(false)
|
||||
})
|
||||
|
||||
it('should report missing trigger plugins when no provider matches', () => {
|
||||
const node = {
|
||||
type: BlockEnum.TriggerPlugin,
|
||||
title: 'Trigger',
|
||||
desc: '',
|
||||
provider_id: 'missing-trigger',
|
||||
plugin_unique_identifier: 'trigger-plugin@1.0.0',
|
||||
} as CommonNodeType
|
||||
|
||||
expect(isNodePluginMissing(node, { triggerPlugins: [createTriggerProvider()] })).toBe(true)
|
||||
})
|
||||
|
||||
it('should report missing data source plugins when the list is loaded but unmatched', () => {
|
||||
const node = {
|
||||
type: BlockEnum.DataSource,
|
||||
title: 'Data Source',
|
||||
desc: '',
|
||||
provider_name: 'missing-provider',
|
||||
plugin_unique_identifier: 'missing-data-source@1.0.0',
|
||||
} as CommonNodeType
|
||||
|
||||
expect(isNodePluginMissing(node, { dataSourceList: [createTool()] })).toBe(true)
|
||||
})
|
||||
|
||||
it('should return false for unsupported node types', () => {
|
||||
const node = {
|
||||
type: BlockEnum.LLM,
|
||||
title: 'LLM',
|
||||
desc: '',
|
||||
} as CommonNodeType
|
||||
|
||||
expect(isNodePluginMissing(node, {})).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user