Files
dify/web/app/components/workflow/nodes/knowledge-base/node.spec.tsx
yyh bbe975c6bc feat: enhance model plugin workflow checks and model provider management UX (#33289)
Signed-off-by: yyh <yuanyouhuilyz@gmail.com>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: CodingOnStar <hanxujiang@dify.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Coding On Star <447357187@qq.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: -LAN- <laipz8200@outlook.com>
Co-authored-by: statxc <tyleradams93226@gmail.com>
2026-03-18 10:16:15 +08:00

234 lines
7.3 KiB
TypeScript

import type { KnowledgeBaseNodeType } from './types'
import type { ModelItem } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { CommonNodeType } from '@/app/components/workflow/types'
import { render, screen } from '@testing-library/react'
import {
ConfigurationMethodEnum,
ModelStatusEnum,
ModelTypeEnum,
} from '@/app/components/header/account-setting/model-provider-page/declarations'
import { BlockEnum } from '@/app/components/workflow/types'
import Node from './node'
import {
ChunkStructureEnum,
IndexMethodEnum,
RetrievalSearchMethodEnum,
} from './types'
const mockUseModelList = vi.hoisted(() => vi.fn())
const mockUseSettingsDisplay = vi.hoisted(() => vi.fn())
const mockUseEmbeddingModelStatus = vi.hoisted(() => vi.fn())
vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return {
...actual,
useQuery: () => ({ data: undefined }),
}
})
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', async (importOriginal) => {
const actual = await importOriginal<typeof import('@/app/components/header/account-setting/model-provider-page/hooks')>()
return {
...actual,
useLanguage: () => 'en_US',
useModelList: mockUseModelList,
}
})
vi.mock('./hooks/use-settings-display', () => ({
useSettingsDisplay: mockUseSettingsDisplay,
}))
vi.mock('./hooks/use-embedding-model-status', () => ({
useEmbeddingModelStatus: mockUseEmbeddingModelStatus,
}))
const createModelItem = (overrides: Partial<ModelItem> = {}): ModelItem => ({
model: 'text-embedding-3-large',
label: { en_US: 'Text Embedding 3 Large', zh_Hans: 'Text Embedding 3 Large' },
model_type: ModelTypeEnum.textEmbedding,
fetch_from: ConfigurationMethodEnum.predefinedModel,
status: ModelStatusEnum.active,
model_properties: {},
load_balancing_enabled: false,
...overrides,
})
const createNodeData = (overrides: Partial<CommonNodeType<KnowledgeBaseNodeType>> = {}): CommonNodeType<KnowledgeBaseNodeType> => ({
title: 'Knowledge Base',
desc: '',
type: BlockEnum.KnowledgeBase,
index_chunk_variable_selector: ['result'],
chunk_structure: ChunkStructureEnum.general,
indexing_technique: IndexMethodEnum.QUALIFIED,
embedding_model: 'text-embedding-3-large',
embedding_model_provider: 'openai',
keyword_number: 10,
retrieval_model: {
top_k: 3,
score_threshold_enabled: false,
score_threshold: 0.5,
search_method: RetrievalSearchMethodEnum.semantic,
},
...overrides,
})
describe('KnowledgeBaseNode', () => {
beforeEach(() => {
vi.clearAllMocks()
mockUseModelList.mockReturnValue({ data: [] })
mockUseSettingsDisplay.mockReturnValue({
[IndexMethodEnum.QUALIFIED]: 'High Quality',
[RetrievalSearchMethodEnum.semantic]: 'Vector Search',
})
mockUseEmbeddingModelStatus.mockReturnValue({
providerMeta: undefined,
modelProvider: undefined,
currentModel: createModelItem(),
status: 'active',
})
})
// Embedding model row should mirror the selector status labels.
describe('Embedding Model Status', () => {
it('should render active embedding model label when the model is available', () => {
render(<Node id="knowledge-base-1" data={createNodeData()} />)
expect(screen.getByText('Text Embedding 3 Large')).toBeInTheDocument()
})
it('should render configure required when embedding model status requires configuration', () => {
mockUseEmbeddingModelStatus.mockReturnValue({
providerMeta: undefined,
modelProvider: undefined,
currentModel: createModelItem({ status: ModelStatusEnum.noConfigure }),
status: 'configure-required',
})
render(<Node id="knowledge-base-1" data={createNodeData()} />)
expect(screen.getByText('common.modelProvider.selector.configureRequired')).toBeInTheDocument()
})
it('should render disabled when embedding model status is disabled', () => {
mockUseEmbeddingModelStatus.mockReturnValue({
providerMeta: undefined,
modelProvider: undefined,
currentModel: createModelItem({ status: ModelStatusEnum.disabled }),
status: 'disabled',
})
render(<Node id="knowledge-base-1" data={createNodeData()} />)
expect(screen.getByText('common.modelProvider.selector.disabled')).toBeInTheDocument()
})
it('should render incompatible when embedding model status is incompatible', () => {
mockUseEmbeddingModelStatus.mockReturnValue({
providerMeta: undefined,
modelProvider: undefined,
currentModel: undefined,
status: 'incompatible',
})
render(<Node id="knowledge-base-1" data={createNodeData()} />)
expect(screen.getByText('common.modelProvider.selector.incompatible')).toBeInTheDocument()
})
it('should render configure model prompt when no embedding model is selected', () => {
mockUseEmbeddingModelStatus.mockReturnValue({
providerMeta: undefined,
modelProvider: undefined,
currentModel: undefined,
status: 'empty',
})
render(
<Node
id="knowledge-base-1"
data={createNodeData({
embedding_model: undefined,
embedding_model_provider: undefined,
})}
/>,
)
expect(screen.getByText('plugin.detailPanel.configureModel')).toBeInTheDocument()
})
})
describe('Validation warnings', () => {
it('should render a warning banner when chunk structure is missing', () => {
render(
<Node
id="knowledge-base-1"
data={createNodeData({
chunk_structure: undefined,
})}
/>,
)
expect(screen.getByText(/chunkIsRequired/i)).toBeInTheDocument()
})
it('should render a warning value for the chunks input row when no chunk variable is selected', () => {
render(
<Node
id="knowledge-base-1"
data={createNodeData({
index_chunk_variable_selector: [],
})}
/>,
)
expect(screen.getByText(/chunksVariableIsRequired/i)).toBeInTheDocument()
})
it('should render a warning value for retrieval settings when reranking is incomplete', () => {
mockUseModelList.mockImplementation((modelType: ModelTypeEnum) => {
if (modelType === ModelTypeEnum.textEmbedding) {
return {
data: [{
provider: 'openai',
models: [createModelItem()],
}],
}
}
return { data: [] }
})
render(
<Node
id="knowledge-base-1"
data={createNodeData({
retrieval_model: {
top_k: 3,
score_threshold_enabled: false,
score_threshold: 0.5,
search_method: RetrievalSearchMethodEnum.semantic,
reranking_enable: true,
},
})}
/>,
)
expect(screen.getByText(/rerankingModelIsRequired/i)).toBeInTheDocument()
})
it('should hide the embedding model row when the index method is not qualified', () => {
render(
<Node
id="knowledge-base-1"
data={createNodeData({
indexing_technique: IndexMethodEnum.ECONOMICAL,
})}
/>,
)
expect(screen.queryByText('Text Embedding 3 Large')).not.toBeInTheDocument()
})
})
})