mirror of
https://github.com/langgenius/dify.git
synced 2025-12-25 01:00:42 -05:00
chore: tests for configuration (#29870)
This commit is contained in:
@@ -0,0 +1,227 @@
|
||||
import React from 'react'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import ConfigVision from './index'
|
||||
import ParamConfig from './param-config'
|
||||
import ParamConfigContent from './param-config-content'
|
||||
import type { FeatureStoreState } from '@/app/components/base/features/store'
|
||||
import type { FileUpload } from '@/app/components/base/features/types'
|
||||
import { Resolution, TransferMethod } from '@/types/app'
|
||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
|
||||
const mockUseContext = jest.fn()
|
||||
jest.mock('use-context-selector', () => {
|
||||
const actual = jest.requireActual('use-context-selector')
|
||||
return {
|
||||
...actual,
|
||||
useContext: (context: unknown) => mockUseContext(context),
|
||||
}
|
||||
})
|
||||
|
||||
const mockUseFeatures = jest.fn()
|
||||
const mockUseFeaturesStore = jest.fn()
|
||||
jest.mock('@/app/components/base/features/hooks', () => ({
|
||||
useFeatures: (selector: (state: FeatureStoreState) => any) => mockUseFeatures(selector),
|
||||
useFeaturesStore: () => mockUseFeaturesStore(),
|
||||
}))
|
||||
|
||||
const defaultFile: FileUpload = {
|
||||
enabled: false,
|
||||
allowed_file_types: [],
|
||||
allowed_file_upload_methods: [TransferMethod.local_file, TransferMethod.remote_url],
|
||||
number_limits: 3,
|
||||
image: {
|
||||
enabled: false,
|
||||
detail: Resolution.low,
|
||||
number_limits: 3,
|
||||
transfer_methods: [TransferMethod.local_file, TransferMethod.remote_url],
|
||||
},
|
||||
}
|
||||
|
||||
let featureStoreState: FeatureStoreState
|
||||
let setFeaturesMock: jest.Mock
|
||||
|
||||
const setupFeatureStore = (fileOverrides: Partial<FileUpload> = {}) => {
|
||||
const mergedFile: FileUpload = {
|
||||
...defaultFile,
|
||||
...fileOverrides,
|
||||
image: {
|
||||
...defaultFile.image,
|
||||
...fileOverrides.image,
|
||||
},
|
||||
}
|
||||
featureStoreState = {
|
||||
features: {
|
||||
file: mergedFile,
|
||||
},
|
||||
setFeatures: jest.fn(),
|
||||
showFeaturesModal: false,
|
||||
setShowFeaturesModal: jest.fn(),
|
||||
}
|
||||
setFeaturesMock = featureStoreState.setFeatures as jest.Mock
|
||||
mockUseFeaturesStore.mockReturnValue({
|
||||
getState: () => featureStoreState,
|
||||
})
|
||||
mockUseFeatures.mockImplementation(selector => selector(featureStoreState))
|
||||
}
|
||||
|
||||
const getLatestFileConfig = () => {
|
||||
expect(setFeaturesMock).toHaveBeenCalled()
|
||||
const latestFeatures = setFeaturesMock.mock.calls[setFeaturesMock.mock.calls.length - 1][0] as { file: FileUpload }
|
||||
return latestFeatures.file
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
mockUseContext.mockReturnValue({
|
||||
isShowVisionConfig: true,
|
||||
isAllowVideoUpload: false,
|
||||
})
|
||||
setupFeatureStore()
|
||||
})
|
||||
|
||||
// ConfigVision handles toggling file upload types + visibility rules.
|
||||
describe('ConfigVision', () => {
|
||||
it('should not render when vision configuration is hidden', () => {
|
||||
mockUseContext.mockReturnValue({
|
||||
isShowVisionConfig: false,
|
||||
isAllowVideoUpload: false,
|
||||
})
|
||||
|
||||
render(<ConfigVision />)
|
||||
|
||||
expect(screen.queryByText('appDebug.vision.name')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show the toggle and parameter controls when visible', () => {
|
||||
render(<ConfigVision />)
|
||||
|
||||
expect(screen.getByText('appDebug.vision.name')).toBeInTheDocument()
|
||||
expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'false')
|
||||
})
|
||||
|
||||
it('should enable both image and video uploads when toggled on with video support', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockUseContext.mockReturnValue({
|
||||
isShowVisionConfig: true,
|
||||
isAllowVideoUpload: true,
|
||||
})
|
||||
setupFeatureStore({
|
||||
allowed_file_types: [],
|
||||
})
|
||||
|
||||
render(<ConfigVision />)
|
||||
await user.click(screen.getByRole('switch'))
|
||||
|
||||
const updatedFile = getLatestFileConfig()
|
||||
expect(updatedFile.allowed_file_types).toEqual([SupportUploadFileTypes.image, SupportUploadFileTypes.video])
|
||||
expect(updatedFile.image?.enabled).toBe(true)
|
||||
expect(updatedFile.enabled).toBe(true)
|
||||
})
|
||||
|
||||
it('should disable image and video uploads when toggled off and no other types remain', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockUseContext.mockReturnValue({
|
||||
isShowVisionConfig: true,
|
||||
isAllowVideoUpload: true,
|
||||
})
|
||||
setupFeatureStore({
|
||||
allowed_file_types: [SupportUploadFileTypes.image, SupportUploadFileTypes.video],
|
||||
enabled: true,
|
||||
image: {
|
||||
enabled: true,
|
||||
},
|
||||
})
|
||||
|
||||
render(<ConfigVision />)
|
||||
await user.click(screen.getByRole('switch'))
|
||||
|
||||
const updatedFile = getLatestFileConfig()
|
||||
expect(updatedFile.allowed_file_types).toEqual([])
|
||||
expect(updatedFile.enabled).toBe(false)
|
||||
expect(updatedFile.image?.enabled).toBe(false)
|
||||
})
|
||||
|
||||
it('should keep file uploads enabled when other file types remain after disabling vision', async () => {
|
||||
const user = userEvent.setup()
|
||||
mockUseContext.mockReturnValue({
|
||||
isShowVisionConfig: true,
|
||||
isAllowVideoUpload: false,
|
||||
})
|
||||
setupFeatureStore({
|
||||
allowed_file_types: [SupportUploadFileTypes.image, SupportUploadFileTypes.document],
|
||||
enabled: true,
|
||||
image: { enabled: true },
|
||||
})
|
||||
|
||||
render(<ConfigVision />)
|
||||
await user.click(screen.getByRole('switch'))
|
||||
|
||||
const updatedFile = getLatestFileConfig()
|
||||
expect(updatedFile.allowed_file_types).toEqual([SupportUploadFileTypes.document])
|
||||
expect(updatedFile.enabled).toBe(true)
|
||||
expect(updatedFile.image?.enabled).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
// ParamConfig exposes ParamConfigContent via an inline trigger.
|
||||
describe('ParamConfig', () => {
|
||||
it('should toggle parameter panel when clicking the settings button', async () => {
|
||||
setupFeatureStore()
|
||||
const user = userEvent.setup()
|
||||
|
||||
render(<ParamConfig />)
|
||||
|
||||
expect(screen.queryByText('appDebug.vision.visionSettings.title')).not.toBeInTheDocument()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'appDebug.voice.settings' }))
|
||||
|
||||
expect(await screen.findByText('appDebug.vision.visionSettings.title')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
// ParamConfigContent manages resolution, upload source, and count limits.
|
||||
describe('ParamConfigContent', () => {
|
||||
it('should set resolution to high when the corresponding option is selected', async () => {
|
||||
const user = userEvent.setup()
|
||||
setupFeatureStore({
|
||||
image: { detail: Resolution.low },
|
||||
})
|
||||
|
||||
render(<ParamConfigContent />)
|
||||
|
||||
await user.click(screen.getByText('appDebug.vision.visionSettings.high'))
|
||||
|
||||
const updatedFile = getLatestFileConfig()
|
||||
expect(updatedFile.image?.detail).toBe(Resolution.high)
|
||||
})
|
||||
|
||||
it('should switch upload method to local only', async () => {
|
||||
const user = userEvent.setup()
|
||||
setupFeatureStore({
|
||||
allowed_file_upload_methods: [TransferMethod.local_file, TransferMethod.remote_url],
|
||||
})
|
||||
|
||||
render(<ParamConfigContent />)
|
||||
|
||||
await user.click(screen.getByText('appDebug.vision.visionSettings.localUpload'))
|
||||
|
||||
const updatedFile = getLatestFileConfig()
|
||||
expect(updatedFile.allowed_file_upload_methods).toEqual([TransferMethod.local_file])
|
||||
expect(updatedFile.image?.transfer_methods).toEqual([TransferMethod.local_file])
|
||||
})
|
||||
|
||||
it('should update upload limit value when input changes', async () => {
|
||||
setupFeatureStore({
|
||||
number_limits: 2,
|
||||
})
|
||||
|
||||
render(<ParamConfigContent />)
|
||||
const input = screen.getByRole('spinbutton') as HTMLInputElement
|
||||
fireEvent.change(input, { target: { value: '4' } })
|
||||
|
||||
const updatedFile = getLatestFileConfig()
|
||||
expect(updatedFile.number_limits).toBe(4)
|
||||
expect(updatedFile.image?.number_limits).toBe(4)
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,100 @@
|
||||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import AgentSettingButton from './agent-setting-button'
|
||||
import type { AgentConfig } from '@/models/debug'
|
||||
import { AgentStrategy } from '@/types/app'
|
||||
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
let latestAgentSettingProps: any
|
||||
jest.mock('./agent/agent-setting', () => ({
|
||||
__esModule: true,
|
||||
default: (props: any) => {
|
||||
latestAgentSettingProps = props
|
||||
return (
|
||||
<div data-testid="agent-setting">
|
||||
<button onClick={() => props.onSave({ ...props.payload, max_iteration: 9 })}>
|
||||
save-agent
|
||||
</button>
|
||||
<button onClick={props.onCancel}>
|
||||
cancel-agent
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
}))
|
||||
|
||||
const createAgentConfig = (overrides: Partial<AgentConfig> = {}): AgentConfig => ({
|
||||
enabled: true,
|
||||
strategy: AgentStrategy.react,
|
||||
max_iteration: 3,
|
||||
tools: [],
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const setup = (overrides: Partial<React.ComponentProps<typeof AgentSettingButton>> = {}) => {
|
||||
const props: React.ComponentProps<typeof AgentSettingButton> = {
|
||||
isFunctionCall: false,
|
||||
isChatModel: true,
|
||||
onAgentSettingChange: jest.fn(),
|
||||
agentConfig: createAgentConfig(),
|
||||
...overrides,
|
||||
}
|
||||
|
||||
const user = userEvent.setup()
|
||||
render(<AgentSettingButton {...props} />)
|
||||
return { props, user }
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
latestAgentSettingProps = undefined
|
||||
})
|
||||
|
||||
describe('AgentSettingButton', () => {
|
||||
it('should render button label from translation key', () => {
|
||||
setup()
|
||||
|
||||
expect(screen.getByRole('button', { name: 'appDebug.agent.setting.name' })).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should open AgentSetting with the provided configuration when clicked', async () => {
|
||||
const { user, props } = setup({ isFunctionCall: true, isChatModel: false })
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'appDebug.agent.setting.name' }))
|
||||
|
||||
expect(screen.getByTestId('agent-setting')).toBeInTheDocument()
|
||||
expect(latestAgentSettingProps.isFunctionCall).toBe(true)
|
||||
expect(latestAgentSettingProps.isChatModel).toBe(false)
|
||||
expect(latestAgentSettingProps.payload).toEqual(props.agentConfig)
|
||||
})
|
||||
|
||||
it('should call onAgentSettingChange and close when AgentSetting saves', async () => {
|
||||
const { user, props } = setup()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'appDebug.agent.setting.name' }))
|
||||
await user.click(screen.getByText('save-agent'))
|
||||
|
||||
expect(props.onAgentSettingChange).toHaveBeenCalledTimes(1)
|
||||
expect(props.onAgentSettingChange).toHaveBeenCalledWith({
|
||||
...props.agentConfig,
|
||||
max_iteration: 9,
|
||||
})
|
||||
expect(screen.queryByTestId('agent-setting')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should close AgentSetting without saving when cancel is triggered', async () => {
|
||||
const { user, props } = setup()
|
||||
|
||||
await user.click(screen.getByRole('button', { name: 'appDebug.agent.setting.name' }))
|
||||
await user.click(screen.getByText('cancel-agent'))
|
||||
|
||||
expect(props.onAgentSettingChange).not.toHaveBeenCalled()
|
||||
expect(screen.queryByTestId('agent-setting')).not.toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,123 @@
|
||||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import ConfigAudio from './config-audio'
|
||||
import type { FeatureStoreState } from '@/app/components/base/features/store'
|
||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
|
||||
const mockUseContext = jest.fn()
|
||||
jest.mock('use-context-selector', () => {
|
||||
const actual = jest.requireActual('use-context-selector')
|
||||
return {
|
||||
...actual,
|
||||
useContext: (context: unknown) => mockUseContext(context),
|
||||
}
|
||||
})
|
||||
|
||||
jest.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({
|
||||
t: (key: string) => key,
|
||||
}),
|
||||
}))
|
||||
|
||||
const mockUseFeatures = jest.fn()
|
||||
const mockUseFeaturesStore = jest.fn()
|
||||
jest.mock('@/app/components/base/features/hooks', () => ({
|
||||
useFeatures: (selector: (state: FeatureStoreState) => any) => mockUseFeatures(selector),
|
||||
useFeaturesStore: () => mockUseFeaturesStore(),
|
||||
}))
|
||||
|
||||
type SetupOptions = {
|
||||
isVisible?: boolean
|
||||
allowedTypes?: SupportUploadFileTypes[]
|
||||
}
|
||||
|
||||
let mockFeatureStoreState: FeatureStoreState
|
||||
let mockSetFeatures: jest.Mock
|
||||
const mockStore = {
|
||||
getState: jest.fn<FeatureStoreState, []>(() => mockFeatureStoreState),
|
||||
}
|
||||
|
||||
const setupFeatureStore = (allowedTypes: SupportUploadFileTypes[] = []) => {
|
||||
mockSetFeatures = jest.fn()
|
||||
mockFeatureStoreState = {
|
||||
features: {
|
||||
file: {
|
||||
allowed_file_types: allowedTypes,
|
||||
enabled: allowedTypes.length > 0,
|
||||
},
|
||||
},
|
||||
setFeatures: mockSetFeatures,
|
||||
showFeaturesModal: false,
|
||||
setShowFeaturesModal: jest.fn(),
|
||||
}
|
||||
mockStore.getState.mockImplementation(() => mockFeatureStoreState)
|
||||
mockUseFeaturesStore.mockReturnValue(mockStore)
|
||||
mockUseFeatures.mockImplementation(selector => selector(mockFeatureStoreState))
|
||||
}
|
||||
|
||||
const renderConfigAudio = (options: SetupOptions = {}) => {
|
||||
const {
|
||||
isVisible = true,
|
||||
allowedTypes = [],
|
||||
} = options
|
||||
setupFeatureStore(allowedTypes)
|
||||
mockUseContext.mockReturnValue({
|
||||
isShowAudioConfig: isVisible,
|
||||
})
|
||||
const user = userEvent.setup()
|
||||
render(<ConfigAudio />)
|
||||
return {
|
||||
user,
|
||||
setFeatures: mockSetFeatures,
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('ConfigAudio', () => {
|
||||
it('should not render when the audio configuration is hidden', () => {
|
||||
renderConfigAudio({ isVisible: false })
|
||||
|
||||
expect(screen.queryByText('appDebug.feature.audioUpload.title')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display the audio toggle state based on feature store data', () => {
|
||||
renderConfigAudio({ allowedTypes: [SupportUploadFileTypes.audio] })
|
||||
|
||||
expect(screen.getByText('appDebug.feature.audioUpload.title')).toBeInTheDocument()
|
||||
expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
|
||||
it('should enable audio uploads when toggled on', async () => {
|
||||
const { user, setFeatures } = renderConfigAudio()
|
||||
const toggle = screen.getByRole('switch')
|
||||
|
||||
expect(toggle).toHaveAttribute('aria-checked', 'false')
|
||||
await user.click(toggle)
|
||||
|
||||
expect(setFeatures).toHaveBeenCalledWith(expect.objectContaining({
|
||||
file: expect.objectContaining({
|
||||
allowed_file_types: [SupportUploadFileTypes.audio],
|
||||
enabled: true,
|
||||
}),
|
||||
}))
|
||||
})
|
||||
|
||||
it('should disable audio uploads and turn off file feature when last type is removed', async () => {
|
||||
const { user, setFeatures } = renderConfigAudio({ allowedTypes: [SupportUploadFileTypes.audio] })
|
||||
const toggle = screen.getByRole('switch')
|
||||
|
||||
expect(toggle).toHaveAttribute('aria-checked', 'true')
|
||||
await user.click(toggle)
|
||||
|
||||
expect(setFeatures).toHaveBeenCalledWith(expect.objectContaining({
|
||||
file: expect.objectContaining({
|
||||
allowed_file_types: [],
|
||||
enabled: false,
|
||||
}),
|
||||
}))
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,119 @@
|
||||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import ConfigDocument from './config-document'
|
||||
import type { FeatureStoreState } from '@/app/components/base/features/store'
|
||||
import { SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
|
||||
const mockUseContext = jest.fn()
|
||||
jest.mock('use-context-selector', () => {
|
||||
const actual = jest.requireActual('use-context-selector')
|
||||
return {
|
||||
...actual,
|
||||
useContext: (context: unknown) => mockUseContext(context),
|
||||
}
|
||||
})
|
||||
|
||||
const mockUseFeatures = jest.fn()
|
||||
const mockUseFeaturesStore = jest.fn()
|
||||
jest.mock('@/app/components/base/features/hooks', () => ({
|
||||
useFeatures: (selector: (state: FeatureStoreState) => any) => mockUseFeatures(selector),
|
||||
useFeaturesStore: () => mockUseFeaturesStore(),
|
||||
}))
|
||||
|
||||
type SetupOptions = {
|
||||
isVisible?: boolean
|
||||
allowedTypes?: SupportUploadFileTypes[]
|
||||
}
|
||||
|
||||
let mockFeatureStoreState: FeatureStoreState
|
||||
let mockSetFeatures: jest.Mock
|
||||
const mockStore = {
|
||||
getState: jest.fn<FeatureStoreState, []>(() => mockFeatureStoreState),
|
||||
}
|
||||
|
||||
const setupFeatureStore = (allowedTypes: SupportUploadFileTypes[] = []) => {
|
||||
mockSetFeatures = jest.fn()
|
||||
mockFeatureStoreState = {
|
||||
features: {
|
||||
file: {
|
||||
allowed_file_types: allowedTypes,
|
||||
enabled: allowedTypes.length > 0,
|
||||
},
|
||||
},
|
||||
setFeatures: mockSetFeatures,
|
||||
showFeaturesModal: false,
|
||||
setShowFeaturesModal: jest.fn(),
|
||||
}
|
||||
mockStore.getState.mockImplementation(() => mockFeatureStoreState)
|
||||
mockUseFeaturesStore.mockReturnValue(mockStore)
|
||||
mockUseFeatures.mockImplementation(selector => selector(mockFeatureStoreState))
|
||||
}
|
||||
|
||||
const renderConfigDocument = (options: SetupOptions = {}) => {
|
||||
const {
|
||||
isVisible = true,
|
||||
allowedTypes = [],
|
||||
} = options
|
||||
setupFeatureStore(allowedTypes)
|
||||
mockUseContext.mockReturnValue({
|
||||
isShowDocumentConfig: isVisible,
|
||||
})
|
||||
const user = userEvent.setup()
|
||||
render(<ConfigDocument />)
|
||||
return {
|
||||
user,
|
||||
setFeatures: mockSetFeatures,
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('ConfigDocument', () => {
|
||||
it('should not render when the document configuration is hidden', () => {
|
||||
renderConfigDocument({ isVisible: false })
|
||||
|
||||
expect(screen.queryByText('appDebug.feature.documentUpload.title')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show document toggle badge when configuration is visible', () => {
|
||||
renderConfigDocument({ allowedTypes: [SupportUploadFileTypes.document] })
|
||||
|
||||
expect(screen.getByText('appDebug.feature.documentUpload.title')).toBeInTheDocument()
|
||||
expect(screen.getByRole('switch')).toHaveAttribute('aria-checked', 'true')
|
||||
})
|
||||
|
||||
it('should add document type to allowed list when toggled on', async () => {
|
||||
const { user, setFeatures } = renderConfigDocument({ allowedTypes: [SupportUploadFileTypes.audio] })
|
||||
const toggle = screen.getByRole('switch')
|
||||
|
||||
expect(toggle).toHaveAttribute('aria-checked', 'false')
|
||||
await user.click(toggle)
|
||||
|
||||
expect(setFeatures).toHaveBeenCalledWith(expect.objectContaining({
|
||||
file: expect.objectContaining({
|
||||
allowed_file_types: [SupportUploadFileTypes.audio, SupportUploadFileTypes.document],
|
||||
enabled: true,
|
||||
}),
|
||||
}))
|
||||
})
|
||||
|
||||
it('should remove document type but keep file feature enabled when other types remain', async () => {
|
||||
const { user, setFeatures } = renderConfigDocument({
|
||||
allowedTypes: [SupportUploadFileTypes.document, SupportUploadFileTypes.audio],
|
||||
})
|
||||
const toggle = screen.getByRole('switch')
|
||||
|
||||
expect(toggle).toHaveAttribute('aria-checked', 'true')
|
||||
await user.click(toggle)
|
||||
|
||||
expect(setFeatures).toHaveBeenCalledWith(expect.objectContaining({
|
||||
file: expect.objectContaining({
|
||||
allowed_file_types: [SupportUploadFileTypes.audio],
|
||||
enabled: true,
|
||||
}),
|
||||
}))
|
||||
})
|
||||
})
|
||||
254
web/app/components/app/configuration/config/index.spec.tsx
Normal file
254
web/app/components/app/configuration/config/index.spec.tsx
Normal file
@@ -0,0 +1,254 @@
|
||||
import React from 'react'
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import Config from './index'
|
||||
import type { ModelConfig, PromptVariable } from '@/models/debug'
|
||||
import * as useContextSelector from 'use-context-selector'
|
||||
import type { ToolItem } from '@/types/app'
|
||||
import { AgentStrategy, AppModeEnum, ModelModeType } from '@/types/app'
|
||||
|
||||
jest.mock('use-context-selector', () => {
|
||||
const actual = jest.requireActual('use-context-selector')
|
||||
return {
|
||||
...actual,
|
||||
useContext: jest.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
const mockFormattingDispatcher = jest.fn()
|
||||
jest.mock('../debug/hooks', () => ({
|
||||
__esModule: true,
|
||||
useFormattingChangedDispatcher: () => mockFormattingDispatcher,
|
||||
}))
|
||||
|
||||
let latestConfigPromptProps: any
|
||||
jest.mock('@/app/components/app/configuration/config-prompt', () => ({
|
||||
__esModule: true,
|
||||
default: (props: any) => {
|
||||
latestConfigPromptProps = props
|
||||
return <div data-testid="config-prompt" />
|
||||
},
|
||||
}))
|
||||
|
||||
let latestConfigVarProps: any
|
||||
jest.mock('@/app/components/app/configuration/config-var', () => ({
|
||||
__esModule: true,
|
||||
default: (props: any) => {
|
||||
latestConfigVarProps = props
|
||||
return <div data-testid="config-var" />
|
||||
},
|
||||
}))
|
||||
|
||||
jest.mock('../dataset-config', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="dataset-config" />,
|
||||
}))
|
||||
|
||||
jest.mock('./agent/agent-tools', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="agent-tools" />,
|
||||
}))
|
||||
|
||||
jest.mock('../config-vision', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="config-vision" />,
|
||||
}))
|
||||
|
||||
jest.mock('./config-document', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="config-document" />,
|
||||
}))
|
||||
|
||||
jest.mock('./config-audio', () => ({
|
||||
__esModule: true,
|
||||
default: () => <div data-testid="config-audio" />,
|
||||
}))
|
||||
|
||||
let latestHistoryPanelProps: any
|
||||
jest.mock('../config-prompt/conversation-history/history-panel', () => ({
|
||||
__esModule: true,
|
||||
default: (props: any) => {
|
||||
latestHistoryPanelProps = props
|
||||
return <div data-testid="history-panel" />
|
||||
},
|
||||
}))
|
||||
|
||||
type MockContext = {
|
||||
mode: AppModeEnum
|
||||
isAdvancedMode: boolean
|
||||
modelModeType: ModelModeType
|
||||
isAgent: boolean
|
||||
hasSetBlockStatus: {
|
||||
context: boolean
|
||||
history: boolean
|
||||
query: boolean
|
||||
}
|
||||
showHistoryModal: jest.Mock
|
||||
modelConfig: ModelConfig
|
||||
setModelConfig: jest.Mock
|
||||
setPrevPromptConfig: jest.Mock
|
||||
}
|
||||
|
||||
const createPromptVariable = (overrides: Partial<PromptVariable> = {}): PromptVariable => ({
|
||||
key: 'variable',
|
||||
name: 'Variable',
|
||||
type: 'string',
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createModelConfig = (overrides: Partial<ModelConfig> = {}): ModelConfig => ({
|
||||
provider: 'openai',
|
||||
model_id: 'gpt-4',
|
||||
mode: ModelModeType.chat,
|
||||
configs: {
|
||||
prompt_template: 'Hello {{variable}}',
|
||||
prompt_variables: [createPromptVariable({ key: 'existing' })],
|
||||
},
|
||||
chat_prompt_config: null,
|
||||
completion_prompt_config: null,
|
||||
opening_statement: null,
|
||||
more_like_this: null,
|
||||
suggested_questions: null,
|
||||
suggested_questions_after_answer: null,
|
||||
speech_to_text: null,
|
||||
text_to_speech: null,
|
||||
file_upload: null,
|
||||
retriever_resource: null,
|
||||
sensitive_word_avoidance: null,
|
||||
annotation_reply: null,
|
||||
external_data_tools: null,
|
||||
system_parameters: {
|
||||
audio_file_size_limit: 1,
|
||||
file_size_limit: 1,
|
||||
image_file_size_limit: 1,
|
||||
video_file_size_limit: 1,
|
||||
workflow_file_upload_limit: 1,
|
||||
},
|
||||
dataSets: [],
|
||||
agentConfig: {
|
||||
enabled: false,
|
||||
strategy: AgentStrategy.react,
|
||||
max_iteration: 1,
|
||||
tools: [] as ToolItem[],
|
||||
},
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const createContextValue = (overrides: Partial<MockContext> = {}): MockContext => ({
|
||||
mode: AppModeEnum.CHAT,
|
||||
isAdvancedMode: false,
|
||||
modelModeType: ModelModeType.chat,
|
||||
isAgent: false,
|
||||
hasSetBlockStatus: {
|
||||
context: false,
|
||||
history: true,
|
||||
query: false,
|
||||
},
|
||||
showHistoryModal: jest.fn(),
|
||||
modelConfig: createModelConfig(),
|
||||
setModelConfig: jest.fn(),
|
||||
setPrevPromptConfig: jest.fn(),
|
||||
...overrides,
|
||||
})
|
||||
|
||||
const mockUseContext = useContextSelector.useContext as jest.Mock
|
||||
|
||||
const renderConfig = (contextOverrides: Partial<MockContext> = {}) => {
|
||||
const contextValue = createContextValue(contextOverrides)
|
||||
mockUseContext.mockReturnValue(contextValue)
|
||||
return {
|
||||
contextValue,
|
||||
...render(<Config />),
|
||||
}
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks()
|
||||
latestConfigPromptProps = undefined
|
||||
latestConfigVarProps = undefined
|
||||
latestHistoryPanelProps = undefined
|
||||
})
|
||||
|
||||
// Rendering scenarios ensure the layout toggles agent/history specific sections correctly.
|
||||
describe('Config - Rendering', () => {
|
||||
it('should render baseline sections without agent specific panels', () => {
|
||||
renderConfig()
|
||||
|
||||
expect(screen.getByTestId('config-prompt')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('config-var')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('dataset-config')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('config-vision')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('config-document')).toBeInTheDocument()
|
||||
expect(screen.getByTestId('config-audio')).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('agent-tools')).not.toBeInTheDocument()
|
||||
expect(screen.queryByTestId('history-panel')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should show AgentTools when app runs in agent mode', () => {
|
||||
renderConfig({ isAgent: true })
|
||||
|
||||
expect(screen.getByTestId('agent-tools')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('should display HistoryPanel only when advanced chat completion values apply', () => {
|
||||
const showHistoryModal = jest.fn()
|
||||
renderConfig({
|
||||
isAdvancedMode: true,
|
||||
mode: AppModeEnum.ADVANCED_CHAT,
|
||||
modelModeType: ModelModeType.completion,
|
||||
hasSetBlockStatus: {
|
||||
context: false,
|
||||
history: false,
|
||||
query: false,
|
||||
},
|
||||
showHistoryModal,
|
||||
})
|
||||
|
||||
expect(screen.getByTestId('history-panel')).toBeInTheDocument()
|
||||
expect(latestHistoryPanelProps.showWarning).toBe(true)
|
||||
expect(latestHistoryPanelProps.onShowEditModal).toBe(showHistoryModal)
|
||||
})
|
||||
})
|
||||
|
||||
// Prompt handling scenarios validate integration between Config and prompt children.
|
||||
describe('Config - Prompt Handling', () => {
|
||||
it('should update prompt template and dispatch formatting event when text changes', () => {
|
||||
const { contextValue } = renderConfig()
|
||||
const previousVariables = contextValue.modelConfig.configs.prompt_variables
|
||||
const additions = [createPromptVariable({ key: 'new', name: 'New' })]
|
||||
|
||||
latestConfigPromptProps.onChange('Updated template', additions)
|
||||
|
||||
expect(contextValue.setPrevPromptConfig).toHaveBeenCalledWith(contextValue.modelConfig.configs)
|
||||
expect(contextValue.setModelConfig).toHaveBeenCalledWith(expect.objectContaining({
|
||||
configs: expect.objectContaining({
|
||||
prompt_template: 'Updated template',
|
||||
prompt_variables: [...previousVariables, ...additions],
|
||||
}),
|
||||
}))
|
||||
expect(mockFormattingDispatcher).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('should skip formatting dispatcher when template remains identical', () => {
|
||||
const { contextValue } = renderConfig()
|
||||
const unchangedTemplate = contextValue.modelConfig.configs.prompt_template
|
||||
|
||||
latestConfigPromptProps.onChange(unchangedTemplate, [createPromptVariable({ key: 'added' })])
|
||||
|
||||
expect(contextValue.setPrevPromptConfig).toHaveBeenCalledWith(contextValue.modelConfig.configs)
|
||||
expect(mockFormattingDispatcher).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should replace prompt variables when ConfigVar reports updates', () => {
|
||||
const { contextValue } = renderConfig()
|
||||
const replacementVariables = [createPromptVariable({ key: 'replacement' })]
|
||||
|
||||
latestConfigVarProps.onPromptVariablesChange(replacementVariables)
|
||||
|
||||
expect(contextValue.setPrevPromptConfig).toHaveBeenCalledWith(contextValue.modelConfig.configs)
|
||||
expect(contextValue.setModelConfig).toHaveBeenCalledWith(expect.objectContaining({
|
||||
configs: expect.objectContaining({
|
||||
prompt_variables: replacementVariables,
|
||||
}),
|
||||
}))
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user