mirror of
https://github.com/langgenius/dify.git
synced 2026-05-03 17:08:03 +08:00
Merge branch 'feat/model-plugins-implementing' into deploy/dev
This commit is contained in:
@ -0,0 +1,77 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { ChunkStructureEnum } from '../../types'
|
||||
import ChunkStructure from './index'
|
||||
|
||||
const mockUseChunkStructure = vi.hoisted(() => vi.fn())
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/layout', () => ({
|
||||
Field: ({ children, fieldTitleProps }: { children: ReactNode, fieldTitleProps: { title: string, warningDot?: boolean, operation?: ReactNode } }) => (
|
||||
<div data-testid="field" data-warning-dot={String(!!fieldTitleProps.warningDot)}>
|
||||
<div>{fieldTitleProps.title}</div>
|
||||
{fieldTitleProps.operation}
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('./hooks', () => ({
|
||||
useChunkStructure: mockUseChunkStructure,
|
||||
}))
|
||||
|
||||
vi.mock('../option-card', () => ({
|
||||
default: ({ title }: { title: string }) => <div data-testid="option-card">{title}</div>,
|
||||
}))
|
||||
|
||||
vi.mock('./selector', () => ({
|
||||
default: ({ trigger, value }: { trigger?: ReactNode, value?: string }) => (
|
||||
<div data-testid="selector">
|
||||
{value ?? 'no-value'}
|
||||
{trigger}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('./instruction', () => ({
|
||||
default: ({ className }: { className?: string }) => <div data-testid="instruction" className={className}>Instruction</div>,
|
||||
}))
|
||||
|
||||
describe('ChunkStructure', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseChunkStructure.mockReturnValue({
|
||||
options: [{ value: ChunkStructureEnum.general, label: 'General' }],
|
||||
optionMap: {
|
||||
[ChunkStructureEnum.general]: {
|
||||
title: 'General Chunk Structure',
|
||||
},
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should render the selected option and warning dot metadata when a chunk structure is chosen', () => {
|
||||
render(
|
||||
<ChunkStructure
|
||||
chunkStructure={ChunkStructureEnum.general}
|
||||
warningDot
|
||||
onChunkStructureChange={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByTestId('field')).toHaveAttribute('data-warning-dot', 'true')
|
||||
expect(screen.getByTestId('selector')).toHaveTextContent(ChunkStructureEnum.general)
|
||||
expect(screen.getByTestId('option-card')).toHaveTextContent('General Chunk Structure')
|
||||
expect(screen.queryByTestId('instruction')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should render the add trigger and instruction when no chunk structure is selected', () => {
|
||||
render(
|
||||
<ChunkStructure
|
||||
onChunkStructureChange={vi.fn()}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.getByRole('button', { name: /chooseChunkStructure/i })).toBeInTheDocument()
|
||||
expect(screen.getByTestId('instruction')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,62 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { render } from '@testing-library/react'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import EmbeddingModel from './embedding-model'
|
||||
|
||||
const mockUseModelList = vi.hoisted(() => vi.fn())
|
||||
const mockModelSelector = vi.hoisted(() => vi.fn(() => <div data-testid="model-selector">selector</div>))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/layout', () => ({
|
||||
Field: ({ children, fieldTitleProps }: { children: ReactNode, fieldTitleProps: { warningDot?: boolean } }) => (
|
||||
<div data-testid="field" data-warning-dot={String(!!fieldTitleProps.warningDot)}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
||||
useModelList: mockUseModelList,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/model-selector', () => ({
|
||||
default: mockModelSelector,
|
||||
}))
|
||||
|
||||
describe('EmbeddingModel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseModelList.mockReturnValue({ data: [{ provider: 'openai', model: 'text-embedding-3-large' }] })
|
||||
})
|
||||
|
||||
it('should pass the selected model configuration and warning state to the selector field', () => {
|
||||
const onEmbeddingModelChange = vi.fn()
|
||||
|
||||
render(
|
||||
<EmbeddingModel
|
||||
embeddingModel="text-embedding-3-large"
|
||||
embeddingModelProvider="openai"
|
||||
warningDot
|
||||
onEmbeddingModelChange={onEmbeddingModelChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(mockUseModelList).toHaveBeenCalledWith(ModelTypeEnum.textEmbedding)
|
||||
expect(mockModelSelector).toHaveBeenCalledWith(expect.objectContaining({
|
||||
defaultModel: {
|
||||
provider: 'openai',
|
||||
model: 'text-embedding-3-large',
|
||||
},
|
||||
modelList: [{ provider: 'openai', model: 'text-embedding-3-large' }],
|
||||
readonly: false,
|
||||
showDeprecatedWarnIcon: true,
|
||||
}), undefined)
|
||||
})
|
||||
|
||||
it('should pass an undefined default model when the embedding model is incomplete', () => {
|
||||
render(<EmbeddingModel embeddingModel="text-embedding-3-large" />)
|
||||
|
||||
expect(mockModelSelector).toHaveBeenCalledWith(expect.objectContaining({
|
||||
defaultModel: undefined,
|
||||
}), undefined)
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,74 @@
|
||||
import type { KnowledgeBaseNodeType } from './types'
|
||||
import type { Model, ModelItem } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import {
|
||||
ConfigurationMethodEnum,
|
||||
ModelStatusEnum,
|
||||
ModelTypeEnum,
|
||||
} from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import nodeDefault from './default'
|
||||
import { ChunkStructureEnum, IndexMethodEnum, RetrievalSearchMethodEnum } from './types'
|
||||
|
||||
const t = (key: string) => key
|
||||
|
||||
const makeEmbeddingModelList = (status: ModelStatusEnum): Model[] => [{
|
||||
provider: 'openai',
|
||||
icon_small: { en_US: '', zh_Hans: '' },
|
||||
label: { en_US: 'OpenAI', zh_Hans: 'OpenAI' },
|
||||
models: [{
|
||||
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,
|
||||
model_properties: {},
|
||||
load_balancing_enabled: false,
|
||||
}],
|
||||
status,
|
||||
}]
|
||||
|
||||
const makeEmbeddingProviderModelList = (status: ModelStatusEnum): 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,
|
||||
model_properties: {},
|
||||
load_balancing_enabled: false,
|
||||
}]
|
||||
|
||||
const createPayload = (overrides: Partial<KnowledgeBaseNodeType> = {}): KnowledgeBaseNodeType => ({
|
||||
...nodeDefault.defaultValue,
|
||||
index_chunk_variable_selector: ['chunks', 'results'],
|
||||
chunk_structure: ChunkStructureEnum.general,
|
||||
indexing_technique: IndexMethodEnum.QUALIFIED,
|
||||
embedding_model: 'text-embedding-3-large',
|
||||
embedding_model_provider: 'openai',
|
||||
retrieval_model: {
|
||||
...nodeDefault.defaultValue.retrieval_model,
|
||||
search_method: RetrievalSearchMethodEnum.semantic,
|
||||
},
|
||||
_embeddingModelList: makeEmbeddingModelList(ModelStatusEnum.active),
|
||||
_embeddingProviderModelList: makeEmbeddingProviderModelList(ModelStatusEnum.active),
|
||||
_rerankModelList: [],
|
||||
...overrides,
|
||||
}) as KnowledgeBaseNodeType
|
||||
|
||||
describe('knowledge-base default node validation', () => {
|
||||
it('should return an invalid result when the payload has a validation issue', () => {
|
||||
const result = nodeDefault.checkValid(createPayload({ chunk_structure: undefined }), t)
|
||||
|
||||
expect(result).toEqual({
|
||||
isValid: false,
|
||||
errorMessage: 'nodes.knowledgeBase.chunkIsRequired',
|
||||
})
|
||||
})
|
||||
|
||||
it('should return a valid result when the payload is complete', () => {
|
||||
const result = nodeDefault.checkValid(createPayload(), t)
|
||||
|
||||
expect(result).toEqual({
|
||||
isValid: true,
|
||||
errorMessage: '',
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -158,4 +158,76 @@ describe('KnowledgeBaseNode', () => {
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
198
web/app/components/workflow/nodes/knowledge-base/panel.spec.tsx
Normal file
198
web/app/components/workflow/nodes/knowledge-base/panel.spec.tsx
Normal file
@ -0,0 +1,198 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import type { PanelProps } from '@/types/workflow'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
|
||||
import Panel from './panel'
|
||||
import { ChunkStructureEnum, IndexMethodEnum, RetrievalSearchMethodEnum } from './types'
|
||||
|
||||
const mockUseModelList = vi.hoisted(() => vi.fn())
|
||||
const mockUseQuery = vi.hoisted(() => vi.fn())
|
||||
const mockUseEmbeddingModelStatus = vi.hoisted(() => vi.fn())
|
||||
const mockChunkStructure = vi.hoisted(() => vi.fn(() => <div data-testid="chunk-structure" />))
|
||||
const mockEmbeddingModel = vi.hoisted(() => vi.fn(() => <div data-testid="embedding-model" />))
|
||||
const mockSummaryIndexSetting = vi.hoisted(() => vi.fn(() => <div data-testid="summary-index-setting" />))
|
||||
const mockQueryOptions = vi.hoisted(() => vi.fn((options: unknown) => options))
|
||||
|
||||
vi.mock('@tanstack/react-query', () => ({
|
||||
useQuery: mockUseQuery,
|
||||
}))
|
||||
|
||||
vi.mock('@/service/client', () => ({
|
||||
consoleQuery: {
|
||||
modelProviders: {
|
||||
models: {
|
||||
queryOptions: mockQueryOptions,
|
||||
},
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/header/account-setting/model-provider-page/hooks', () => ({
|
||||
useModelList: mockUseModelList,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/hooks', () => ({
|
||||
useNodesReadOnly: () => ({ nodesReadOnly: false }),
|
||||
}))
|
||||
|
||||
vi.mock('./hooks/use-config', () => ({
|
||||
useConfig: () => ({
|
||||
handleChunkStructureChange: vi.fn(),
|
||||
handleIndexMethodChange: vi.fn(),
|
||||
handleKeywordNumberChange: vi.fn(),
|
||||
handleEmbeddingModelChange: vi.fn(),
|
||||
handleRetrievalSearchMethodChange: vi.fn(),
|
||||
handleHybridSearchModeChange: vi.fn(),
|
||||
handleRerankingModelEnabledChange: vi.fn(),
|
||||
handleWeighedScoreChange: vi.fn(),
|
||||
handleRerankingModelChange: vi.fn(),
|
||||
handleTopKChange: vi.fn(),
|
||||
handleScoreThresholdChange: vi.fn(),
|
||||
handleScoreThresholdEnabledChange: vi.fn(),
|
||||
handleInputVariableChange: vi.fn(),
|
||||
handleSummaryIndexSettingChange: vi.fn(),
|
||||
}),
|
||||
}))
|
||||
|
||||
vi.mock('./hooks/use-embedding-model-status', () => ({
|
||||
useEmbeddingModelStatus: mockUseEmbeddingModelStatus,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/settings/utils', () => ({
|
||||
checkShowMultiModalTip: () => false,
|
||||
}))
|
||||
|
||||
vi.mock('@/config', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/config')>()
|
||||
return {
|
||||
...actual,
|
||||
IS_CE_EDITION: true,
|
||||
}
|
||||
})
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/layout', () => ({
|
||||
Group: ({ children }: { children: ReactNode }) => <div data-testid="group">{children}</div>,
|
||||
BoxGroup: ({ children }: { children: ReactNode }) => <div data-testid="box-group">{children}</div>,
|
||||
BoxGroupField: ({ children, fieldProps }: { children: ReactNode, fieldProps: { fieldTitleProps: { warningDot?: boolean } } }) => (
|
||||
<div data-testid="box-group-field" data-warning-dot={String(!!fieldProps.fieldTitleProps.warningDot)}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/variable/var-reference-picker', () => ({
|
||||
default: () => <div data-testid="var-reference-picker" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/workflow/nodes/_base/components/split', () => ({
|
||||
default: () => <div data-testid="split" />,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/datasets/settings/summary-index-setting', () => ({
|
||||
default: mockSummaryIndexSetting,
|
||||
}))
|
||||
|
||||
vi.mock('./components/chunk-structure', () => ({
|
||||
default: mockChunkStructure,
|
||||
}))
|
||||
|
||||
vi.mock('./components/index-method', () => ({
|
||||
default: () => <div data-testid="index-method" />,
|
||||
}))
|
||||
|
||||
vi.mock('./components/embedding-model', () => ({
|
||||
default: mockEmbeddingModel,
|
||||
}))
|
||||
|
||||
vi.mock('./components/retrieval-setting', () => ({
|
||||
default: () => <div data-testid="retrieval-setting" />,
|
||||
}))
|
||||
|
||||
const createData = (overrides: Record<string, unknown> = {}) => ({
|
||||
index_chunk_variable_selector: ['chunks', 'results'],
|
||||
chunk_structure: ChunkStructureEnum.general,
|
||||
indexing_technique: IndexMethodEnum.QUALIFIED,
|
||||
embedding_model: 'text-embedding-3-large',
|
||||
embedding_model_provider: 'openai',
|
||||
keyword_number: 10,
|
||||
retrieval_model: {
|
||||
search_method: RetrievalSearchMethodEnum.semantic,
|
||||
reranking_enable: false,
|
||||
top_k: 3,
|
||||
score_threshold_enabled: false,
|
||||
score_threshold: 0.5,
|
||||
},
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const panelProps: PanelProps = {
|
||||
getInputVars: () => [],
|
||||
toVarInputs: () => [],
|
||||
runInputData: {},
|
||||
runInputDataRef: { current: {} },
|
||||
setRunInputData: vi.fn(),
|
||||
runResult: undefined,
|
||||
}
|
||||
|
||||
describe('KnowledgeBasePanel', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
mockUseQuery.mockReturnValue({ data: undefined })
|
||||
mockUseModelList.mockImplementation((modelType: ModelTypeEnum) => {
|
||||
if (modelType === ModelTypeEnum.textEmbedding) {
|
||||
return {
|
||||
data: [{
|
||||
provider: 'openai',
|
||||
models: [{ model: 'text-embedding-3-large' }],
|
||||
}],
|
||||
}
|
||||
}
|
||||
return { data: [] }
|
||||
})
|
||||
mockUseEmbeddingModelStatus.mockReturnValue({ status: 'active' })
|
||||
})
|
||||
|
||||
it('should show a warning dot on chunk structure and skip nested sections when chunk structure is missing', () => {
|
||||
render(<Panel id="knowledge-base-1" data={createData({ chunk_structure: undefined }) as never} panelProps={panelProps} />)
|
||||
|
||||
expect(mockChunkStructure).toHaveBeenCalledWith(expect.objectContaining({
|
||||
warningDot: true,
|
||||
}), undefined)
|
||||
expect(screen.queryByTestId('box-group-field')).not.toBeInTheDocument()
|
||||
expect(mockQueryOptions).toHaveBeenCalledWith(expect.objectContaining({
|
||||
enabled: true,
|
||||
}))
|
||||
})
|
||||
|
||||
it('should pass warning dots and render summary settings when the qualified configuration needs attention', () => {
|
||||
mockUseEmbeddingModelStatus.mockReturnValue({ status: 'disabled' })
|
||||
|
||||
render(<Panel id="knowledge-base-1" data={createData({ index_chunk_variable_selector: [] }) as never} panelProps={panelProps} />)
|
||||
|
||||
expect(screen.getByTestId('box-group-field')).toHaveAttribute('data-warning-dot', 'true')
|
||||
expect(mockEmbeddingModel).toHaveBeenCalledWith(expect.objectContaining({
|
||||
warningDot: true,
|
||||
}), undefined)
|
||||
expect(mockQueryOptions).toHaveBeenCalledWith(expect.objectContaining({
|
||||
input: { params: { provider: 'openai' } },
|
||||
enabled: true,
|
||||
}))
|
||||
expect(screen.getByTestId('summary-index-setting')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should hide embedding and summary settings for non-qualified index methods', () => {
|
||||
render(
|
||||
<Panel
|
||||
id="knowledge-base-1"
|
||||
data={createData({ indexing_technique: IndexMethodEnum.ECONOMICAL }) as never}
|
||||
panelProps={panelProps}
|
||||
/>,
|
||||
)
|
||||
|
||||
expect(screen.queryByTestId('embedding-model')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('summary-index-setting')).not.toBeInTheDocument()
|
||||
expect(mockQueryOptions).toHaveBeenCalledWith(expect.objectContaining({
|
||||
enabled: false,
|
||||
}))
|
||||
})
|
||||
})
|
||||
@ -12,6 +12,9 @@ import {
|
||||
} from './types'
|
||||
import {
|
||||
getKnowledgeBaseValidationIssue,
|
||||
getKnowledgeBaseValidationMessage,
|
||||
isHighQualitySearchMethod,
|
||||
isKnowledgeBaseEmbeddingIssue,
|
||||
KnowledgeBaseValidationIssueCode,
|
||||
} from './utils'
|
||||
|
||||
@ -69,6 +72,13 @@ const makePayload = (overrides: Partial<KnowledgeBaseNodeType> = {}): KnowledgeB
|
||||
}
|
||||
|
||||
describe('knowledge-base validation issue', () => {
|
||||
it('identifies high quality retrieval methods', () => {
|
||||
expect(isHighQualitySearchMethod(RetrievalSearchMethodEnum.semantic)).toBe(true)
|
||||
expect(isHighQualitySearchMethod(RetrievalSearchMethodEnum.hybrid)).toBe(true)
|
||||
expect(isHighQualitySearchMethod(RetrievalSearchMethodEnum.fullText)).toBe(true)
|
||||
expect(isHighQualitySearchMethod('unknown-method' as RetrievalSearchMethodEnum)).toBe(false)
|
||||
})
|
||||
|
||||
it('returns chunk structure issue when chunk structure is missing', () => {
|
||||
const issue = getKnowledgeBaseValidationIssue(makePayload({ chunk_structure: undefined }))
|
||||
expect(issue?.code).toBe(KnowledgeBaseValidationIssueCode.chunkStructureRequired)
|
||||
@ -123,4 +133,94 @@ describe('knowledge-base validation issue', () => {
|
||||
)
|
||||
expect(issue).toBeNull()
|
||||
})
|
||||
|
||||
it('returns embedding-model-not-configured when the qualified index is missing provider details', () => {
|
||||
const issue = getKnowledgeBaseValidationIssue(
|
||||
makePayload({ embedding_model: undefined }),
|
||||
)
|
||||
|
||||
expect(issue?.code).toBe(KnowledgeBaseValidationIssueCode.embeddingModelNotConfigured)
|
||||
})
|
||||
|
||||
it('maps no-permission embedding models to incompatible', () => {
|
||||
const issue = getKnowledgeBaseValidationIssue(
|
||||
makePayload({ _embeddingProviderModelList: makeEmbeddingProviderModelList(ModelStatusEnum.noPermission) }),
|
||||
)
|
||||
|
||||
expect(issue?.code).toBe(KnowledgeBaseValidationIssueCode.embeddingModelIncompatible)
|
||||
})
|
||||
|
||||
it('returns retrieval-setting-required when retrieval search method is missing', () => {
|
||||
const issue = getKnowledgeBaseValidationIssue(
|
||||
makePayload({ retrieval_model: undefined as never }),
|
||||
)
|
||||
|
||||
expect(issue?.code).toBe(KnowledgeBaseValidationIssueCode.retrievalSettingRequired)
|
||||
})
|
||||
|
||||
it('returns reranking-model-required when reranking is enabled without a model', () => {
|
||||
const issue = getKnowledgeBaseValidationIssue(
|
||||
makePayload({
|
||||
retrieval_model: {
|
||||
...makePayload().retrieval_model,
|
||||
reranking_enable: true,
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expect(issue?.code).toBe(KnowledgeBaseValidationIssueCode.rerankingModelRequired)
|
||||
})
|
||||
|
||||
it('returns reranking-model-invalid when the configured reranking model is unavailable', () => {
|
||||
const issue = getKnowledgeBaseValidationIssue(
|
||||
makePayload({
|
||||
retrieval_model: {
|
||||
...makePayload().retrieval_model,
|
||||
reranking_enable: true,
|
||||
reranking_model: {
|
||||
reranking_provider_name: 'missing-provider',
|
||||
reranking_model_name: 'missing-model',
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
|
||||
expect(issue?.code).toBe(KnowledgeBaseValidationIssueCode.rerankingModelInvalid)
|
||||
})
|
||||
})
|
||||
|
||||
describe('knowledge-base validation messaging', () => {
|
||||
const t = (key: string) => key
|
||||
|
||||
it.each([
|
||||
[KnowledgeBaseValidationIssueCode.chunkStructureRequired, 'nodes.knowledgeBase.chunkIsRequired'],
|
||||
[KnowledgeBaseValidationIssueCode.chunksVariableRequired, 'nodes.knowledgeBase.chunksVariableIsRequired'],
|
||||
[KnowledgeBaseValidationIssueCode.indexMethodRequired, 'nodes.knowledgeBase.indexMethodIsRequired'],
|
||||
[KnowledgeBaseValidationIssueCode.embeddingModelNotConfigured, 'nodes.knowledgeBase.embeddingModelNotConfigured'],
|
||||
[KnowledgeBaseValidationIssueCode.embeddingModelConfigureRequired, 'modelProvider.selector.configureRequired'],
|
||||
[KnowledgeBaseValidationIssueCode.embeddingModelApiKeyUnavailable, 'modelProvider.selector.apiKeyUnavailable'],
|
||||
[KnowledgeBaseValidationIssueCode.embeddingModelCreditsExhausted, 'modelProvider.selector.creditsExhausted'],
|
||||
[KnowledgeBaseValidationIssueCode.embeddingModelDisabled, 'modelProvider.selector.disabled'],
|
||||
[KnowledgeBaseValidationIssueCode.embeddingModelIncompatible, 'modelProvider.selector.incompatible'],
|
||||
[KnowledgeBaseValidationIssueCode.retrievalSettingRequired, 'nodes.knowledgeBase.retrievalSettingIsRequired'],
|
||||
[KnowledgeBaseValidationIssueCode.rerankingModelRequired, 'nodes.knowledgeBase.rerankingModelIsRequired'],
|
||||
[KnowledgeBaseValidationIssueCode.rerankingModelInvalid, 'nodes.knowledgeBase.rerankingModelIsInvalid'],
|
||||
] as const)('maps %s to the expected translation key', (code, expectedKey) => {
|
||||
expect(getKnowledgeBaseValidationMessage({ code }, t as never)).toBe(expectedKey)
|
||||
})
|
||||
|
||||
it('returns an empty string when there is no issue', () => {
|
||||
expect(getKnowledgeBaseValidationMessage(undefined, t as never)).toBe('')
|
||||
})
|
||||
})
|
||||
|
||||
describe('isKnowledgeBaseEmbeddingIssue', () => {
|
||||
it('returns true for embedding-related issues', () => {
|
||||
expect(isKnowledgeBaseEmbeddingIssue({ code: KnowledgeBaseValidationIssueCode.embeddingModelDisabled })).toBe(true)
|
||||
})
|
||||
|
||||
it('returns false for non-embedding issues and missing values', () => {
|
||||
expect(isKnowledgeBaseEmbeddingIssue({ code: KnowledgeBaseValidationIssueCode.rerankingModelInvalid })).toBe(false)
|
||||
expect(isKnowledgeBaseEmbeddingIssue(undefined)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user