mirror of
https://github.com/langgenius/dify.git
synced 2026-03-21 06:18:27 +08:00
test(web): add coverage for workflow plugin install flows
This commit is contained in:
@ -0,0 +1,127 @@
|
||||
import type { Credential, ModelProvider } from '../../declarations'
|
||||
import { act, renderHook } from '@testing-library/react'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useActivateCredential } from './use-activate-credential'
|
||||
|
||||
const mockMutate = vi.fn()
|
||||
const mockUpdateModelProviders = vi.fn()
|
||||
const mockUpdateModelList = vi.fn()
|
||||
let mockIsPending = false
|
||||
|
||||
vi.mock('@/service/use-models', () => ({
|
||||
useActiveProviderCredential: () => ({
|
||||
mutate: mockMutate,
|
||||
isPending: mockIsPending,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('../../hooks', () => ({
|
||||
useUpdateModelProviders: () => mockUpdateModelProviders,
|
||||
useUpdateModelList: () => mockUpdateModelList,
|
||||
}))
|
||||
|
||||
const createProvider = (overrides: Partial<ModelProvider> = {}): ModelProvider => ({
|
||||
provider: 'langgenius/openai/openai',
|
||||
supported_model_types: ['llm', 'text-embedding'],
|
||||
custom_configuration: {
|
||||
current_credential_id: 'cred-1',
|
||||
available_credentials: [
|
||||
{ credential_id: 'cred-1', credential_name: 'Primary' },
|
||||
{ credential_id: 'cred-2', credential_name: 'Backup' },
|
||||
],
|
||||
},
|
||||
...overrides,
|
||||
} as unknown as ModelProvider)
|
||||
|
||||
const createCredential = (overrides: Partial<Credential> = {}): Credential => ({
|
||||
credential_id: 'cred-2',
|
||||
credential_name: 'Backup',
|
||||
...overrides,
|
||||
} as Credential)
|
||||
|
||||
describe('useActivateCredential', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockIsPending = false
|
||||
vi.spyOn(Toast, 'notify').mockImplementation(() => ({ clear: vi.fn() }))
|
||||
})
|
||||
|
||||
it('should expose the current credential id by default', () => {
|
||||
const { result } = renderHook(() => useActivateCredential(createProvider()))
|
||||
|
||||
expect(result.current.selectedCredentialId).toBe('cred-1')
|
||||
expect(result.current.isActivating).toBe(false)
|
||||
})
|
||||
|
||||
it('should expose the pending mutation state', () => {
|
||||
mockIsPending = true
|
||||
|
||||
const { result } = renderHook(() => useActivateCredential(createProvider()))
|
||||
|
||||
expect(result.current.isActivating).toBe(true)
|
||||
})
|
||||
|
||||
it('should skip mutation when the selected credential is already active', () => {
|
||||
const { result } = renderHook(() => useActivateCredential(createProvider()))
|
||||
|
||||
act(() => {
|
||||
result.current.activate(createCredential({ credential_id: 'cred-1' }))
|
||||
})
|
||||
|
||||
expect(mockMutate).not.toHaveBeenCalled()
|
||||
expect(result.current.selectedCredentialId).toBe('cred-1')
|
||||
})
|
||||
|
||||
it('should optimistically select the credential and refresh provider data on success', () => {
|
||||
const { result } = renderHook(() => useActivateCredential(createProvider()))
|
||||
|
||||
act(() => {
|
||||
result.current.activate(createCredential())
|
||||
})
|
||||
|
||||
expect(result.current.selectedCredentialId).toBe('cred-2')
|
||||
expect(mockMutate).toHaveBeenCalledWith(
|
||||
{ credential_id: 'cred-2' },
|
||||
expect.objectContaining({
|
||||
onSuccess: expect.any(Function),
|
||||
onError: expect.any(Function),
|
||||
}),
|
||||
)
|
||||
|
||||
const [, callbacks] = mockMutate.mock.calls[0]
|
||||
|
||||
act(() => {
|
||||
callbacks.onSuccess()
|
||||
})
|
||||
|
||||
expect(Toast.notify).toHaveBeenCalledWith({
|
||||
type: 'success',
|
||||
message: 'common.api.actionSuccess',
|
||||
})
|
||||
expect(mockUpdateModelProviders).toHaveBeenCalledTimes(1)
|
||||
expect(mockUpdateModelList).toHaveBeenNthCalledWith(1, 'llm')
|
||||
expect(mockUpdateModelList).toHaveBeenNthCalledWith(2, 'text-embedding')
|
||||
})
|
||||
|
||||
it('should reset the optimistic selection and show an error toast when activation fails', () => {
|
||||
const { result } = renderHook(() => useActivateCredential(createProvider()))
|
||||
|
||||
act(() => {
|
||||
result.current.activate(createCredential())
|
||||
})
|
||||
|
||||
expect(result.current.selectedCredentialId).toBe('cred-2')
|
||||
|
||||
const [, callbacks] = mockMutate.mock.calls[0]
|
||||
|
||||
act(() => {
|
||||
callbacks.onError()
|
||||
})
|
||||
|
||||
expect(result.current.selectedCredentialId).toBe('cred-1')
|
||||
expect(Toast.notify).toHaveBeenCalledWith({
|
||||
type: 'error',
|
||||
message: 'common.actionMsg.modifiedUnsuccessfully',
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,226 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { PluginDetail } from '@/app/components/plugins/types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import { PluginSource } from '@/app/components/plugins/types'
|
||||
import ProviderCardActions from './provider-card-actions'
|
||||
|
||||
const mockHandleUpdate = vi.fn()
|
||||
const mockHandleUpdatedFromMarketplace = vi.fn()
|
||||
const mockHandleDelete = vi.fn()
|
||||
const mockGetMarketplaceUrl = vi.fn()
|
||||
const mockShowPluginInfo = vi.fn()
|
||||
const mockShowDeleteConfirm = vi.fn()
|
||||
const mockSetTargetVersion = vi.fn()
|
||||
const mockSetVersionPickerOpen = vi.fn()
|
||||
|
||||
let mockHeaderState = {
|
||||
modalStates: {
|
||||
showPluginInfo: mockShowPluginInfo,
|
||||
showDeleteConfirm: mockShowDeleteConfirm,
|
||||
},
|
||||
versionPicker: {
|
||||
isShow: false,
|
||||
setIsShow: mockSetVersionPickerOpen,
|
||||
setTargetVersion: mockSetTargetVersion,
|
||||
targetVersion: undefined,
|
||||
isDowngrade: false,
|
||||
},
|
||||
hasNewVersion: true,
|
||||
isAutoUpgradeEnabled: false,
|
||||
isFromMarketplace: true,
|
||||
isFromGitHub: false,
|
||||
}
|
||||
|
||||
vi.mock('@/app/components/plugins/plugin-detail-panel/detail-header/hooks', () => ({
|
||||
useDetailHeaderState: () => mockHeaderState,
|
||||
usePluginOperations: () => ({
|
||||
handleUpdate: mockHandleUpdate,
|
||||
handleUpdatedFromMarketplace: mockHandleUpdatedFromMarketplace,
|
||||
handleDelete: mockHandleDelete,
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/plugins/plugin-detail-panel/detail-header/components', () => ({
|
||||
HeaderModals: ({ targetVersion, isDowngrade, isAutoUpgradeEnabled }: {
|
||||
targetVersion?: { version: string, unique_identifier: string }
|
||||
isDowngrade: boolean
|
||||
isAutoUpgradeEnabled: boolean
|
||||
}) => (
|
||||
<div
|
||||
data-testid="header-modals"
|
||||
data-target-version={targetVersion?.version ?? ''}
|
||||
data-is-downgrade={String(isDowngrade)}
|
||||
data-auto-upgrade={String(isAutoUpgradeEnabled)}
|
||||
/>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/plugins/plugin-detail-panel/operation-dropdown', () => ({
|
||||
default: ({ detailUrl, onInfo, onCheckVersion, onRemove }: {
|
||||
detailUrl: string
|
||||
onInfo: () => void
|
||||
onCheckVersion: () => void
|
||||
onRemove: () => void
|
||||
}) => (
|
||||
<div data-testid="operation-dropdown" data-detail-url={detailUrl}>
|
||||
<button type="button" onClick={onInfo}>info</button>
|
||||
<button type="button" onClick={onCheckVersion}>check version</button>
|
||||
<button type="button" onClick={onRemove}>remove</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/plugins/update-plugin/plugin-version-picker', () => ({
|
||||
default: ({ trigger, onSelect, disabled }: {
|
||||
trigger: ReactNode
|
||||
onSelect: (state: { version: string, unique_identifier: string, isDowngrade?: boolean }) => void
|
||||
disabled?: boolean
|
||||
}) => (
|
||||
<div data-testid="plugin-version-picker" data-disabled={String(Boolean(disabled))}>
|
||||
{trigger}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onSelect({ version: '2.0.0', unique_identifier: 'plugin@2.0.0', isDowngrade: true })}
|
||||
>
|
||||
select version
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/context/i18n', () => ({
|
||||
useLocale: () => 'en-US',
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/use-theme', () => ({
|
||||
default: () => ({ theme: 'light' }),
|
||||
}))
|
||||
|
||||
vi.mock('@/utils/var', () => ({
|
||||
getMarketplaceUrl: (...args: unknown[]) => mockGetMarketplaceUrl(...args),
|
||||
}))
|
||||
|
||||
const createDetail = (overrides: Partial<PluginDetail> = {}): PluginDetail => ({
|
||||
plugin_id: 'plugin-id',
|
||||
plugin_unique_identifier: 'plugin-id@1.0.0',
|
||||
name: 'provider-plugin',
|
||||
source: PluginSource.marketplace,
|
||||
version: '1.0.0',
|
||||
latest_version: '2.0.0',
|
||||
latest_unique_identifier: 'plugin-id@2.0.0',
|
||||
declaration: {
|
||||
author: 'langgenius',
|
||||
name: 'provider-plugin',
|
||||
},
|
||||
meta: undefined,
|
||||
...overrides,
|
||||
} as PluginDetail)
|
||||
|
||||
describe('ProviderCardActions', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockHeaderState = {
|
||||
modalStates: {
|
||||
showPluginInfo: mockShowPluginInfo,
|
||||
showDeleteConfirm: mockShowDeleteConfirm,
|
||||
},
|
||||
versionPicker: {
|
||||
isShow: false,
|
||||
setIsShow: mockSetVersionPickerOpen,
|
||||
setTargetVersion: mockSetTargetVersion,
|
||||
targetVersion: undefined,
|
||||
isDowngrade: false,
|
||||
},
|
||||
hasNewVersion: true,
|
||||
isAutoUpgradeEnabled: false,
|
||||
isFromMarketplace: true,
|
||||
isFromGitHub: false,
|
||||
}
|
||||
mockGetMarketplaceUrl.mockReturnValue('https://marketplace.example.com/plugins/langgenius/provider-plugin')
|
||||
})
|
||||
|
||||
it('should render version controls for marketplace plugins and handle manual version selection', () => {
|
||||
render(<ProviderCardActions detail={createDetail()} />)
|
||||
|
||||
expect(screen.getByText('1.0.0')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('plugin-version-picker')).toHaveAttribute('data-disabled', 'false')
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'select version' }))
|
||||
|
||||
expect(mockSetTargetVersion).toHaveBeenCalledWith({
|
||||
version: '2.0.0',
|
||||
unique_identifier: 'plugin@2.0.0',
|
||||
isDowngrade: true,
|
||||
})
|
||||
expect(mockHandleUpdate).toHaveBeenCalledWith(true)
|
||||
})
|
||||
|
||||
it('should trigger the latest marketplace update when clicking the update button', () => {
|
||||
render(<ProviderCardActions detail={createDetail()} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.detailPanel.operation.update' }))
|
||||
|
||||
expect(mockSetTargetVersion).toHaveBeenCalledWith({
|
||||
version: '2.0.0',
|
||||
unique_identifier: 'plugin-id@2.0.0',
|
||||
})
|
||||
expect(mockHandleUpdate).toHaveBeenCalledWith()
|
||||
})
|
||||
|
||||
it('should pass the marketplace detail url to the operation dropdown', () => {
|
||||
render(<ProviderCardActions detail={createDetail()} />)
|
||||
|
||||
expect(mockGetMarketplaceUrl).toHaveBeenCalledWith('/plugins/langgenius/provider-plugin', {
|
||||
language: 'en-US',
|
||||
theme: 'light',
|
||||
})
|
||||
expect(screen.getByTestId('operation-dropdown')).toHaveAttribute(
|
||||
'data-detail-url',
|
||||
'https://marketplace.example.com/plugins/langgenius/provider-plugin',
|
||||
)
|
||||
})
|
||||
|
||||
it('should relay operation dropdown actions', () => {
|
||||
render(<ProviderCardActions detail={createDetail()} />)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'info' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'check version' }))
|
||||
fireEvent.click(screen.getByRole('button', { name: 'remove' }))
|
||||
|
||||
expect(mockShowPluginInfo).toHaveBeenCalledTimes(1)
|
||||
expect(mockHandleUpdate).toHaveBeenCalledTimes(1)
|
||||
expect(mockShowDeleteConfirm).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should use the GitHub repo url and skip marketplace version preselection for GitHub plugins', () => {
|
||||
mockHeaderState = {
|
||||
...mockHeaderState,
|
||||
hasNewVersion: false,
|
||||
isFromMarketplace: false,
|
||||
isFromGitHub: true,
|
||||
}
|
||||
|
||||
render(
|
||||
<ProviderCardActions detail={createDetail({
|
||||
source: PluginSource.github,
|
||||
meta: {
|
||||
repo: 'langgenius/provider-plugin',
|
||||
version: '1.0.0',
|
||||
package: 'provider-plugin.difypkg',
|
||||
},
|
||||
})}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('plugin-version-picker')).toHaveAttribute('data-disabled', 'true')
|
||||
expect(screen.getByTestId('operation-dropdown')).toHaveAttribute(
|
||||
'data-detail-url',
|
||||
'https://github.com/langgenius/provider-plugin',
|
||||
)
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: 'plugin.detailPanel.operation.update' }))
|
||||
|
||||
expect(mockSetTargetVersion).not.toHaveBeenCalled()
|
||||
expect(mockHandleUpdate).toHaveBeenCalledWith()
|
||||
})
|
||||
})
|
||||
@ -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