diff --git a/web/app/components/app/configuration/config-vision/index.spec.tsx b/web/app/components/app/configuration/config-vision/index.spec.tsx new file mode 100644 index 0000000000..e22db7b24e --- /dev/null +++ b/web/app/components/app/configuration/config-vision/index.spec.tsx @@ -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 = {}) => { + 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() + + expect(screen.queryByText('appDebug.vision.name')).not.toBeInTheDocument() + }) + + it('should show the toggle and parameter controls when visible', () => { + render() + + 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() + 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() + 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() + 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() + + 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() + + 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() + + 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() + 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) + }) +}) diff --git a/web/app/components/app/configuration/config/agent-setting-button.spec.tsx b/web/app/components/app/configuration/config/agent-setting-button.spec.tsx new file mode 100644 index 0000000000..db70865e51 --- /dev/null +++ b/web/app/components/app/configuration/config/agent-setting-button.spec.tsx @@ -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 ( +
+ + +
+ ) + }, +})) + +const createAgentConfig = (overrides: Partial = {}): AgentConfig => ({ + enabled: true, + strategy: AgentStrategy.react, + max_iteration: 3, + tools: [], + ...overrides, +}) + +const setup = (overrides: Partial> = {}) => { + const props: React.ComponentProps = { + isFunctionCall: false, + isChatModel: true, + onAgentSettingChange: jest.fn(), + agentConfig: createAgentConfig(), + ...overrides, + } + + const user = userEvent.setup() + render() + 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() + }) +}) diff --git a/web/app/components/app/configuration/config/config-audio.spec.tsx b/web/app/components/app/configuration/config/config-audio.spec.tsx new file mode 100644 index 0000000000..94eeb87c99 --- /dev/null +++ b/web/app/components/app/configuration/config/config-audio.spec.tsx @@ -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(() => 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() + 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, + }), + })) + }) +}) diff --git a/web/app/components/app/configuration/config/config-document.spec.tsx b/web/app/components/app/configuration/config/config-document.spec.tsx new file mode 100644 index 0000000000..aeb504fdbd --- /dev/null +++ b/web/app/components/app/configuration/config/config-document.spec.tsx @@ -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(() => 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() + 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, + }), + })) + }) +}) diff --git a/web/app/components/app/configuration/config/index.spec.tsx b/web/app/components/app/configuration/config/index.spec.tsx new file mode 100644 index 0000000000..814c52c3d7 --- /dev/null +++ b/web/app/components/app/configuration/config/index.spec.tsx @@ -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
+ }, +})) + +let latestConfigVarProps: any +jest.mock('@/app/components/app/configuration/config-var', () => ({ + __esModule: true, + default: (props: any) => { + latestConfigVarProps = props + return
+ }, +})) + +jest.mock('../dataset-config', () => ({ + __esModule: true, + default: () =>
, +})) + +jest.mock('./agent/agent-tools', () => ({ + __esModule: true, + default: () =>
, +})) + +jest.mock('../config-vision', () => ({ + __esModule: true, + default: () =>
, +})) + +jest.mock('./config-document', () => ({ + __esModule: true, + default: () =>
, +})) + +jest.mock('./config-audio', () => ({ + __esModule: true, + default: () =>
, +})) + +let latestHistoryPanelProps: any +jest.mock('../config-prompt/conversation-history/history-panel', () => ({ + __esModule: true, + default: (props: any) => { + latestHistoryPanelProps = props + return
+ }, +})) + +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 => ({ + key: 'variable', + name: 'Variable', + type: 'string', + ...overrides, +}) + +const createModelConfig = (overrides: Partial = {}): 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 => ({ + 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 = {}) => { + const contextValue = createContextValue(contextOverrides) + mockUseContext.mockReturnValue(contextValue) + return { + contextValue, + ...render(), + } +} + +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, + }), + })) + }) +})