test: add integration tests for chunk preview formatting and input field editor flow

This commit is contained in:
CodingOnStar
2026-02-11 14:50:32 +08:00
parent e9db50f781
commit faefb98746
14 changed files with 2350 additions and 76 deletions

View File

@ -0,0 +1,210 @@
/**
* Integration test: Chunk preview formatting pipeline
*
* Tests the formatPreviewChunks utility across all chunking modes
* (text, parentChild, QA) with real data structures.
*/
import { describe, expect, it, vi } from 'vitest'
vi.mock('@/config', () => ({
RAG_PIPELINE_PREVIEW_CHUNK_NUM: 3,
}))
vi.mock('@/models/datasets', () => ({
ChunkingMode: {
text: 'text',
parentChild: 'parent-child',
qa: 'qa',
},
}))
const { formatPreviewChunks } = await import(
'@/app/components/rag-pipeline/components/panel/test-run/result/result-preview/utils',
)
describe('Chunk Preview Formatting', () => {
describe('general text chunks', () => {
it('should format text chunks correctly', () => {
const outputs = {
chunk_structure: 'text',
preview: [
{ content: 'Chunk 1 content', summary: 'Summary 1' },
{ content: 'Chunk 2 content' },
],
}
const result = formatPreviewChunks(outputs)
expect(Array.isArray(result)).toBe(true)
const chunks = result as Array<{ content: string, summary?: string }>
expect(chunks).toHaveLength(2)
expect(chunks[0].content).toBe('Chunk 1 content')
expect(chunks[0].summary).toBe('Summary 1')
expect(chunks[1].content).toBe('Chunk 2 content')
})
it('should limit chunks to RAG_PIPELINE_PREVIEW_CHUNK_NUM', () => {
const outputs = {
chunk_structure: 'text',
preview: Array.from({ length: 10 }, (_, i) => ({
content: `Chunk ${i + 1}`,
})),
}
const result = formatPreviewChunks(outputs)
const chunks = result as Array<{ content: string }>
expect(chunks).toHaveLength(3) // Mocked limit
})
})
describe('parent-child chunks — paragraph mode', () => {
it('should format paragraph parent-child chunks', () => {
const outputs = {
chunk_structure: 'parent-child',
parent_mode: 'paragraph',
preview: [
{
content: 'Parent paragraph',
child_chunks: ['Child 1', 'Child 2'],
summary: 'Parent summary',
},
],
}
const result = formatPreviewChunks(outputs) as {
parent_child_chunks: Array<{
parent_content: string
parent_summary?: string
child_contents: string[]
parent_mode: string
}>
parent_mode: string
}
expect(result.parent_mode).toBe('paragraph')
expect(result.parent_child_chunks).toHaveLength(1)
expect(result.parent_child_chunks[0].parent_content).toBe('Parent paragraph')
expect(result.parent_child_chunks[0].parent_summary).toBe('Parent summary')
expect(result.parent_child_chunks[0].child_contents).toEqual(['Child 1', 'Child 2'])
})
it('should limit parent chunks in paragraph mode', () => {
const outputs = {
chunk_structure: 'parent-child',
parent_mode: 'paragraph',
preview: Array.from({ length: 10 }, (_, i) => ({
content: `Parent ${i + 1}`,
child_chunks: [`Child of ${i + 1}`],
})),
}
const result = formatPreviewChunks(outputs) as {
parent_child_chunks: unknown[]
}
expect(result.parent_child_chunks).toHaveLength(3) // Mocked limit
})
})
describe('parent-child chunks — full-doc mode', () => {
it('should format full-doc parent-child chunks', () => {
const outputs = {
chunk_structure: 'parent-child',
parent_mode: 'full-doc',
preview: [
{
content: 'Full document content',
child_chunks: ['Section 1', 'Section 2', 'Section 3'],
},
],
}
const result = formatPreviewChunks(outputs) as {
parent_child_chunks: Array<{
parent_content: string
child_contents: string[]
parent_mode: string
}>
}
expect(result.parent_child_chunks).toHaveLength(1)
expect(result.parent_child_chunks[0].parent_content).toBe('Full document content')
expect(result.parent_child_chunks[0].parent_mode).toBe('full-doc')
})
it('should limit child chunks in full-doc mode', () => {
const outputs = {
chunk_structure: 'parent-child',
parent_mode: 'full-doc',
preview: [
{
content: 'Document',
child_chunks: Array.from({ length: 20 }, (_, i) => `Section ${i + 1}`),
},
],
}
const result = formatPreviewChunks(outputs) as {
parent_child_chunks: Array<{ child_contents: string[] }>
}
expect(result.parent_child_chunks[0].child_contents).toHaveLength(3) // Mocked limit
})
})
describe('QA chunks', () => {
it('should format QA chunks correctly', () => {
const outputs = {
chunk_structure: 'qa',
qa_preview: [
{ question: 'What is AI?', answer: 'Artificial Intelligence is...' },
{ question: 'What is ML?', answer: 'Machine Learning is...' },
],
}
const result = formatPreviewChunks(outputs) as {
qa_chunks: Array<{ question: string, answer: string }>
}
expect(result.qa_chunks).toHaveLength(2)
expect(result.qa_chunks[0].question).toBe('What is AI?')
expect(result.qa_chunks[0].answer).toBe('Artificial Intelligence is...')
})
it('should limit QA chunks', () => {
const outputs = {
chunk_structure: 'qa',
qa_preview: Array.from({ length: 10 }, (_, i) => ({
question: `Q${i + 1}`,
answer: `A${i + 1}`,
})),
}
const result = formatPreviewChunks(outputs) as {
qa_chunks: unknown[]
}
expect(result.qa_chunks).toHaveLength(3) // Mocked limit
})
})
describe('edge cases', () => {
it('should return undefined for null outputs', () => {
expect(formatPreviewChunks(null)).toBeUndefined()
})
it('should return undefined for undefined outputs', () => {
expect(formatPreviewChunks(undefined)).toBeUndefined()
})
it('should return undefined for unknown chunk_structure', () => {
const outputs = {
chunk_structure: 'unknown-type',
preview: [],
}
expect(formatPreviewChunks(outputs)).toBeUndefined()
})
})
})

View File

@ -0,0 +1,199 @@
/**
* Integration test: Input field editor data conversion flow
*
* Tests the full pipeline: InputVar -> FormData -> InputVar roundtrip
* and schema validation for various input types.
*/
import type { InputVar } from '@/models/pipeline'
import { describe, expect, it, vi } from 'vitest'
import { PipelineInputVarType } from '@/models/pipeline'
// Mock the config module for VAR_ITEM_TEMPLATE_IN_PIPELINE
vi.mock('@/config', () => ({
VAR_ITEM_TEMPLATE_IN_PIPELINE: {
type: 'text-input',
label: '',
variable: '',
max_length: 48,
required: false,
options: [],
allowed_file_upload_methods: [],
allowed_file_types: [],
allowed_file_extensions: [],
},
MAX_VAR_KEY_LENGTH: 30,
RAG_PIPELINE_PREVIEW_CHUNK_NUM: 10,
}))
// Import real functions (not mocked)
const { convertToInputFieldFormData, convertFormDataToINputField } = await import(
'@/app/components/rag-pipeline/components/panel/input-field/editor/utils',
)
describe('Input Field Editor Data Flow', () => {
describe('convertToInputFieldFormData', () => {
it('should convert a text input InputVar to FormData', () => {
const inputVar: InputVar = {
type: 'text-input',
label: 'Name',
variable: 'user_name',
max_length: 100,
required: true,
default_value: 'John',
tooltips: 'Enter your name',
placeholder: 'Type here...',
options: [],
} as InputVar
const formData = convertToInputFieldFormData(inputVar)
expect(formData.type).toBe('text-input')
expect(formData.label).toBe('Name')
expect(formData.variable).toBe('user_name')
expect(formData.maxLength).toBe(100)
expect(formData.required).toBe(true)
expect(formData.default).toBe('John')
expect(formData.tooltips).toBe('Enter your name')
expect(formData.placeholder).toBe('Type here...')
})
it('should handle file input with upload settings', () => {
const inputVar: InputVar = {
type: 'file',
label: 'Document',
variable: 'doc',
required: false,
allowed_file_upload_methods: ['local_file', 'remote_url'],
allowed_file_types: ['document', 'image'],
allowed_file_extensions: ['.pdf', '.jpg'],
options: [],
} as InputVar
const formData = convertToInputFieldFormData(inputVar)
expect(formData.allowedFileUploadMethods).toEqual(['local_file', 'remote_url'])
expect(formData.allowedTypesAndExtensions).toEqual({
allowedFileTypes: ['document', 'image'],
allowedFileExtensions: ['.pdf', '.jpg'],
})
})
it('should use template defaults when no data provided', () => {
const formData = convertToInputFieldFormData(undefined)
expect(formData.type).toBe('text-input')
expect(formData.maxLength).toBe(48)
expect(formData.required).toBe(false)
})
it('should omit undefined/null optional fields', () => {
const inputVar: InputVar = {
type: 'text-input',
label: 'Simple',
variable: 'simple_var',
max_length: 50,
required: false,
options: [],
} as InputVar
const formData = convertToInputFieldFormData(inputVar)
expect(formData.default).toBeUndefined()
expect(formData.tooltips).toBeUndefined()
expect(formData.placeholder).toBeUndefined()
expect(formData.unit).toBeUndefined()
})
})
describe('convertFormDataToINputField', () => {
it('should convert FormData back to InputVar', () => {
const formData = {
type: PipelineInputVarType.textInput,
label: 'Name',
variable: 'user_name',
maxLength: 100,
required: true,
default: 'John',
tooltips: 'Enter your name',
options: [],
placeholder: 'Type here...',
allowedTypesAndExtensions: {
allowedFileTypes: undefined,
allowedFileExtensions: undefined,
},
}
const inputVar = convertFormDataToINputField(formData)
expect(inputVar.type).toBe('text-input')
expect(inputVar.label).toBe('Name')
expect(inputVar.variable).toBe('user_name')
expect(inputVar.max_length).toBe(100)
expect(inputVar.required).toBe(true)
expect(inputVar.default_value).toBe('John')
expect(inputVar.tooltips).toBe('Enter your name')
})
})
describe('roundtrip conversion', () => {
it('should preserve text input data through roundtrip', () => {
const original: InputVar = {
type: 'text-input',
label: 'Question',
variable: 'question',
max_length: 200,
required: true,
default_value: 'What is AI?',
tooltips: 'Enter your question',
placeholder: 'Ask something...',
options: [],
} as InputVar
const formData = convertToInputFieldFormData(original)
const restored = convertFormDataToINputField(formData)
expect(restored.type).toBe(original.type)
expect(restored.label).toBe(original.label)
expect(restored.variable).toBe(original.variable)
expect(restored.max_length).toBe(original.max_length)
expect(restored.required).toBe(original.required)
expect(restored.default_value).toBe(original.default_value)
expect(restored.tooltips).toBe(original.tooltips)
expect(restored.placeholder).toBe(original.placeholder)
})
it('should preserve number input data through roundtrip', () => {
const original = {
type: 'number',
label: 'Temperature',
variable: 'temp',
required: false,
default_value: '0.7',
unit: '°C',
options: [],
} as InputVar
const formData = convertToInputFieldFormData(original)
const restored = convertFormDataToINputField(formData)
expect(restored.type).toBe('number')
expect(restored.unit).toBe('°C')
expect(restored.default_value).toBe(0.7)
})
it('should preserve select options through roundtrip', () => {
const original: InputVar = {
type: 'select',
label: 'Mode',
variable: 'mode',
required: true,
options: ['fast', 'balanced', 'quality'],
} as InputVar
const formData = convertToInputFieldFormData(original)
const restored = convertFormDataToINputField(formData)
expect(restored.options).toEqual(['fast', 'balanced', 'quality'])
})
})
})

View File

@ -0,0 +1,383 @@
import { renderHook } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { PipelineInputVarType } from '@/models/pipeline'
import { useConfigurations, useHiddenConfigurations, useHiddenFieldNames } from './hooks'
// ============================================================================
// Mocks
// ============================================================================
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
vi.mock('@/app/components/base/file-uploader/hooks', () => ({
useFileSizeLimit: () => ({
imgSizeLimit: 10 * 1024 * 1024,
docSizeLimit: 15 * 1024 * 1024,
audioSizeLimit: 50 * 1024 * 1024,
videoSizeLimit: 100 * 1024 * 1024,
}),
}))
vi.mock('@/service/use-common', () => ({
useFileUploadConfig: () => ({ data: {} }),
}))
vi.mock('@/app/components/workflow/constants', () => ({
DEFAULT_FILE_UPLOAD_SETTING: {
allowed_file_upload_methods: ['local_file', 'remote_url'],
allowed_file_types: ['image', 'document'],
allowed_file_extensions: ['.jpg', '.png', '.pdf'],
max_length: 5,
},
}))
vi.mock('./schema', () => ({
TEXT_MAX_LENGTH: 256,
}))
vi.mock('@/utils/format', () => ({
formatFileSize: (size: number) => `${Math.round(size / 1024 / 1024)}MB`,
}))
// ============================================================================
// Tests
// ============================================================================
describe('useHiddenFieldNames', () => {
afterEach(() => {
vi.clearAllMocks()
})
it('should return field names for textInput type', () => {
const { result } = renderHook(() => useHiddenFieldNames(PipelineInputVarType.textInput))
expect(result.current).toContain('variableconfig.defaultvalue')
expect(result.current).toContain('variableconfig.placeholder')
expect(result.current).toContain('variableconfig.tooltips')
})
it('should return field names for paragraph type', () => {
const { result } = renderHook(() => useHiddenFieldNames(PipelineInputVarType.paragraph))
expect(result.current).toContain('variableconfig.defaultvalue')
expect(result.current).toContain('variableconfig.placeholder')
expect(result.current).toContain('variableconfig.tooltips')
})
it('should return field names for number type including unit', () => {
const { result } = renderHook(() => useHiddenFieldNames(PipelineInputVarType.number))
expect(result.current).toContain('variableconfig.defaultvalue')
expect(result.current).toContain('variableconfig.unit')
expect(result.current).toContain('variableconfig.placeholder')
expect(result.current).toContain('variableconfig.tooltips')
})
it('should return field names for select type', () => {
const { result } = renderHook(() => useHiddenFieldNames(PipelineInputVarType.select))
expect(result.current).toContain('variableconfig.defaultvalue')
expect(result.current).toContain('variableconfig.tooltips')
})
it('should return field names for singleFile type', () => {
const { result } = renderHook(() => useHiddenFieldNames(PipelineInputVarType.singleFile))
expect(result.current).toContain('variableconfig.uploadmethod')
expect(result.current).toContain('variableconfig.tooltips')
})
it('should return field names for multiFiles type including max number', () => {
const { result } = renderHook(() => useHiddenFieldNames(PipelineInputVarType.multiFiles))
expect(result.current).toContain('variableconfig.uploadmethod')
expect(result.current).toContain('variableconfig.maxnumberofuploads')
expect(result.current).toContain('variableconfig.tooltips')
})
it('should return field names for checkbox type', () => {
const { result } = renderHook(() => useHiddenFieldNames(PipelineInputVarType.checkbox))
expect(result.current).toContain('variableconfig.startchecked')
expect(result.current).toContain('variableconfig.tooltips')
})
it('should return only tooltips for unknown type', () => {
const { result } = renderHook(() => useHiddenFieldNames('unknown-type' as PipelineInputVarType))
expect(result.current).toBe('variableconfig.tooltips')
})
it('should return comma-separated lowercase string', () => {
const { result } = renderHook(() => useHiddenFieldNames(PipelineInputVarType.textInput))
// The result should be comma-separated
expect(result.current).toMatch(/,/)
// All lowercase
expect(result.current).toBe(result.current.toLowerCase())
})
})
describe('useConfigurations', () => {
let mockGetFieldValue: ReturnType<typeof vi.fn<(...args: unknown[]) => unknown>>
let mockSetFieldValue: ReturnType<typeof vi.fn<(...args: unknown[]) => void>>
beforeEach(() => {
mockGetFieldValue = vi.fn()
mockSetFieldValue = vi.fn()
vi.clearAllMocks()
})
it('should return array of configurations', () => {
const { result } = renderHook(() =>
useConfigurations({
getFieldValue: mockGetFieldValue,
setFieldValue: mockSetFieldValue,
supportFile: true,
}),
)
expect(Array.isArray(result.current)).toBe(true)
expect(result.current.length).toBeGreaterThan(0)
})
it('should include field type select configuration', () => {
const { result } = renderHook(() =>
useConfigurations({
getFieldValue: mockGetFieldValue,
setFieldValue: mockSetFieldValue,
supportFile: true,
}),
)
const typeConfig = result.current.find(c => c.variable === 'type')
expect(typeConfig).toBeDefined()
expect(typeConfig?.required).toBe(true)
})
it('should include variable name configuration', () => {
const { result } = renderHook(() =>
useConfigurations({
getFieldValue: mockGetFieldValue,
setFieldValue: mockSetFieldValue,
supportFile: true,
}),
)
const varConfig = result.current.find(c => c.variable === 'variable')
expect(varConfig).toBeDefined()
expect(varConfig?.required).toBe(true)
})
it('should include display name configuration', () => {
const { result } = renderHook(() =>
useConfigurations({
getFieldValue: mockGetFieldValue,
setFieldValue: mockSetFieldValue,
supportFile: true,
}),
)
const labelConfig = result.current.find(c => c.variable === 'label')
expect(labelConfig).toBeDefined()
expect(labelConfig?.required).toBe(false)
})
it('should include required checkbox configuration', () => {
const { result } = renderHook(() =>
useConfigurations({
getFieldValue: mockGetFieldValue,
setFieldValue: mockSetFieldValue,
supportFile: true,
}),
)
const requiredConfig = result.current.find(c => c.variable === 'required')
expect(requiredConfig).toBeDefined()
})
it('should set file defaults when type changes to singleFile', () => {
const { result } = renderHook(() =>
useConfigurations({
getFieldValue: mockGetFieldValue,
setFieldValue: mockSetFieldValue,
supportFile: true,
}),
)
const typeConfig = result.current.find(c => c.variable === 'type')
typeConfig?.listeners?.onChange?.({ value: PipelineInputVarType.singleFile, fieldApi: {} as never })
expect(mockSetFieldValue).toHaveBeenCalledWith('allowedFileUploadMethods', ['local_file', 'remote_url'])
expect(mockSetFieldValue).toHaveBeenCalledWith('allowedTypesAndExtensions', {
allowedFileTypes: ['image', 'document'],
allowedFileExtensions: ['.jpg', '.png', '.pdf'],
})
})
it('should set maxLength when type changes to multiFiles', () => {
const { result } = renderHook(() =>
useConfigurations({
getFieldValue: mockGetFieldValue,
setFieldValue: mockSetFieldValue,
supportFile: true,
}),
)
const typeConfig = result.current.find(c => c.variable === 'type')
typeConfig?.listeners?.onChange?.({ value: PipelineInputVarType.multiFiles, fieldApi: {} as never })
expect(mockSetFieldValue).toHaveBeenCalledWith('maxLength', 5)
})
it('should not set file defaults when type changes to text', () => {
const { result } = renderHook(() =>
useConfigurations({
getFieldValue: mockGetFieldValue,
setFieldValue: mockSetFieldValue,
supportFile: true,
}),
)
const typeConfig = result.current.find(c => c.variable === 'type')
typeConfig?.listeners?.onChange?.({ value: PipelineInputVarType.textInput, fieldApi: {} as never })
expect(mockSetFieldValue).not.toHaveBeenCalled()
})
it('should auto-fill label from variable name on blur', () => {
mockGetFieldValue.mockReturnValue('')
const { result } = renderHook(() =>
useConfigurations({
getFieldValue: mockGetFieldValue,
setFieldValue: mockSetFieldValue,
supportFile: true,
}),
)
const varConfig = result.current.find(c => c.variable === 'variable')
varConfig?.listeners?.onBlur?.({ value: 'myVariable', fieldApi: {} as never })
expect(mockSetFieldValue).toHaveBeenCalledWith('label', 'myVariable')
})
it('should not auto-fill label if label already exists', () => {
mockGetFieldValue.mockReturnValue('Existing Label')
const { result } = renderHook(() =>
useConfigurations({
getFieldValue: mockGetFieldValue,
setFieldValue: mockSetFieldValue,
supportFile: true,
}),
)
const varConfig = result.current.find(c => c.variable === 'variable')
varConfig?.listeners?.onBlur?.({ value: 'myVariable', fieldApi: {} as never })
expect(mockSetFieldValue).not.toHaveBeenCalled()
})
it('should reset label to variable name when display name is cleared', () => {
mockGetFieldValue.mockReturnValue('existingVar')
const { result } = renderHook(() =>
useConfigurations({
getFieldValue: mockGetFieldValue,
setFieldValue: mockSetFieldValue,
supportFile: true,
}),
)
const labelConfig = result.current.find(c => c.variable === 'label')
labelConfig?.listeners?.onBlur?.({ value: '', fieldApi: {} as never })
expect(mockSetFieldValue).toHaveBeenCalledWith('label', 'existingVar')
})
})
describe('useHiddenConfigurations', () => {
afterEach(() => {
vi.clearAllMocks()
})
it('should return array of hidden configurations', () => {
const { result } = renderHook(() =>
useHiddenConfigurations({ options: undefined }),
)
expect(Array.isArray(result.current)).toBe(true)
expect(result.current.length).toBeGreaterThan(0)
})
it('should include default value config for textInput', () => {
const { result } = renderHook(() =>
useHiddenConfigurations({ options: undefined }),
)
const defaultConfigs = result.current.filter(c => c.variable === 'default')
expect(defaultConfigs.length).toBeGreaterThan(0)
})
it('should include tooltips configuration for all types', () => {
const { result } = renderHook(() =>
useHiddenConfigurations({ options: undefined }),
)
const tooltipsConfig = result.current.find(c => c.variable === 'tooltips')
expect(tooltipsConfig).toBeDefined()
expect(tooltipsConfig?.showConditions).toEqual([])
})
it('should build select options from provided options', () => {
const { result } = renderHook(() =>
useHiddenConfigurations({ options: ['opt1', 'opt2'] }),
)
const selectDefault = result.current.find(
c => c.variable === 'default' && c.showConditions?.some(sc => sc.value === PipelineInputVarType.select),
)
expect(selectDefault?.options).toBeDefined()
// First option should be "no default selected"
expect(selectDefault?.options?.[0]?.value).toBe('')
expect(selectDefault?.options?.[1]?.value).toBe('opt1')
expect(selectDefault?.options?.[2]?.value).toBe('opt2')
})
it('should return empty options when options prop is undefined', () => {
const { result } = renderHook(() =>
useHiddenConfigurations({ options: undefined }),
)
const selectDefault = result.current.find(
c => c.variable === 'default' && c.showConditions?.some(sc => sc.value === PipelineInputVarType.select),
)
expect(selectDefault?.options).toEqual([])
})
it('should include upload method configs for file types', () => {
const { result } = renderHook(() =>
useHiddenConfigurations({ options: undefined }),
)
const uploadMethods = result.current.filter(c => c.variable === 'allowedFileUploadMethods')
expect(uploadMethods.length).toBe(2) // singleFile + multiFiles
})
it('should include maxLength slider for multiFiles', () => {
const { result } = renderHook(() =>
useHiddenConfigurations({ options: undefined }),
)
const maxLength = result.current.find(
c => c.variable === 'maxLength' && c.showConditions?.some(sc => sc.value === PipelineInputVarType.multiFiles),
)
expect(maxLength).toBeDefined()
expect(maxLength?.description).toBeDefined()
})
})

View File

@ -0,0 +1,269 @@
import type { TFunction } from 'i18next'
import { describe, expect, it, vi } from 'vitest'
import { PipelineInputVarType } from '@/models/pipeline'
import { createInputFieldSchema, TEXT_MAX_LENGTH } from './schema'
// ============================================================================
// Mocks
// ============================================================================
vi.mock('@/config', () => ({
MAX_VAR_KEY_LENGTH: 30,
}))
// Minimal t function for testing
const t: TFunction = ((key: string) => key) as unknown as TFunction
const defaultOptions = { maxFileUploadLimit: 10 }
// ============================================================================
// Tests
// ============================================================================
describe('TEXT_MAX_LENGTH', () => {
it('should be 256', () => {
expect(TEXT_MAX_LENGTH).toBe(256)
})
})
describe('createInputFieldSchema', () => {
describe('common schema validation', () => {
it('should reject empty variable name', () => {
const schema = createInputFieldSchema(PipelineInputVarType.textInput, t, defaultOptions)
const result = schema.safeParse({
type: 'text-input',
variable: '',
label: 'Test',
required: false,
maxLength: 48,
})
expect(result.success).toBe(false)
})
it('should reject variable starting with number', () => {
const schema = createInputFieldSchema(PipelineInputVarType.textInput, t, defaultOptions)
const result = schema.safeParse({
type: 'text-input',
variable: '123abc',
label: 'Test',
required: false,
maxLength: 48,
})
expect(result.success).toBe(false)
})
it('should accept valid variable name', () => {
const schema = createInputFieldSchema(PipelineInputVarType.textInput, t, defaultOptions)
const result = schema.safeParse({
type: 'text-input',
variable: 'valid_var',
label: 'Test',
required: false,
maxLength: 48,
})
expect(result.success).toBe(true)
})
it('should reject empty label', () => {
const schema = createInputFieldSchema(PipelineInputVarType.textInput, t, defaultOptions)
const result = schema.safeParse({
type: 'text-input',
variable: 'my_var',
label: '',
required: false,
maxLength: 48,
})
expect(result.success).toBe(false)
})
})
describe('text input type', () => {
it('should validate maxLength within range', () => {
const schema = createInputFieldSchema(PipelineInputVarType.textInput, t, defaultOptions)
const valid = schema.safeParse({
type: 'text-input',
variable: 'text_var',
label: 'Text',
required: false,
maxLength: 100,
})
expect(valid.success).toBe(true)
const tooLow = schema.safeParse({
type: 'text-input',
variable: 'text_var',
label: 'Text',
required: false,
maxLength: 0,
})
expect(tooLow.success).toBe(false)
})
it('should allow optional default and tooltips', () => {
const schema = createInputFieldSchema(PipelineInputVarType.textInput, t, defaultOptions)
const result = schema.safeParse({
type: 'text-input',
variable: 'text_var',
label: 'Text',
required: false,
maxLength: 48,
default: 'default value',
tooltips: 'Some help text',
})
expect(result.success).toBe(true)
})
})
describe('paragraph type', () => {
it('should use same schema as text input', () => {
const schema = createInputFieldSchema(PipelineInputVarType.paragraph, t, defaultOptions)
const result = schema.safeParse({
type: 'paragraph',
variable: 'para_var',
label: 'Paragraph',
required: false,
maxLength: 100,
})
expect(result.success).toBe(true)
})
})
describe('number type', () => {
it('should allow optional unit and placeholder', () => {
const schema = createInputFieldSchema(PipelineInputVarType.number, t, defaultOptions)
const result = schema.safeParse({
type: 'number',
variable: 'num_var',
label: 'Number',
required: false,
default: 42,
unit: 'kg',
placeholder: 'Enter weight',
})
expect(result.success).toBe(true)
})
})
describe('select type', () => {
it('should require non-empty options array', () => {
const schema = createInputFieldSchema(PipelineInputVarType.select, t, defaultOptions)
const empty = schema.safeParse({
type: 'select',
variable: 'sel_var',
label: 'Select',
required: false,
options: [],
})
expect(empty.success).toBe(false)
const valid = schema.safeParse({
type: 'select',
variable: 'sel_var',
label: 'Select',
required: false,
options: ['opt1', 'opt2'],
})
expect(valid.success).toBe(true)
})
it('should reject duplicate options', () => {
const schema = createInputFieldSchema(PipelineInputVarType.select, t, defaultOptions)
const result = schema.safeParse({
type: 'select',
variable: 'sel_var',
label: 'Select',
required: false,
options: ['opt1', 'opt1'],
})
expect(result.success).toBe(false)
})
})
describe('singleFile type', () => {
it('should require file upload methods and types', () => {
const schema = createInputFieldSchema(PipelineInputVarType.singleFile, t, defaultOptions)
const result = schema.safeParse({
type: 'file',
variable: 'file_var',
label: 'File',
required: false,
allowedFileUploadMethods: ['local_file'],
allowedTypesAndExtensions: {
allowedFileTypes: ['document'],
},
})
expect(result.success).toBe(true)
})
})
describe('multiFiles type', () => {
it('should validate maxLength against maxFileUploadLimit', () => {
const schema = createInputFieldSchema(PipelineInputVarType.multiFiles, t, { maxFileUploadLimit: 5 })
const valid = schema.safeParse({
type: 'file-list',
variable: 'files_var',
label: 'Files',
required: false,
allowedFileUploadMethods: ['local_file'],
allowedTypesAndExtensions: {
allowedFileTypes: ['image'],
},
maxLength: 3,
})
expect(valid.success).toBe(true)
const tooMany = schema.safeParse({
type: 'file-list',
variable: 'files_var',
label: 'Files',
required: false,
allowedFileUploadMethods: ['local_file'],
allowedTypesAndExtensions: {
allowedFileTypes: ['image'],
},
maxLength: 10,
})
expect(tooMany.success).toBe(false)
})
})
describe('checkbox / default type', () => {
it('should use common schema for checkbox type', () => {
const schema = createInputFieldSchema(PipelineInputVarType.checkbox, t, defaultOptions)
const result = schema.safeParse({
type: 'checkbox',
variable: 'check_var',
label: 'Agree',
required: true,
})
expect(result.success).toBe(true)
})
it('should allow passthrough of extra fields', () => {
const schema = createInputFieldSchema(PipelineInputVarType.checkbox, t, defaultOptions)
const result = schema.safeParse({
type: 'checkbox',
variable: 'check_var',
label: 'Agree',
required: true,
default: true,
extraField: 'should pass through',
})
expect(result.success).toBe(true)
})
})
})

View File

@ -0,0 +1,397 @@
import type { InputVar } from '@/models/pipeline'
import { renderHook } from '@testing-library/react'
import { act } from 'react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useFieldList } from './hooks'
// ============================================================================
// Mocks
// ============================================================================
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
const mockToggleInputFieldEditPanel = vi.fn()
vi.mock('@/app/components/rag-pipeline/hooks', () => ({
useInputFieldPanel: () => ({
toggleInputFieldEditPanel: mockToggleInputFieldEditPanel,
}),
}))
const mockHandleInputVarRename = vi.fn()
const mockIsVarUsedInNodes = vi.fn()
const mockRemoveUsedVarInNodes = vi.fn()
vi.mock('../../../../hooks/use-pipeline', () => ({
usePipeline: () => ({
handleInputVarRename: mockHandleInputVarRename,
isVarUsedInNodes: mockIsVarUsedInNodes,
removeUsedVarInNodes: mockRemoveUsedVarInNodes,
}),
}))
const mockToastNotify = vi.fn()
vi.mock('@/app/components/base/toast', () => ({
default: {
notify: (...args: unknown[]) => mockToastNotify(...args),
},
}))
vi.mock('@/app/components/workflow/types', () => ({
ChangeType: {
changeVarName: 'changeVarName',
remove: 'remove',
},
}))
// ============================================================================
// Test data helpers
// ============================================================================
function createInputVar(overrides?: Partial<InputVar>): InputVar {
return {
type: 'text-input',
variable: 'test_var',
label: 'Test Var',
required: false,
...overrides,
} as InputVar
}
function createDefaultProps(overrides?: Partial<Parameters<typeof useFieldList>[0]>) {
return {
initialInputFields: [] as InputVar[],
onInputFieldsChange: vi.fn(),
nodeId: 'node-1',
allVariableNames: [] as string[],
...overrides,
}
}
// ============================================================================
// Tests
// ============================================================================
describe('useFieldList', () => {
beforeEach(() => {
vi.clearAllMocks()
mockIsVarUsedInNodes.mockReturnValue(false)
})
afterEach(() => {
vi.clearAllMocks()
})
describe('initialization', () => {
it('should return inputFields from initialInputFields', () => {
const fields = [createInputVar({ variable: 'var1' })]
const { result } = renderHook(() => useFieldList(createDefaultProps({ initialInputFields: fields })))
expect(result.current.inputFields).toEqual(fields)
})
it('should return empty inputFields when initialized with empty array', () => {
const { result } = renderHook(() => useFieldList(createDefaultProps()))
expect(result.current.inputFields).toEqual([])
})
it('should return all expected functions', () => {
const { result } = renderHook(() => useFieldList(createDefaultProps()))
expect(typeof result.current.handleListSortChange).toBe('function')
expect(typeof result.current.handleRemoveField).toBe('function')
expect(typeof result.current.handleOpenInputFieldEditor).toBe('function')
expect(typeof result.current.hideRemoveVarConfirm).toBe('function')
expect(typeof result.current.onRemoveVarConfirm).toBe('function')
})
it('should have isShowRemoveVarConfirm as false initially', () => {
const { result } = renderHook(() => useFieldList(createDefaultProps()))
expect(result.current.isShowRemoveVarConfirm).toBe(false)
})
})
describe('handleListSortChange', () => {
it('should reorder input fields and notify parent', () => {
const var1 = createInputVar({ variable: 'var1', label: 'V1' })
const var2 = createInputVar({ variable: 'var2', label: 'V2' })
const onInputFieldsChange = vi.fn()
const { result } = renderHook(() =>
useFieldList(createDefaultProps({
initialInputFields: [var1, var2],
onInputFieldsChange,
})),
)
act(() => {
result.current.handleListSortChange([
{ ...var2, id: '1', chosen: false, selected: false },
{ ...var1, id: '0', chosen: false, selected: false },
])
})
expect(onInputFieldsChange).toHaveBeenCalledWith([var2, var1])
})
it('should strip sortable metadata (id, chosen, selected) from items', () => {
const var1 = createInputVar({ variable: 'var1' })
const onInputFieldsChange = vi.fn()
const { result } = renderHook(() =>
useFieldList(createDefaultProps({
initialInputFields: [var1],
onInputFieldsChange,
})),
)
act(() => {
result.current.handleListSortChange([
{ ...var1, id: '0', chosen: true, selected: true },
])
})
const updatedFields = onInputFieldsChange.mock.calls[0][0]
expect(updatedFields[0]).not.toHaveProperty('id')
expect(updatedFields[0]).not.toHaveProperty('chosen')
expect(updatedFields[0]).not.toHaveProperty('selected')
})
})
describe('handleRemoveField', () => {
it('should remove field when variable is not used in nodes', () => {
const var1 = createInputVar({ variable: 'var1' })
const var2 = createInputVar({ variable: 'var2' })
const onInputFieldsChange = vi.fn()
mockIsVarUsedInNodes.mockReturnValue(false)
const { result } = renderHook(() =>
useFieldList(createDefaultProps({
initialInputFields: [var1, var2],
onInputFieldsChange,
})),
)
act(() => {
result.current.handleRemoveField(0)
})
expect(onInputFieldsChange).toHaveBeenCalledWith([var2])
})
it('should show confirmation when variable is used in other nodes', () => {
const var1 = createInputVar({ variable: 'used_var' })
const onInputFieldsChange = vi.fn()
mockIsVarUsedInNodes.mockReturnValue(true)
const { result } = renderHook(() =>
useFieldList(createDefaultProps({
initialInputFields: [var1],
onInputFieldsChange,
})),
)
act(() => {
result.current.handleRemoveField(0)
})
expect(result.current.isShowRemoveVarConfirm).toBe(true)
expect(onInputFieldsChange).not.toHaveBeenCalled()
})
})
describe('onRemoveVarConfirm', () => {
it('should remove field and clean up variable references after confirmation', () => {
const var1 = createInputVar({ variable: 'used_var' })
const onInputFieldsChange = vi.fn()
mockIsVarUsedInNodes.mockReturnValue(true)
const { result } = renderHook(() =>
useFieldList(createDefaultProps({
initialInputFields: [var1],
onInputFieldsChange,
nodeId: 'node-1',
})),
)
// Trigger remove to set up confirmation state
act(() => {
result.current.handleRemoveField(0)
})
expect(result.current.isShowRemoveVarConfirm).toBe(true)
// Confirm removal
act(() => {
result.current.onRemoveVarConfirm()
})
expect(onInputFieldsChange).toHaveBeenCalledWith([])
expect(mockRemoveUsedVarInNodes).toHaveBeenCalledWith(['rag', 'node-1', 'used_var'])
expect(result.current.isShowRemoveVarConfirm).toBe(false)
})
})
describe('handleOpenInputFieldEditor', () => {
it('should open editor with existing field data when id matches', () => {
const var1 = createInputVar({ variable: 'var1', label: 'Label 1' })
const { result } = renderHook(() =>
useFieldList(createDefaultProps({ initialInputFields: [var1] })),
)
act(() => {
result.current.handleOpenInputFieldEditor('var1')
})
expect(mockToggleInputFieldEditPanel).toHaveBeenCalledWith(
expect.objectContaining({
initialData: var1,
}),
)
})
it('should open editor for new field when id does not match', () => {
const { result } = renderHook(() =>
useFieldList(createDefaultProps()),
)
act(() => {
result.current.handleOpenInputFieldEditor('non-existent')
})
expect(mockToggleInputFieldEditPanel).toHaveBeenCalledWith(
expect.objectContaining({
initialData: undefined,
}),
)
})
it('should open editor for new field when no id provided', () => {
const { result } = renderHook(() =>
useFieldList(createDefaultProps()),
)
act(() => {
result.current.handleOpenInputFieldEditor()
})
expect(mockToggleInputFieldEditPanel).toHaveBeenCalledWith(
expect.objectContaining({
initialData: undefined,
}),
)
})
})
describe('field submission (via editor)', () => {
it('should add new field when editingFieldIndex is -1', () => {
const onInputFieldsChange = vi.fn()
const { result } = renderHook(() =>
useFieldList(createDefaultProps({ onInputFieldsChange })),
)
// Open editor for new field
act(() => {
result.current.handleOpenInputFieldEditor()
})
// Get the onSubmit callback from editor
const editorProps = mockToggleInputFieldEditPanel.mock.calls[0][0]
const newField = createInputVar({ variable: 'new_var', label: 'New' })
act(() => {
editorProps.onSubmit(newField)
})
expect(onInputFieldsChange).toHaveBeenCalledWith([newField])
})
it('should show error toast for duplicate variable names', () => {
const var1 = createInputVar({ variable: 'existing_var' })
const onInputFieldsChange = vi.fn()
const { result } = renderHook(() =>
useFieldList(createDefaultProps({
initialInputFields: [var1],
onInputFieldsChange,
allVariableNames: ['existing_var'],
})),
)
// Open editor for new field (not editing existing)
act(() => {
result.current.handleOpenInputFieldEditor()
})
const editorProps = mockToggleInputFieldEditPanel.mock.calls[0][0]
const duplicateField = createInputVar({ variable: 'existing_var' })
act(() => {
editorProps.onSubmit(duplicateField)
})
expect(mockToastNotify).toHaveBeenCalledWith(
expect.objectContaining({ type: 'error' }),
)
expect(onInputFieldsChange).not.toHaveBeenCalled()
})
it('should trigger variable rename when ChangeType is changeVarName', () => {
const var1 = createInputVar({ variable: 'old_name' })
const onInputFieldsChange = vi.fn()
const { result } = renderHook(() =>
useFieldList(createDefaultProps({
initialInputFields: [var1],
onInputFieldsChange,
nodeId: 'node-1',
allVariableNames: ['old_name'],
})),
)
// Open editor for existing field
act(() => {
result.current.handleOpenInputFieldEditor('old_name')
})
const editorProps = mockToggleInputFieldEditPanel.mock.calls[0][0]
const updatedField = createInputVar({ variable: 'new_name' })
act(() => {
editorProps.onSubmit(updatedField, {
type: 'changeVarName',
payload: { beforeKey: 'old_name', afterKey: 'new_name' },
})
})
expect(mockHandleInputVarRename).toHaveBeenCalledWith(
'node-1',
['rag', 'node-1', 'old_name'],
['rag', 'node-1', 'new_name'],
)
})
})
describe('hideRemoveVarConfirm', () => {
it('should hide the confirmation dialog', () => {
const var1 = createInputVar({ variable: 'used_var' })
mockIsVarUsedInNodes.mockReturnValue(true)
const { result } = renderHook(() =>
useFieldList(createDefaultProps({ initialInputFields: [var1] })),
)
// Show confirmation
act(() => {
result.current.handleRemoveField(0)
})
expect(result.current.isShowRemoveVarConfirm).toBe(true)
// Hide confirmation
act(() => {
result.current.hideRemoveVarConfirm()
})
expect(result.current.isShowRemoveVarConfirm).toBe(false)
})
})
})

View File

@ -2185,7 +2185,7 @@ describe('handleSubmitField', () => {
// Simulate form submission with moreInfo but different type
const updatedFieldData = createInputVar({ variable: 'var_0', label: 'Updated Label' })
editorProps.onSubmit(updatedFieldData, { type: 'otherType' as any })
editorProps.onSubmit(updatedFieldData, { type: 'otherType' as never })
// Assert - handleInputVarRename should NOT be called
expect(mockHandleInputVarRename).not.toHaveBeenCalled()

View File

@ -0,0 +1,243 @@
import type { DataSourceNodeType } from '@/app/components/workflow/nodes/data-source/types'
import { renderHook } from '@testing-library/react'
import { act } from 'react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { BlockEnum } from '@/app/components/workflow/types'
import { useDatasourceOptions, useOnlineDocument, useOnlineDrive, useTestRunSteps, useWebsiteCrawl } from './hooks'
// ============================================================================
// Mocks
// ============================================================================
vi.mock('react-i18next', () => ({
useTranslation: () => ({
t: (key: string) => key,
}),
}))
const mockNodes: Array<{ id: string, data: Partial<DataSourceNodeType> & { type: string } }> = []
vi.mock('reactflow', () => ({
useNodes: () => mockNodes,
}))
const mockDataSourceStoreGetState = vi.fn()
vi.mock('@/app/components/datasets/documents/create-from-pipeline/data-source/store', () => ({
useDataSourceStore: () => ({
getState: mockDataSourceStoreGetState,
}),
}))
vi.mock('@/app/components/workflow/types', () => ({
BlockEnum: {
DataSource: 'data-source',
},
}))
vi.mock('../types', () => ({
TestRunStep: {
dataSource: 'dataSource',
documentProcessing: 'documentProcessing',
},
}))
vi.mock('@/models/datasets', () => ({
CrawlStep: {
init: 'init',
},
}))
// ============================================================================
// Tests
// ============================================================================
describe('useTestRunSteps', () => {
afterEach(() => {
vi.clearAllMocks()
})
it('should initialize with step 1', () => {
const { result } = renderHook(() => useTestRunSteps())
expect(result.current.currentStep).toBe(1)
})
it('should return 2 steps (dataSource and documentProcessing)', () => {
const { result } = renderHook(() => useTestRunSteps())
expect(result.current.steps).toHaveLength(2)
expect(result.current.steps[0].value).toBe('dataSource')
expect(result.current.steps[1].value).toBe('documentProcessing')
})
it('should increment step on handleNextStep', () => {
const { result } = renderHook(() => useTestRunSteps())
act(() => {
result.current.handleNextStep()
})
expect(result.current.currentStep).toBe(2)
})
it('should decrement step on handleBackStep', () => {
const { result } = renderHook(() => useTestRunSteps())
// Go to step 2 first
act(() => {
result.current.handleNextStep()
})
expect(result.current.currentStep).toBe(2)
// Go back
act(() => {
result.current.handleBackStep()
})
expect(result.current.currentStep).toBe(1)
})
it('should have translated step labels', () => {
const { result } = renderHook(() => useTestRunSteps())
expect(result.current.steps[0].label).toBeDefined()
expect(typeof result.current.steps[0].label).toBe('string')
})
})
describe('useDatasourceOptions', () => {
beforeEach(() => {
mockNodes.length = 0
vi.clearAllMocks()
})
it('should return empty options when no DataSource nodes', () => {
mockNodes.push({ id: 'n1', data: { type: BlockEnum.LLM, title: 'LLM' } })
const { result } = renderHook(() => useDatasourceOptions())
expect(result.current).toEqual([])
})
it('should return options from DataSource nodes', () => {
mockNodes.push(
{ id: 'ds-1', data: { type: BlockEnum.DataSource, title: 'Source A' } },
{ id: 'ds-2', data: { type: BlockEnum.DataSource, title: 'Source B' } },
)
const { result } = renderHook(() => useDatasourceOptions())
expect(result.current).toHaveLength(2)
expect(result.current[0]).toEqual({
label: 'Source A',
value: 'ds-1',
data: expect.objectContaining({ type: 'data-source' }),
})
expect(result.current[1]).toEqual({
label: 'Source B',
value: 'ds-2',
data: expect.objectContaining({ type: 'data-source' }),
})
})
it('should filter out non-DataSource nodes', () => {
mockNodes.push(
{ id: 'ds-1', data: { type: BlockEnum.DataSource, title: 'Source' } },
{ id: 'llm-1', data: { type: BlockEnum.LLM, title: 'LLM' } },
{ id: 'end-1', data: { type: BlockEnum.End, title: 'End' } },
)
const { result } = renderHook(() => useDatasourceOptions())
expect(result.current).toHaveLength(1)
expect(result.current[0].value).toBe('ds-1')
})
})
describe('useOnlineDocument', () => {
it('should clear all online document data', () => {
const mockSetDocumentsData = vi.fn()
const mockSetSearchValue = vi.fn()
const mockSetSelectedPagesId = vi.fn()
const mockSetOnlineDocuments = vi.fn()
const mockSetCurrentDocument = vi.fn()
mockDataSourceStoreGetState.mockReturnValue({
setDocumentsData: mockSetDocumentsData,
setSearchValue: mockSetSearchValue,
setSelectedPagesId: mockSetSelectedPagesId,
setOnlineDocuments: mockSetOnlineDocuments,
setCurrentDocument: mockSetCurrentDocument,
})
const { result } = renderHook(() => useOnlineDocument())
act(() => {
result.current.clearOnlineDocumentData()
})
expect(mockSetDocumentsData).toHaveBeenCalledWith([])
expect(mockSetSearchValue).toHaveBeenCalledWith('')
expect(mockSetSelectedPagesId).toHaveBeenCalledWith(new Set())
expect(mockSetOnlineDocuments).toHaveBeenCalledWith([])
expect(mockSetCurrentDocument).toHaveBeenCalledWith(undefined)
})
})
describe('useWebsiteCrawl', () => {
it('should clear all website crawl data', () => {
const mockSetStep = vi.fn()
const mockSetCrawlResult = vi.fn()
const mockSetWebsitePages = vi.fn()
const mockSetPreviewIndex = vi.fn()
const mockSetCurrentWebsite = vi.fn()
mockDataSourceStoreGetState.mockReturnValue({
setStep: mockSetStep,
setCrawlResult: mockSetCrawlResult,
setWebsitePages: mockSetWebsitePages,
setPreviewIndex: mockSetPreviewIndex,
setCurrentWebsite: mockSetCurrentWebsite,
})
const { result } = renderHook(() => useWebsiteCrawl())
act(() => {
result.current.clearWebsiteCrawlData()
})
expect(mockSetStep).toHaveBeenCalledWith('init')
expect(mockSetCrawlResult).toHaveBeenCalledWith(undefined)
expect(mockSetCurrentWebsite).toHaveBeenCalledWith(undefined)
expect(mockSetWebsitePages).toHaveBeenCalledWith([])
expect(mockSetPreviewIndex).toHaveBeenCalledWith(-1)
})
})
describe('useOnlineDrive', () => {
it('should clear all online drive data', () => {
const mockSetOnlineDriveFileList = vi.fn()
const mockSetBucket = vi.fn()
const mockSetPrefix = vi.fn()
const mockSetKeywords = vi.fn()
const mockSetSelectedFileIds = vi.fn()
mockDataSourceStoreGetState.mockReturnValue({
setOnlineDriveFileList: mockSetOnlineDriveFileList,
setBucket: mockSetBucket,
setPrefix: mockSetPrefix,
setKeywords: mockSetKeywords,
setSelectedFileIds: mockSetSelectedFileIds,
})
const { result } = renderHook(() => useOnlineDrive())
act(() => {
result.current.clearOnlineDriveData()
})
expect(mockSetOnlineDriveFileList).toHaveBeenCalledWith([])
expect(mockSetBucket).toHaveBeenCalledWith('')
expect(mockSetPrefix).toHaveBeenCalledWith([])
expect(mockSetKeywords).toHaveBeenCalledWith('')
expect(mockSetSelectedFileIds).toHaveBeenCalledWith([])
})
})

View File

@ -307,11 +307,10 @@ describe('useInputFieldPanel', () => {
it('should set edit panel props when toggleInputFieldEditPanel is called', () => {
const { result } = renderHook(() => useInputFieldPanel())
const editContent = { type: 'edit', data: {} }
const editContent = { onClose: vi.fn(), onSubmit: vi.fn() }
act(() => {
// eslint-disable-next-line ts/no-explicit-any
result.current.toggleInputFieldEditPanel(editContent as any)
result.current.toggleInputFieldEditPanel(editContent)
})
expect(mockSetInputFieldEditPanelProps).toHaveBeenCalledWith(editContent)

View File

@ -1,4 +1,4 @@
/* eslint-disable ts/no-explicit-any */
import type { VersionHistory } from '@/types/workflow'
import { renderHook } from '@testing-library/react'
import { act } from 'react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
@ -316,7 +316,7 @@ describe('usePipelineRun', () => {
const { result } = renderHook(() => usePipelineRun())
act(() => {
result.current.handleRestoreFromPublishedWorkflow(publishedWorkflow as any)
result.current.handleRestoreFromPublishedWorkflow(publishedWorkflow as unknown as VersionHistory)
})
expect(mockHandleUpdateWorkflowCanvas).toHaveBeenCalledWith({
@ -340,7 +340,7 @@ describe('usePipelineRun', () => {
const { result } = renderHook(() => usePipelineRun())
act(() => {
result.current.handleRestoreFromPublishedWorkflow(publishedWorkflow as any)
result.current.handleRestoreFromPublishedWorkflow(publishedWorkflow as unknown as VersionHistory)
})
expect(mockSetEnvironmentVariables).toHaveBeenCalledWith([{ key: 'ENV', value: 'value' }])
@ -360,7 +360,7 @@ describe('usePipelineRun', () => {
const { result } = renderHook(() => usePipelineRun())
act(() => {
result.current.handleRestoreFromPublishedWorkflow(publishedWorkflow as any)
result.current.handleRestoreFromPublishedWorkflow(publishedWorkflow as unknown as VersionHistory)
})
expect(mockSetRagPipelineVariables).toHaveBeenCalledWith([{ variable: 'query', type: 'text-input' }])
@ -380,7 +380,7 @@ describe('usePipelineRun', () => {
const { result } = renderHook(() => usePipelineRun())
act(() => {
result.current.handleRestoreFromPublishedWorkflow(publishedWorkflow as any)
result.current.handleRestoreFromPublishedWorkflow(publishedWorkflow as unknown as VersionHistory)
})
expect(mockSetEnvironmentVariables).toHaveBeenCalledWith([])
@ -779,7 +779,7 @@ describe('usePipelineRun', () => {
const { result } = renderHook(() => usePipelineRun())
await act(async () => {
await result.current.handleRun({ inputs: {} }, { onData: customCallback } as any)
await result.current.handleRun({ inputs: {} }, { onData: customCallback } as unknown as Parameters<typeof result.current.handleRun>[1])
})
expect(capturedCallbacks.onData).toBeDefined()

View File

@ -0,0 +1,339 @@
import { renderHook } from '@testing-library/react'
import { act } from 'react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { usePipeline } from './use-pipeline'
// ============================================================================
// Mocks
// ============================================================================
const mockGetNodes = vi.fn()
const mockSetNodes = vi.fn()
const mockEdges: Array<{ id: string, source: string, target: string }> = []
vi.mock('reactflow', () => ({
useStoreApi: () => ({
getState: () => ({
getNodes: mockGetNodes,
setNodes: mockSetNodes,
edges: mockEdges,
}),
}),
getOutgoers: (node: { id: string }, nodes: Array<{ id: string }>, edges: Array<{ source: string, target: string }>) => {
return nodes.filter(n => edges.some(e => e.source === node.id && e.target === n.id))
},
}))
const mockFindUsedVarNodes = vi.fn()
const mockUpdateNodeVars = vi.fn()
vi.mock('../../workflow/nodes/_base/components/variable/utils', () => ({
findUsedVarNodes: (...args: unknown[]) => mockFindUsedVarNodes(...args),
updateNodeVars: (...args: unknown[]) => mockUpdateNodeVars(...args),
}))
vi.mock('../../workflow/types', () => ({
BlockEnum: {
DataSource: 'data-source',
},
}))
vi.mock('es-toolkit/compat', () => ({
uniqBy: (arr: Array<{ id: string }>, key: string) => {
const seen = new Set<string>()
return arr.filter((item) => {
const val = item[key as keyof typeof item] as string
if (seen.has(val))
return false
seen.add(val)
return true
})
},
}))
// ============================================================================
// Test data helpers
// ============================================================================
function createNode(id: string, type: string) {
return { id, data: { type }, position: { x: 0, y: 0 } }
}
// ============================================================================
// Tests
// ============================================================================
describe('usePipeline', () => {
beforeEach(() => {
vi.clearAllMocks()
mockEdges.length = 0
})
afterEach(() => {
vi.clearAllMocks()
})
describe('hook initialization', () => {
it('should return handleInputVarRename function', () => {
mockGetNodes.mockReturnValue([])
const { result } = renderHook(() => usePipeline())
expect(result.current.handleInputVarRename).toBeDefined()
expect(typeof result.current.handleInputVarRename).toBe('function')
})
it('should return isVarUsedInNodes function', () => {
mockGetNodes.mockReturnValue([])
const { result } = renderHook(() => usePipeline())
expect(result.current.isVarUsedInNodes).toBeDefined()
expect(typeof result.current.isVarUsedInNodes).toBe('function')
})
it('should return removeUsedVarInNodes function', () => {
mockGetNodes.mockReturnValue([])
const { result } = renderHook(() => usePipeline())
expect(result.current.removeUsedVarInNodes).toBeDefined()
expect(typeof result.current.removeUsedVarInNodes).toBe('function')
})
})
describe('isVarUsedInNodes', () => {
it('should return true when variable is used in downstream nodes', () => {
const dsNode = createNode('ds-1', 'data-source')
const downstreamNode = createNode('node-2', 'llm')
mockGetNodes.mockReturnValue([dsNode, downstreamNode])
mockEdges.push({ id: 'e1', source: 'ds-1', target: 'node-2' })
mockFindUsedVarNodes.mockReturnValue([downstreamNode])
const { result } = renderHook(() => usePipeline())
const isUsed = result.current.isVarUsedInNodes(['rag', 'ds-1', 'var1'])
expect(isUsed).toBe(true)
expect(mockFindUsedVarNodes).toHaveBeenCalledWith(
['rag', 'ds-1', 'var1'],
expect.any(Array),
)
})
it('should return false when variable is not used', () => {
const dsNode = createNode('ds-1', 'data-source')
mockGetNodes.mockReturnValue([dsNode])
mockFindUsedVarNodes.mockReturnValue([])
const { result } = renderHook(() => usePipeline())
const isUsed = result.current.isVarUsedInNodes(['rag', 'ds-1', 'var1'])
expect(isUsed).toBe(false)
})
it('should handle shared nodeId by collecting all datasource nodes', () => {
const ds1 = createNode('ds-1', 'data-source')
const ds2 = createNode('ds-2', 'data-source')
const node3 = createNode('node-3', 'llm')
mockGetNodes.mockReturnValue([ds1, ds2, node3])
mockEdges.push({ id: 'e1', source: 'ds-1', target: 'node-3' })
mockFindUsedVarNodes.mockReturnValue([node3])
const { result } = renderHook(() => usePipeline())
const isUsed = result.current.isVarUsedInNodes(['rag', 'shared', 'var1'])
expect(isUsed).toBe(true)
})
it('should return false for shared nodeId when no datasource nodes exist', () => {
mockGetNodes.mockReturnValue([createNode('node-1', 'llm')])
mockFindUsedVarNodes.mockReturnValue([])
const { result } = renderHook(() => usePipeline())
const isUsed = result.current.isVarUsedInNodes(['rag', 'shared', 'var1'])
expect(isUsed).toBe(false)
})
})
describe('handleInputVarRename', () => {
it('should rename variable in affected nodes', () => {
const dsNode = createNode('ds-1', 'data-source')
const node2 = createNode('node-2', 'llm')
const updatedNode2 = { ...node2, data: { ...node2.data, renamed: true } }
mockGetNodes.mockReturnValue([dsNode, node2])
mockEdges.push({ id: 'e1', source: 'ds-1', target: 'node-2' })
mockFindUsedVarNodes.mockReturnValue([node2])
mockUpdateNodeVars.mockReturnValue(updatedNode2)
const { result } = renderHook(() => usePipeline())
act(() => {
result.current.handleInputVarRename(
'ds-1',
['rag', 'ds-1', 'oldVar'],
['rag', 'ds-1', 'newVar'],
)
})
expect(mockFindUsedVarNodes).toHaveBeenCalledWith(
['rag', 'ds-1', 'oldVar'],
expect.any(Array),
)
expect(mockUpdateNodeVars).toHaveBeenCalledWith(
node2,
['rag', 'ds-1', 'oldVar'],
['rag', 'ds-1', 'newVar'],
)
expect(mockSetNodes).toHaveBeenCalled()
})
it('should not call setNodes when no nodes are affected', () => {
const dsNode = createNode('ds-1', 'data-source')
mockGetNodes.mockReturnValue([dsNode])
mockFindUsedVarNodes.mockReturnValue([])
const { result } = renderHook(() => usePipeline())
act(() => {
result.current.handleInputVarRename(
'ds-1',
['rag', 'ds-1', 'oldVar'],
['rag', 'ds-1', 'newVar'],
)
})
expect(mockSetNodes).not.toHaveBeenCalled()
})
it('should only update affected nodes, leave others unchanged', () => {
const dsNode = createNode('ds-1', 'data-source')
const node2 = createNode('node-2', 'llm')
const node3 = createNode('node-3', 'end')
mockGetNodes.mockReturnValue([dsNode, node2, node3])
mockEdges.push(
{ id: 'e1', source: 'ds-1', target: 'node-2' },
{ id: 'e2', source: 'node-2', target: 'node-3' },
)
// Only node2 uses the variable
mockFindUsedVarNodes.mockReturnValue([node2])
const updatedNode2 = { ...node2, updated: true }
mockUpdateNodeVars.mockReturnValue(updatedNode2)
const { result } = renderHook(() => usePipeline())
act(() => {
result.current.handleInputVarRename(
'ds-1',
['rag', 'ds-1', 'var1'],
['rag', 'ds-1', 'var2'],
)
})
const setNodesArg = mockSetNodes.mock.calls[0][0]
// node2 should be updated, dsNode and node3 should remain the same
expect(setNodesArg).toContain(dsNode)
expect(setNodesArg).toContain(updatedNode2)
expect(setNodesArg).toContain(node3)
})
})
describe('removeUsedVarInNodes', () => {
it('should remove variable references from affected nodes', () => {
const dsNode = createNode('ds-1', 'data-source')
const node2 = createNode('node-2', 'llm')
const cleanedNode2 = { ...node2, data: { ...node2.data, cleaned: true } }
mockGetNodes.mockReturnValue([dsNode, node2])
mockEdges.push({ id: 'e1', source: 'ds-1', target: 'node-2' })
mockFindUsedVarNodes.mockReturnValue([node2])
mockUpdateNodeVars.mockReturnValue(cleanedNode2)
const { result } = renderHook(() => usePipeline())
act(() => {
result.current.removeUsedVarInNodes(['rag', 'ds-1', 'var1'])
})
expect(mockUpdateNodeVars).toHaveBeenCalledWith(
node2,
['rag', 'ds-1', 'var1'],
[], // Empty array removes the variable
)
expect(mockSetNodes).toHaveBeenCalled()
})
it('should not call setNodes when no nodes use the variable', () => {
const dsNode = createNode('ds-1', 'data-source')
mockGetNodes.mockReturnValue([dsNode])
mockFindUsedVarNodes.mockReturnValue([])
const { result } = renderHook(() => usePipeline())
act(() => {
result.current.removeUsedVarInNodes(['rag', 'ds-1', 'var1'])
})
expect(mockSetNodes).not.toHaveBeenCalled()
})
})
describe('getAllNodesInSameBranch — edge cases', () => {
it('should traverse multi-level downstream nodes', () => {
const ds = createNode('ds-1', 'data-source')
const n2 = createNode('node-2', 'llm')
const n3 = createNode('node-3', 'end')
mockGetNodes.mockReturnValue([ds, n2, n3])
mockEdges.push(
{ id: 'e1', source: 'ds-1', target: 'node-2' },
{ id: 'e2', source: 'node-2', target: 'node-3' },
)
mockFindUsedVarNodes.mockReturnValue([n3])
mockUpdateNodeVars.mockReturnValue(n3)
const { result } = renderHook(() => usePipeline())
// isVarUsedInNodes triggers getAllNodesInSameBranch internally
const isUsed = result.current.isVarUsedInNodes(['rag', 'ds-1', 'var1'])
expect(isUsed).toBe(true)
// Verify findUsedVarNodes was called with all downstream nodes (ds-1, node-2, node-3)
const nodesArg = mockFindUsedVarNodes.mock.calls[0][1] as Array<{ id: string }>
const nodeIds = nodesArg.map(n => n.id)
expect(nodeIds).toContain('ds-1')
expect(nodeIds).toContain('node-2')
expect(nodeIds).toContain('node-3')
})
it('should return empty array for non-existent node', () => {
mockGetNodes.mockReturnValue([createNode('ds-1', 'data-source')])
mockFindUsedVarNodes.mockReturnValue([])
const { result } = renderHook(() => usePipeline())
const isUsed = result.current.isVarUsedInNodes(['rag', 'non-existent', 'var1'])
expect(isUsed).toBe(false)
})
it('should deduplicate nodes when traversal finds shared nodes', () => {
// Diamond graph: ds-1 -> n2, ds-1 -> n3, n2 -> n4, n3 -> n4
const ds = createNode('ds-1', 'data-source')
const n2 = createNode('node-2', 'llm')
const n3 = createNode('node-3', 'llm')
const n4 = createNode('node-4', 'end')
mockGetNodes.mockReturnValue([ds, n2, n3, n4])
mockEdges.push(
{ id: 'e1', source: 'ds-1', target: 'node-2' },
{ id: 'e2', source: 'ds-1', target: 'node-3' },
{ id: 'e3', source: 'node-2', target: 'node-4' },
{ id: 'e4', source: 'node-3', target: 'node-4' },
)
mockFindUsedVarNodes.mockReturnValue([])
const { result } = renderHook(() => usePipeline())
result.current.isVarUsedInNodes(['rag', 'ds-1', 'var1'])
// findUsedVarNodes should receive unique nodes (no duplicates of node-4)
const nodesArg = mockFindUsedVarNodes.mock.calls[0][1] as Array<{ id: string }>
const nodeIds = nodesArg.map(n => n.id)
const uniqueIds = [...new Set(nodeIds)]
expect(nodeIds.length).toBe(uniqueIds.length)
})
})
})

View File

@ -0,0 +1,231 @@
import { renderHook } from '@testing-library/react'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { BlockEnum } from '@/app/components/workflow/types'
import { useRagPipelineSearch } from './use-rag-pipeline-search'
// ============================================================================
// Mocks
// ============================================================================
const mockNodes: Array<{ id: string, data: Record<string, unknown> }> = []
vi.mock('@/app/components/workflow/store/workflow/use-nodes', () => ({
default: () => mockNodes,
}))
const mockHandleNodeSelect = vi.fn()
vi.mock('@/app/components/workflow/hooks/use-nodes-interactions', () => ({
useNodesInteractions: () => ({
handleNodeSelect: mockHandleNodeSelect,
}),
}))
vi.mock('@/app/components/workflow/hooks/use-tool-icon', () => ({
useGetToolIcon: () => () => null,
}))
vi.mock('@/app/components/workflow/block-icon', () => ({
default: () => null,
}))
type MockSearchResult = {
title: string
type: string
description?: string
metadata?: { nodeId: string }
}
const mockRagPipelineNodesAction = vi.hoisted(() => {
return { searchFn: undefined as undefined | ((query: string) => MockSearchResult[]) }
})
vi.mock('@/app/components/goto-anything/actions/rag-pipeline-nodes', () => ({
ragPipelineNodesAction: mockRagPipelineNodesAction,
}))
const mockCleanupListener = vi.fn()
vi.mock('@/app/components/workflow/utils/node-navigation', () => ({
setupNodeSelectionListener: () => mockCleanupListener,
}))
// ============================================================================
// Tests
// ============================================================================
describe('useRagPipelineSearch', () => {
beforeEach(() => {
vi.clearAllMocks()
mockNodes.length = 0
mockRagPipelineNodesAction.searchFn = undefined
})
afterEach(() => {
vi.clearAllMocks()
})
describe('hook lifecycle', () => {
it('should return null', () => {
const { result } = renderHook(() => useRagPipelineSearch())
expect(result.current).toBeNull()
})
it('should register search function when nodes exist', () => {
mockNodes.push({
id: 'node-1',
data: { type: BlockEnum.LLM, title: 'LLM Node', desc: '' },
})
renderHook(() => useRagPipelineSearch())
expect(mockRagPipelineNodesAction.searchFn).toBeDefined()
})
it('should not register search function when no nodes', () => {
renderHook(() => useRagPipelineSearch())
expect(mockRagPipelineNodesAction.searchFn).toBeUndefined()
})
it('should cleanup search function on unmount', () => {
mockNodes.push({
id: 'node-1',
data: { type: BlockEnum.Start, title: 'Start', desc: '' },
})
const { unmount } = renderHook(() => useRagPipelineSearch())
expect(mockRagPipelineNodesAction.searchFn).toBeDefined()
unmount()
expect(mockRagPipelineNodesAction.searchFn).toBeUndefined()
})
it('should setup node selection listener', () => {
const { unmount } = renderHook(() => useRagPipelineSearch())
unmount()
expect(mockCleanupListener).toHaveBeenCalled()
})
})
describe('search functionality', () => {
beforeEach(() => {
mockNodes.push(
{
id: 'node-1',
data: { type: BlockEnum.LLM, title: 'GPT Model', desc: 'Language model' },
},
{
id: 'node-2',
data: { type: BlockEnum.KnowledgeRetrieval, title: 'Knowledge Base', desc: 'Search knowledge', dataset_ids: ['ds1', 'ds2'] },
},
{
id: 'node-3',
data: { type: BlockEnum.Tool, title: 'Web Search', desc: '', tool_description: 'Search the web', tool_label: 'WebSearch' },
},
{
id: 'node-4',
data: { type: BlockEnum.Start, title: 'Start Node', desc: 'Pipeline entry' },
},
)
})
it('should find nodes by title', () => {
renderHook(() => useRagPipelineSearch())
const searchFn = mockRagPipelineNodesAction.searchFn!
const results = searchFn('GPT')
expect(results.length).toBeGreaterThan(0)
expect(results[0].title).toBe('GPT Model')
})
it('should find nodes by type', () => {
renderHook(() => useRagPipelineSearch())
const searchFn = mockRagPipelineNodesAction.searchFn!
const results = searchFn(BlockEnum.LLM)
expect(results.some(r => r.title === 'GPT Model')).toBe(true)
})
it('should find nodes by description', () => {
renderHook(() => useRagPipelineSearch())
const searchFn = mockRagPipelineNodesAction.searchFn!
const results = searchFn('knowledge')
expect(results.some(r => r.title === 'Knowledge Base')).toBe(true)
})
it('should return all nodes when search term is empty', () => {
renderHook(() => useRagPipelineSearch())
const searchFn = mockRagPipelineNodesAction.searchFn!
const results = searchFn('')
expect(results.length).toBe(4)
})
it('should sort by alphabetical order when no search term', () => {
renderHook(() => useRagPipelineSearch())
const searchFn = mockRagPipelineNodesAction.searchFn!
const results = searchFn('')
const titles = results.map(r => r.title)
// Should be alphabetically sorted
const sortedTitles = [...titles].sort((a, b) => a.localeCompare(b))
expect(titles).toEqual(sortedTitles)
})
it('should sort by relevance score when search term provided', () => {
renderHook(() => useRagPipelineSearch())
const searchFn = mockRagPipelineNodesAction.searchFn!
const results = searchFn('Search')
// "Web Search" has title match (score 10), "Knowledge Base" has desc match (score 5)
expect(results[0].title).toBe('Web Search')
})
it('should return empty array when no nodes match', () => {
renderHook(() => useRagPipelineSearch())
const searchFn = mockRagPipelineNodesAction.searchFn!
const results = searchFn('nonexistent-xyz-12345')
expect(results).toEqual([])
})
it('should enhance Tool node description from tool_description', () => {
renderHook(() => useRagPipelineSearch())
const searchFn = mockRagPipelineNodesAction.searchFn!
const results = searchFn('web')
const toolResult = results.find(r => r.title === 'Web Search')
expect(toolResult).toBeDefined()
expect(toolResult?.description).toContain('Search the web')
})
it('should include metadata with nodeId', () => {
renderHook(() => useRagPipelineSearch())
const searchFn = mockRagPipelineNodesAction.searchFn!
const results = searchFn('Start')
const startResult = results.find(r => r.title === 'Start Node')
expect(startResult?.metadata?.nodeId).toBe('node-4')
})
it('should set result type as workflow-node', () => {
renderHook(() => useRagPipelineSearch())
const searchFn = mockRagPipelineNodesAction.searchFn!
const results = searchFn('Start')
expect(results[0].type).toBe('workflow-node')
})
})
})

View File

@ -41,7 +41,7 @@ vi.mock('./components/conversion', () => ({
// Mock: Complex component with many hooks and workflow dependencies
vi.mock('./components/rag-pipeline-main', () => ({
default: ({ nodes, edges, viewport }: any) => (
default: ({ nodes, edges, viewport }: { nodes?: unknown[], edges?: unknown[], viewport?: { zoom?: number } }) => (
<div data-testid="rag-pipeline-main">
<span data-testid="nodes-count">{nodes?.length ?? 0}</span>
<span data-testid="edges-count">{edges?.length ?? 0}</span>
@ -72,7 +72,7 @@ const mockProcessNodesWithoutDataSource = vi.mocked(processNodesWithoutDataSourc
// Helper to mock selector with actual execution (increases function coverage)
// This executes the real selector function: s => s.dataset?.pipeline_id
const mockSelectorWithDataset = (pipelineId: string | null | undefined) => {
mockUseDatasetDetailContextWithSelector.mockImplementation((selector: (state: any) => any) => {
mockUseDatasetDetailContextWithSelector.mockImplementation((selector: (state: Record<string, unknown>) => unknown) => {
const mockState = { dataset: pipelineId ? { pipeline_id: pipelineId } : null }
return selector(mockState)
})
@ -327,7 +327,7 @@ describe('RagPipeline', () => {
graph: {
nodes: [],
edges: [],
viewport: undefined as any,
viewport: undefined as never,
},
})
mockUsePipelineInit.mockReturnValue({ data: mockData, isLoading: false })
@ -342,7 +342,7 @@ describe('RagPipeline', () => {
graph: {
nodes: [],
edges: [],
viewport: null as any,
viewport: null as never,
},
})
mockUsePipelineInit.mockReturnValue({ data: mockData, isLoading: false })
@ -438,7 +438,7 @@ describe('processNodesWithoutDataSource utility integration', () => {
const mockData = createMockWorkflowData()
mockUsePipelineInit.mockReturnValue({ data: mockData, isLoading: false })
mockProcessNodesWithoutDataSource.mockReturnValue({
nodes: [{ id: 'processed-node', type: 'custom', data: { type: BlockEnum.Start, title: 'Processed', desc: '' }, position: { x: 0, y: 0 } }] as any,
nodes: [{ id: 'processed-node', type: 'custom', data: { type: BlockEnum.Start, title: 'Processed', desc: '' }, position: { x: 0, y: 0 } }] as unknown as ReturnType<typeof processNodesWithoutDataSource>['nodes'],
viewport: { x: 0, y: 0, zoom: 2 },
})
@ -510,13 +510,13 @@ describe('Error Handling', () => {
it('should throw when graph nodes is null', () => {
const mockData = {
graph: {
nodes: null as any,
edges: null as any,
nodes: null,
edges: null,
viewport: { x: 0, y: 0, zoom: 1 },
},
hash: 'test',
updated_at: 123,
} as FetchWorkflowDraftResponse
} as unknown as FetchWorkflowDraftResponse
mockUsePipelineInit.mockReturnValue({ data: mockData, isLoading: false })

View File

@ -1,6 +1,10 @@
/* eslint-disable ts/no-explicit-any */
import type { InputFieldEditorProps } from '../components/panel/input-field/editor'
import type { RagPipelineSliceShape } from './index'
import type { DataSourceItem } from '@/app/components/workflow/block-selector/types'
import type { RAGPipelineVariables } from '@/models/pipeline'
import { describe, expect, it, vi } from 'vitest'
import { PipelineInputVarType } from '@/models/pipeline'
import { createRagPipelineSliceSlice } from './index'
// Mock the transformDataSourceToTool function
@ -11,60 +15,70 @@ vi.mock('@/app/components/workflow/block-selector/utils', () => ({
}),
}))
// Type-safe stubs for unused Zustand StateCreator params
type SliceCreatorParams = Parameters<typeof createRagPipelineSliceSlice>
const unusedGet = vi.fn() as unknown as SliceCreatorParams[1]
const unusedApi = vi.fn() as unknown as SliceCreatorParams[2]
// Helper to create a slice with a given mockSet
function createSlice(mockSet = vi.fn()) {
return createRagPipelineSliceSlice(mockSet as unknown as SliceCreatorParams[0], unusedGet, unusedApi)
}
describe('createRagPipelineSliceSlice', () => {
const mockSet = vi.fn()
describe('initial state', () => {
it('should have empty pipelineId', () => {
const slice = createRagPipelineSliceSlice(mockSet, vi.fn() as any, vi.fn() as any)
const slice = createSlice(mockSet)
expect(slice.pipelineId).toBe('')
})
it('should have empty knowledgeName', () => {
const slice = createRagPipelineSliceSlice(mockSet, vi.fn() as any, vi.fn() as any)
const slice = createSlice(mockSet)
expect(slice.knowledgeName).toBe('')
})
it('should have showInputFieldPanel as false', () => {
const slice = createRagPipelineSliceSlice(mockSet, vi.fn() as any, vi.fn() as any)
const slice = createSlice(mockSet)
expect(slice.showInputFieldPanel).toBe(false)
})
it('should have showInputFieldPreviewPanel as false', () => {
const slice = createRagPipelineSliceSlice(mockSet, vi.fn() as any, vi.fn() as any)
const slice = createSlice(mockSet)
expect(slice.showInputFieldPreviewPanel).toBe(false)
})
it('should have inputFieldEditPanelProps as null', () => {
const slice = createRagPipelineSliceSlice(mockSet, vi.fn() as any, vi.fn() as any)
const slice = createSlice(mockSet)
expect(slice.inputFieldEditPanelProps).toBeNull()
})
it('should have empty nodesDefaultConfigs', () => {
const slice = createRagPipelineSliceSlice(mockSet, vi.fn() as any, vi.fn() as any)
const slice = createSlice(mockSet)
expect(slice.nodesDefaultConfigs).toEqual({})
})
it('should have empty ragPipelineVariables', () => {
const slice = createRagPipelineSliceSlice(mockSet, vi.fn() as any, vi.fn() as any)
const slice = createSlice(mockSet)
expect(slice.ragPipelineVariables).toEqual([])
})
it('should have empty dataSourceList', () => {
const slice = createRagPipelineSliceSlice(mockSet, vi.fn() as any, vi.fn() as any)
const slice = createSlice(mockSet)
expect(slice.dataSourceList).toEqual([])
})
it('should have isPreparingDataSource as false', () => {
const slice = createRagPipelineSliceSlice(mockSet, vi.fn() as any, vi.fn() as any)
const slice = createSlice(mockSet)
expect(slice.isPreparingDataSource).toBe(false)
})
@ -72,25 +86,25 @@ describe('createRagPipelineSliceSlice', () => {
describe('setShowInputFieldPanel', () => {
it('should call set with showInputFieldPanel true', () => {
const slice = createRagPipelineSliceSlice(mockSet, vi.fn() as any, vi.fn() as any)
const slice = createSlice(mockSet)
slice.setShowInputFieldPanel(true)
expect(mockSet).toHaveBeenCalledWith(expect.any(Function))
// Get the setter function and execute it
const setterFn = mockSet.mock.calls[0][0]
const setterFn = mockSet.mock.calls[0][0] as () => Partial<RagPipelineSliceShape>
const result = setterFn()
expect(result).toEqual({ showInputFieldPanel: true })
})
it('should call set with showInputFieldPanel false', () => {
mockSet.mockClear()
const slice = createRagPipelineSliceSlice(mockSet, vi.fn() as any, vi.fn() as any)
const slice = createSlice(mockSet)
slice.setShowInputFieldPanel(false)
const setterFn = mockSet.mock.calls[0][0]
const setterFn = mockSet.mock.calls[0][0] as () => Partial<RagPipelineSliceShape>
const result = setterFn()
expect(result).toEqual({ showInputFieldPanel: false })
})
@ -99,22 +113,22 @@ describe('createRagPipelineSliceSlice', () => {
describe('setShowInputFieldPreviewPanel', () => {
it('should call set with showInputFieldPreviewPanel true', () => {
mockSet.mockClear()
const slice = createRagPipelineSliceSlice(mockSet, vi.fn() as any, vi.fn() as any)
const slice = createSlice(mockSet)
slice.setShowInputFieldPreviewPanel(true)
const setterFn = mockSet.mock.calls[0][0]
const setterFn = mockSet.mock.calls[0][0] as () => Partial<RagPipelineSliceShape>
const result = setterFn()
expect(result).toEqual({ showInputFieldPreviewPanel: true })
})
it('should call set with showInputFieldPreviewPanel false', () => {
mockSet.mockClear()
const slice = createRagPipelineSliceSlice(mockSet, vi.fn() as any, vi.fn() as any)
const slice = createSlice(mockSet)
slice.setShowInputFieldPreviewPanel(false)
const setterFn = mockSet.mock.calls[0][0]
const setterFn = mockSet.mock.calls[0][0] as () => Partial<RagPipelineSliceShape>
const result = setterFn()
expect(result).toEqual({ showInputFieldPreviewPanel: false })
})
@ -123,23 +137,23 @@ describe('createRagPipelineSliceSlice', () => {
describe('setInputFieldEditPanelProps', () => {
it('should call set with inputFieldEditPanelProps object', () => {
mockSet.mockClear()
const slice = createRagPipelineSliceSlice(mockSet, vi.fn() as any, vi.fn() as any)
const props = { type: 'create' as const }
const slice = createSlice(mockSet)
const props = { onClose: vi.fn(), onSubmit: vi.fn() } as unknown as InputFieldEditorProps
slice.setInputFieldEditPanelProps(props as any)
slice.setInputFieldEditPanelProps(props)
const setterFn = mockSet.mock.calls[0][0]
const setterFn = mockSet.mock.calls[0][0] as () => Partial<RagPipelineSliceShape>
const result = setterFn()
expect(result).toEqual({ inputFieldEditPanelProps: props })
})
it('should call set with inputFieldEditPanelProps null', () => {
mockSet.mockClear()
const slice = createRagPipelineSliceSlice(mockSet, vi.fn() as any, vi.fn() as any)
const slice = createSlice(mockSet)
slice.setInputFieldEditPanelProps(null)
const setterFn = mockSet.mock.calls[0][0]
const setterFn = mockSet.mock.calls[0][0] as () => Partial<RagPipelineSliceShape>
const result = setterFn()
expect(result).toEqual({ inputFieldEditPanelProps: null })
})
@ -148,23 +162,23 @@ describe('createRagPipelineSliceSlice', () => {
describe('setNodesDefaultConfigs', () => {
it('should call set with nodesDefaultConfigs', () => {
mockSet.mockClear()
const slice = createRagPipelineSliceSlice(mockSet, vi.fn() as any, vi.fn() as any)
const configs = { node1: { key: 'value' } }
const slice = createSlice(mockSet)
const configs: Record<string, unknown> = { node1: { key: 'value' } }
slice.setNodesDefaultConfigs(configs)
const setterFn = mockSet.mock.calls[0][0]
const setterFn = mockSet.mock.calls[0][0] as () => Partial<RagPipelineSliceShape>
const result = setterFn()
expect(result).toEqual({ nodesDefaultConfigs: configs })
})
it('should call set with empty nodesDefaultConfigs', () => {
mockSet.mockClear()
const slice = createRagPipelineSliceSlice(mockSet, vi.fn() as any, vi.fn() as any)
const slice = createSlice(mockSet)
slice.setNodesDefaultConfigs({})
const setterFn = mockSet.mock.calls[0][0]
const setterFn = mockSet.mock.calls[0][0] as () => Partial<RagPipelineSliceShape>
const result = setterFn()
expect(result).toEqual({ nodesDefaultConfigs: {} })
})
@ -173,25 +187,25 @@ describe('createRagPipelineSliceSlice', () => {
describe('setRagPipelineVariables', () => {
it('should call set with ragPipelineVariables', () => {
mockSet.mockClear()
const slice = createRagPipelineSliceSlice(mockSet, vi.fn() as any, vi.fn() as any)
const variables = [
{ type: 'text-input', variable: 'var1', label: 'Var 1', required: true },
const slice = createSlice(mockSet)
const variables: RAGPipelineVariables = [
{ type: PipelineInputVarType.textInput, variable: 'var1', label: 'Var 1', required: true, belong_to_node_id: 'node-1' },
]
slice.setRagPipelineVariables(variables as any)
slice.setRagPipelineVariables(variables)
const setterFn = mockSet.mock.calls[0][0]
const setterFn = mockSet.mock.calls[0][0] as () => Partial<RagPipelineSliceShape>
const result = setterFn()
expect(result).toEqual({ ragPipelineVariables: variables })
})
it('should call set with empty ragPipelineVariables', () => {
mockSet.mockClear()
const slice = createRagPipelineSliceSlice(mockSet, vi.fn() as any, vi.fn() as any)
const slice = createSlice(mockSet)
slice.setRagPipelineVariables([])
const setterFn = mockSet.mock.calls[0][0]
const setterFn = mockSet.mock.calls[0][0] as () => Partial<RagPipelineSliceShape>
const result = setterFn()
expect(result).toEqual({ ragPipelineVariables: [] })
})
@ -200,7 +214,7 @@ describe('createRagPipelineSliceSlice', () => {
describe('setDataSourceList', () => {
it('should transform and set dataSourceList', () => {
mockSet.mockClear()
const slice = createRagPipelineSliceSlice(mockSet, vi.fn() as any, vi.fn() as any)
const slice = createSlice(mockSet)
const dataSourceList: DataSourceItem[] = [
{ name: 'source1', key: 'key1' } as unknown as DataSourceItem,
{ name: 'source2', key: 'key2' } as unknown as DataSourceItem,
@ -208,20 +222,20 @@ describe('createRagPipelineSliceSlice', () => {
slice.setDataSourceList(dataSourceList)
const setterFn = mockSet.mock.calls[0][0]
const setterFn = mockSet.mock.calls[0][0] as () => Partial<RagPipelineSliceShape>
const result = setterFn()
expect(result.dataSourceList).toHaveLength(2)
expect(result.dataSourceList[0]).toEqual({ name: 'source1', key: 'key1', transformed: true })
expect(result.dataSourceList[1]).toEqual({ name: 'source2', key: 'key2', transformed: true })
expect(result.dataSourceList![0]).toEqual({ name: 'source1', key: 'key1', transformed: true })
expect(result.dataSourceList![1]).toEqual({ name: 'source2', key: 'key2', transformed: true })
})
it('should set empty dataSourceList', () => {
mockSet.mockClear()
const slice = createRagPipelineSliceSlice(mockSet, vi.fn() as any, vi.fn() as any)
const slice = createSlice(mockSet)
slice.setDataSourceList([])
const setterFn = mockSet.mock.calls[0][0]
const setterFn = mockSet.mock.calls[0][0] as () => Partial<RagPipelineSliceShape>
const result = setterFn()
expect(result.dataSourceList).toEqual([])
})
@ -230,22 +244,22 @@ describe('createRagPipelineSliceSlice', () => {
describe('setIsPreparingDataSource', () => {
it('should call set with isPreparingDataSource true', () => {
mockSet.mockClear()
const slice = createRagPipelineSliceSlice(mockSet, vi.fn() as any, vi.fn() as any)
const slice = createSlice(mockSet)
slice.setIsPreparingDataSource(true)
const setterFn = mockSet.mock.calls[0][0]
const setterFn = mockSet.mock.calls[0][0] as () => Partial<RagPipelineSliceShape>
const result = setterFn()
expect(result).toEqual({ isPreparingDataSource: true })
})
it('should call set with isPreparingDataSource false', () => {
mockSet.mockClear()
const slice = createRagPipelineSliceSlice(mockSet, vi.fn() as any, vi.fn() as any)
const slice = createSlice(mockSet)
slice.setIsPreparingDataSource(false)
const setterFn = mockSet.mock.calls[0][0]
const setterFn = mockSet.mock.calls[0][0] as () => Partial<RagPipelineSliceShape>
const result = setterFn()
expect(result).toEqual({ isPreparingDataSource: false })
})
@ -254,7 +268,7 @@ describe('createRagPipelineSliceSlice', () => {
describe('RagPipelineSliceShape type', () => {
it('should define all required properties', () => {
const slice = createRagPipelineSliceSlice(vi.fn(), vi.fn() as any, vi.fn() as any)
const slice = createSlice()
// Check all properties exist
expect(slice).toHaveProperty('pipelineId')
@ -276,7 +290,7 @@ describe('RagPipelineSliceShape type', () => {
})
it('should have all setters as functions', () => {
const slice = createRagPipelineSliceSlice(vi.fn(), vi.fn() as any, vi.fn() as any)
const slice = createSlice()
expect(typeof slice.setShowInputFieldPanel).toBe('function')
expect(typeof slice.setShowInputFieldPreviewPanel).toBe('function')

View File

@ -5525,11 +5525,6 @@
"count": 3
}
},
"app/components/rag-pipeline/components/panel/input-field/field-list/index.spec.tsx": {
"ts/no-explicit-any": {
"count": 1
}
},
"app/components/rag-pipeline/components/panel/input-field/hooks.ts": {
"react-hooks-extra/no-direct-set-state-in-use-effect": {
"count": 1
@ -5689,11 +5684,6 @@
"count": 1
}
},
"app/components/rag-pipeline/index.spec.tsx": {
"ts/no-explicit-any": {
"count": 8
}
},
"app/components/rag-pipeline/store/index.ts": {
"ts/no-explicit-any": {
"count": 2