From 8cf1da96f5e790d3479bf2f9767e660b0a60ba2b Mon Sep 17 00:00:00 2001 From: Joel Date: Wed, 17 Dec 2025 16:39:53 +0800 Subject: [PATCH] chore: tests for app agent configures (#29789) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../config/agent/agent-setting/index.spec.tsx | 112 +++++ .../agent/agent-setting/item-panel.spec.tsx | 21 + .../config/agent/agent-tools/index.spec.tsx | 466 ++++++++++++++++++ .../config/agent/agent-tools/index.tsx | 3 +- .../setting-built-in-tool.spec.tsx | 248 ++++++++++ 5 files changed, 849 insertions(+), 1 deletion(-) create mode 100644 web/app/components/app/configuration/config/agent/agent-setting/index.spec.tsx create mode 100644 web/app/components/app/configuration/config/agent/agent-setting/item-panel.spec.tsx create mode 100644 web/app/components/app/configuration/config/agent/agent-tools/index.spec.tsx create mode 100644 web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.spec.tsx diff --git a/web/app/components/app/configuration/config/agent/agent-setting/index.spec.tsx b/web/app/components/app/configuration/config/agent/agent-setting/index.spec.tsx new file mode 100644 index 0000000000..00c0776718 --- /dev/null +++ b/web/app/components/app/configuration/config/agent/agent-setting/index.spec.tsx @@ -0,0 +1,112 @@ +import React from 'react' +import { act, fireEvent, render, screen } from '@testing-library/react' +import AgentSetting from './index' +import { MAX_ITERATIONS_NUM } from '@/config' +import type { AgentConfig } from '@/models/debug' + +jest.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string) => key, + }), +})) + +jest.mock('ahooks', () => { + const actual = jest.requireActual('ahooks') + return { + ...actual, + useClickAway: jest.fn(), + } +}) + +jest.mock('react-slider', () => (props: { className?: string; min?: number; max?: number; value: number; onChange: (value: number) => void }) => ( + props.onChange(Number(e.target.value))} + /> +)) + +const basePayload = { + enabled: true, + strategy: 'react', + max_iteration: 5, + tools: [], +} + +const renderModal = (props?: Partial>) => { + const onCancel = jest.fn() + const onSave = jest.fn() + const utils = render( + , + ) + return { ...utils, onCancel, onSave } +} + +describe('AgentSetting', () => { + test('should render agent mode description and default prompt section when not function call', () => { + renderModal() + + expect(screen.getByText('appDebug.agent.agentMode')).toBeInTheDocument() + expect(screen.getByText('appDebug.agent.agentModeType.ReACT')).toBeInTheDocument() + expect(screen.getByText('tools.builtInPromptTitle')).toBeInTheDocument() + }) + + test('should display function call mode when isFunctionCall true', () => { + renderModal({ isFunctionCall: true }) + + expect(screen.getByText('appDebug.agent.agentModeType.functionCall')).toBeInTheDocument() + expect(screen.queryByText('tools.builtInPromptTitle')).not.toBeInTheDocument() + }) + + test('should update iteration via slider and number input', () => { + const { container } = renderModal() + const slider = container.querySelector('.slider') as HTMLInputElement + const numberInput = screen.getByRole('spinbutton') + + fireEvent.change(slider, { target: { value: '7' } }) + expect(screen.getAllByDisplayValue('7')).toHaveLength(2) + + fireEvent.change(numberInput, { target: { value: '2' } }) + expect(screen.getAllByDisplayValue('2')).toHaveLength(2) + }) + + test('should clamp iteration value within min/max range', () => { + renderModal() + + const numberInput = screen.getByRole('spinbutton') + + fireEvent.change(numberInput, { target: { value: '0' } }) + expect(screen.getAllByDisplayValue('1')).toHaveLength(2) + + fireEvent.change(numberInput, { target: { value: '999' } }) + expect(screen.getAllByDisplayValue(String(MAX_ITERATIONS_NUM))).toHaveLength(2) + }) + + test('should call onCancel when cancel button clicked', () => { + const { onCancel } = renderModal() + fireEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' })) + expect(onCancel).toHaveBeenCalled() + }) + + test('should call onSave with updated payload', async () => { + const { onSave } = renderModal() + const numberInput = screen.getByRole('spinbutton') + fireEvent.change(numberInput, { target: { value: '6' } }) + + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: 'common.operation.save' })) + }) + + expect(onSave).toHaveBeenCalledWith(expect.objectContaining({ max_iteration: 6 })) + }) +}) diff --git a/web/app/components/app/configuration/config/agent/agent-setting/item-panel.spec.tsx b/web/app/components/app/configuration/config/agent/agent-setting/item-panel.spec.tsx new file mode 100644 index 0000000000..242f249738 --- /dev/null +++ b/web/app/components/app/configuration/config/agent/agent-setting/item-panel.spec.tsx @@ -0,0 +1,21 @@ +import React from 'react' +import { render, screen } from '@testing-library/react' +import ItemPanel from './item-panel' + +describe('AgentSetting/ItemPanel', () => { + test('should render icon, name, and children content', () => { + render( + icon} + name="Panel name" + description="More info" + children={
child content
} + />, + ) + + expect(screen.getByText('Panel name')).toBeInTheDocument() + expect(screen.getByText('child content')).toBeInTheDocument() + expect(screen.getByText('icon')).toBeInTheDocument() + }) +}) diff --git a/web/app/components/app/configuration/config/agent/agent-tools/index.spec.tsx b/web/app/components/app/configuration/config/agent/agent-tools/index.spec.tsx new file mode 100644 index 0000000000..9899f15375 --- /dev/null +++ b/web/app/components/app/configuration/config/agent/agent-tools/index.spec.tsx @@ -0,0 +1,466 @@ +import type { + PropsWithChildren, +} from 'react' +import React, { + useEffect, + useMemo, + useState, +} from 'react' +import { act, render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import AgentTools from './index' +import ConfigContext from '@/context/debug-configuration' +import type { AgentTool } from '@/types/app' +import { CollectionType, type Tool, type ToolParameter } from '@/app/components/tools/types' +import type { ToolWithProvider } from '@/app/components/workflow/types' +import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types' +import type { ModelConfig } from '@/models/debug' +import { ModelModeType } from '@/types/app' +import { + DEFAULT_AGENT_SETTING, + DEFAULT_CHAT_PROMPT_CONFIG, + DEFAULT_COMPLETION_PROMPT_CONFIG, +} from '@/config' +import copy from 'copy-to-clipboard' +import type ToolPickerType from '@/app/components/workflow/block-selector/tool-picker' +import type SettingBuiltInToolType from './setting-built-in-tool' + +const formattingDispatcherMock = jest.fn() +jest.mock('@/app/components/app/configuration/debug/hooks', () => ({ + useFormattingChangedDispatcher: () => formattingDispatcherMock, +})) + +let pluginInstallHandler: ((names: string[]) => void) | null = null +const subscribeMock = jest.fn((event: string, handler: any) => { + if (event === 'plugin:install:success') + pluginInstallHandler = handler +}) +jest.mock('@/context/mitt-context', () => ({ + useMittContextSelector: (selector: any) => selector({ + useSubscribe: subscribeMock, + }), +})) + +let builtInTools: ToolWithProvider[] = [] +let customTools: ToolWithProvider[] = [] +let workflowTools: ToolWithProvider[] = [] +let mcpTools: ToolWithProvider[] = [] +jest.mock('@/service/use-tools', () => ({ + useAllBuiltInTools: () => ({ data: builtInTools }), + useAllCustomTools: () => ({ data: customTools }), + useAllWorkflowTools: () => ({ data: workflowTools }), + useAllMCPTools: () => ({ data: mcpTools }), +})) + +type ToolPickerProps = React.ComponentProps +let singleToolSelection: ToolDefaultValue | null = null +let multipleToolSelection: ToolDefaultValue[] = [] +const ToolPickerMock = (props: ToolPickerProps) => ( +
+
{props.trigger}
+ + +
+) +jest.mock('@/app/components/workflow/block-selector/tool-picker', () => ({ + __esModule: true, + default: (props: ToolPickerProps) => , +})) + +type SettingBuiltInToolProps = React.ComponentProps +let latestSettingPanelProps: SettingBuiltInToolProps | null = null +let settingPanelSavePayload: Record = {} +let settingPanelCredentialId = 'credential-from-panel' +const SettingBuiltInToolMock = (props: SettingBuiltInToolProps) => { + latestSettingPanelProps = props + return ( +
+ {props.toolName} + + + +
+ ) +} +jest.mock('./setting-built-in-tool', () => ({ + __esModule: true, + default: (props: SettingBuiltInToolProps) => , +})) + +jest.mock('copy-to-clipboard') + +const copyMock = copy as jest.Mock + +const createToolParameter = (overrides?: Partial): ToolParameter => ({ + name: 'api_key', + label: { + en_US: 'API Key', + zh_Hans: 'API Key', + }, + human_description: { + en_US: 'desc', + zh_Hans: 'desc', + }, + type: 'string', + form: 'config', + llm_description: '', + required: true, + multiple: false, + default: 'default', + ...overrides, +}) + +const createToolDefinition = (overrides?: Partial): Tool => ({ + name: 'search', + author: 'tester', + label: { + en_US: 'Search', + zh_Hans: 'Search', + }, + description: { + en_US: 'desc', + zh_Hans: 'desc', + }, + parameters: [createToolParameter()], + labels: [], + output_schema: {}, + ...overrides, +}) + +const createCollection = (overrides?: Partial): ToolWithProvider => ({ + id: overrides?.id || 'provider-1', + name: overrides?.name || 'vendor/provider-1', + author: 'tester', + description: { + en_US: 'desc', + zh_Hans: 'desc', + }, + icon: 'https://example.com/icon.png', + label: { + en_US: 'Provider Label', + zh_Hans: 'Provider Label', + }, + type: overrides?.type || CollectionType.builtIn, + team_credentials: {}, + is_team_authorization: true, + allow_delete: true, + labels: [], + tools: overrides?.tools || [createToolDefinition()], + meta: { + version: '1.0.0', + }, + ...overrides, +}) + +const createAgentTool = (overrides?: Partial): AgentTool => ({ + provider_id: overrides?.provider_id || 'provider-1', + provider_type: overrides?.provider_type || CollectionType.builtIn, + provider_name: overrides?.provider_name || 'vendor/provider-1', + tool_name: overrides?.tool_name || 'search', + tool_label: overrides?.tool_label || 'Search Tool', + tool_parameters: overrides?.tool_parameters || { api_key: 'key' }, + enabled: overrides?.enabled ?? true, + ...overrides, +}) + +const createModelConfig = (tools: AgentTool[]): ModelConfig => ({ + provider: 'OPENAI', + model_id: 'gpt-3.5-turbo', + mode: ModelModeType.chat, + configs: { + prompt_template: '', + prompt_variables: [], + }, + chat_prompt_config: DEFAULT_CHAT_PROMPT_CONFIG, + completion_prompt_config: DEFAULT_COMPLETION_PROMPT_CONFIG, + opening_statement: '', + more_like_this: null, + suggested_questions: [], + 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: [], + system_parameters: { + audio_file_size_limit: 0, + file_size_limit: 0, + image_file_size_limit: 0, + video_file_size_limit: 0, + workflow_file_upload_limit: 0, + }, + dataSets: [], + agentConfig: { + ...DEFAULT_AGENT_SETTING, + tools, + }, +}) + +const renderAgentTools = (initialTools?: AgentTool[]) => { + const tools = initialTools ?? [createAgentTool()] + const modelConfigRef = { current: createModelConfig(tools) } + const Wrapper = ({ children }: PropsWithChildren) => { + const [modelConfig, setModelConfig] = useState(modelConfigRef.current) + useEffect(() => { + modelConfigRef.current = modelConfig + }, [modelConfig]) + const value = useMemo(() => ({ + modelConfig, + setModelConfig, + }), [modelConfig]) + return ( + + {children} + + ) + } + const renderResult = render( + + + , + ) + return { + ...renderResult, + getModelConfig: () => modelConfigRef.current, + } +} + +const hoverInfoIcon = async (rowIndex = 0) => { + const rows = document.querySelectorAll('.group') + const infoTrigger = rows.item(rowIndex)?.querySelector('[data-testid="tool-info-tooltip"]') + if (!infoTrigger) + throw new Error('Info trigger not found') + await userEvent.hover(infoTrigger as HTMLElement) +} + +describe('AgentTools', () => { + beforeEach(() => { + jest.clearAllMocks() + builtInTools = [ + createCollection(), + createCollection({ + id: 'provider-2', + name: 'vendor/provider-2', + tools: [createToolDefinition({ + name: 'translate', + label: { + en_US: 'Translate', + zh_Hans: 'Translate', + }, + })], + }), + createCollection({ + id: 'provider-3', + name: 'vendor/provider-3', + tools: [createToolDefinition({ + name: 'summarize', + label: { + en_US: 'Summary', + zh_Hans: 'Summary', + }, + })], + }), + ] + customTools = [] + workflowTools = [] + mcpTools = [] + singleToolSelection = { + provider_id: 'provider-3', + provider_type: CollectionType.builtIn, + provider_name: 'vendor/provider-3', + tool_name: 'summarize', + tool_label: 'Summary Tool', + tool_description: 'desc', + title: 'Summary Tool', + is_team_authorization: true, + params: { api_key: 'picker-value' }, + paramSchemas: [], + output_schema: {}, + } + multipleToolSelection = [ + { + provider_id: 'provider-2', + provider_type: CollectionType.builtIn, + provider_name: 'vendor/provider-2', + tool_name: 'translate', + tool_label: 'Translate Tool', + tool_description: 'desc', + title: 'Translate Tool', + is_team_authorization: true, + params: { api_key: 'multi-a' }, + paramSchemas: [], + output_schema: {}, + }, + { + provider_id: 'provider-3', + provider_type: CollectionType.builtIn, + provider_name: 'vendor/provider-3', + tool_name: 'summarize', + tool_label: 'Summary Tool', + tool_description: 'desc', + title: 'Summary Tool', + is_team_authorization: true, + params: { api_key: 'multi-b' }, + paramSchemas: [], + output_schema: {}, + }, + ] + latestSettingPanelProps = null + settingPanelSavePayload = {} + settingPanelCredentialId = 'credential-from-panel' + pluginInstallHandler = null + }) + + test('should show enabled count and provider information', () => { + renderAgentTools([ + createAgentTool(), + createAgentTool({ + provider_id: 'provider-2', + provider_name: 'vendor/provider-2', + tool_name: 'translate', + tool_label: 'Translate Tool', + enabled: false, + }), + ]) + + const enabledText = screen.getByText(content => content.includes('appDebug.agent.tools.enabled')) + expect(enabledText).toHaveTextContent('1/2') + expect(screen.getByText('provider-1')).toBeInTheDocument() + expect(screen.getByText('Translate Tool')).toBeInTheDocument() + }) + + test('should copy tool name from tooltip action', async () => { + renderAgentTools() + + await hoverInfoIcon() + const copyButton = await screen.findByText('tools.copyToolName') + await userEvent.click(copyButton) + expect(copyMock).toHaveBeenCalledWith('search') + }) + + test('should toggle tool enabled state via switch', async () => { + const { getModelConfig } = renderAgentTools() + + const switchButton = screen.getByRole('switch') + await userEvent.click(switchButton) + + await waitFor(() => { + const tools = getModelConfig().agentConfig.tools as Array<{ tool_name?: string; enabled?: boolean }> + const toggledTool = tools.find(tool => tool.tool_name === 'search') + expect(toggledTool?.enabled).toBe(false) + }) + expect(formattingDispatcherMock).toHaveBeenCalled() + }) + + test('should remove tool when delete action is clicked', async () => { + const { getModelConfig } = renderAgentTools() + const deleteButton = screen.getByTestId('delete-removed-tool') + if (!deleteButton) + throw new Error('Delete button not found') + await userEvent.click(deleteButton) + await waitFor(() => { + expect(getModelConfig().agentConfig.tools).toHaveLength(0) + }) + expect(formattingDispatcherMock).toHaveBeenCalled() + }) + + test('should add a tool when ToolPicker selects one', async () => { + const { getModelConfig } = renderAgentTools([]) + const addSingleButton = screen.getByRole('button', { name: 'pick-single' }) + await userEvent.click(addSingleButton) + + await waitFor(() => { + expect(screen.getByText('Summary Tool')).toBeInTheDocument() + }) + expect(getModelConfig().agentConfig.tools).toHaveLength(1) + }) + + test('should append multiple selected tools at once', async () => { + const { getModelConfig } = renderAgentTools([]) + await userEvent.click(screen.getByRole('button', { name: 'pick-multiple' })) + + await waitFor(() => { + expect(screen.getByText('Translate Tool')).toBeInTheDocument() + expect(screen.getAllByText('Summary Tool')).toHaveLength(1) + }) + expect(getModelConfig().agentConfig.tools).toHaveLength(2) + }) + + test('should open settings panel for not authorized tool', async () => { + renderAgentTools([ + createAgentTool({ + notAuthor: true, + }), + ]) + + const notAuthorizedButton = screen.getByRole('button', { name: /tools.notAuthorized/ }) + await userEvent.click(notAuthorizedButton) + expect(screen.getByTestId('setting-built-in-tool')).toBeInTheDocument() + expect(latestSettingPanelProps?.toolName).toBe('search') + }) + + test('should persist tool parameters when SettingBuiltInTool saves values', async () => { + const { getModelConfig } = renderAgentTools([ + createAgentTool({ + notAuthor: true, + }), + ]) + await userEvent.click(screen.getByRole('button', { name: /tools.notAuthorized/ })) + settingPanelSavePayload = { api_key: 'updated' } + await userEvent.click(screen.getByRole('button', { name: 'save-from-panel' })) + + await waitFor(() => { + expect((getModelConfig().agentConfig.tools[0] as { tool_parameters: Record }).tool_parameters).toEqual({ api_key: 'updated' }) + }) + }) + + test('should update credential id when authorization selection changes', async () => { + const { getModelConfig } = renderAgentTools([ + createAgentTool({ + notAuthor: true, + }), + ]) + await userEvent.click(screen.getByRole('button', { name: /tools.notAuthorized/ })) + settingPanelCredentialId = 'credential-123' + await userEvent.click(screen.getByRole('button', { name: 'auth-from-panel' })) + + await waitFor(() => { + expect((getModelConfig().agentConfig.tools[0] as { credential_id: string }).credential_id).toBe('credential-123') + }) + expect(formattingDispatcherMock).toHaveBeenCalled() + }) + + test('should reinstate deleted tools after plugin install success event', async () => { + const { getModelConfig } = renderAgentTools([ + createAgentTool({ + provider_id: 'provider-1', + provider_name: 'vendor/provider-1', + tool_name: 'search', + tool_label: 'Search Tool', + isDeleted: true, + }), + ]) + if (!pluginInstallHandler) + throw new Error('Plugin handler not registered') + + await act(async () => { + pluginInstallHandler?.(['provider-1']) + }) + + await waitFor(() => { + expect((getModelConfig().agentConfig.tools[0] as { isDeleted: boolean }).isDeleted).toBe(false) + }) + }) +}) diff --git a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx index 5716bfd92d..4793b5fe49 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx @@ -217,7 +217,7 @@ const AgentTools: FC = () => { } >
-
+
@@ -277,6 +277,7 @@ const AgentTools: FC = () => { }} onMouseOver={() => setIsDeleting(index)} onMouseLeave={() => setIsDeleting(-1)} + data-testid='delete-removed-tool' >
diff --git a/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.spec.tsx b/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.spec.tsx new file mode 100644 index 0000000000..8cd95472dc --- /dev/null +++ b/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.spec.tsx @@ -0,0 +1,248 @@ +import React from 'react' +import { render, screen, waitFor } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import SettingBuiltInTool from './setting-built-in-tool' +import I18n from '@/context/i18n' +import { CollectionType, type Tool, type ToolParameter } from '@/app/components/tools/types' + +const fetchModelToolList = jest.fn() +const fetchBuiltInToolList = jest.fn() +const fetchCustomToolList = jest.fn() +const fetchWorkflowToolList = jest.fn() +jest.mock('@/service/tools', () => ({ + fetchModelToolList: (collectionName: string) => fetchModelToolList(collectionName), + fetchBuiltInToolList: (collectionName: string) => fetchBuiltInToolList(collectionName), + fetchCustomToolList: (collectionName: string) => fetchCustomToolList(collectionName), + fetchWorkflowToolList: (appId: string) => fetchWorkflowToolList(appId), +})) + +type MockFormProps = { + value: Record + onChange: (val: Record) => void +} +let nextFormValue: Record = {} +const FormMock = ({ value, onChange }: MockFormProps) => { + return ( +
+
{JSON.stringify(value)}
+ +
+ ) +} +jest.mock('@/app/components/header/account-setting/model-provider-page/model-modal/Form', () => ({ + __esModule: true, + default: (props: MockFormProps) => , +})) + +let pluginAuthClickValue = 'credential-from-plugin' +jest.mock('@/app/components/plugins/plugin-auth', () => ({ + AuthCategory: { tool: 'tool' }, + PluginAuthInAgent: (props: { onAuthorizationItemClick?: (id: string) => void }) => ( +
+ +
+ ), +})) + +jest.mock('@/app/components/plugins/readme-panel/entrance', () => ({ + ReadmeEntrance: ({ className }: { className?: string }) =>
readme
, +})) + +const createParameter = (overrides?: Partial): ToolParameter => ({ + name: 'settingParam', + label: { + en_US: 'Setting Param', + zh_Hans: 'Setting Param', + }, + human_description: { + en_US: 'desc', + zh_Hans: 'desc', + }, + type: 'string', + form: 'config', + llm_description: '', + required: true, + multiple: false, + default: '', + ...overrides, +}) + +const createTool = (overrides?: Partial): Tool => ({ + name: 'search', + author: 'tester', + label: { + en_US: 'Search Tool', + zh_Hans: 'Search Tool', + }, + description: { + en_US: 'tool description', + zh_Hans: 'tool description', + }, + parameters: [ + createParameter({ + name: 'infoParam', + label: { + en_US: 'Info Param', + zh_Hans: 'Info Param', + }, + form: 'llm', + required: false, + }), + createParameter(), + ], + labels: [], + output_schema: {}, + ...overrides, +}) + +const baseCollection = { + id: 'provider-1', + name: 'vendor/provider-1', + author: 'tester', + description: { + en_US: 'desc', + zh_Hans: 'desc', + }, + icon: 'https://example.com/icon.png', + label: { + en_US: 'Provider Label', + zh_Hans: 'Provider Label', + }, + type: CollectionType.builtIn, + team_credentials: {}, + is_team_authorization: true, + allow_delete: true, + labels: [], + tools: [createTool()], +} + +const renderComponent = (props?: Partial>) => { + const onHide = jest.fn() + const onSave = jest.fn() + const onAuthorizationItemClick = jest.fn() + const utils = render( + + + , + ) + return { + ...utils, + onHide, + onSave, + onAuthorizationItemClick, + } +} + +describe('SettingBuiltInTool', () => { + beforeEach(() => { + jest.clearAllMocks() + nextFormValue = {} + pluginAuthClickValue = 'credential-from-plugin' + }) + + test('should fetch tool list when collection has no tools', async () => { + fetchModelToolList.mockResolvedValueOnce([createTool()]) + renderComponent({ + collection: { + ...baseCollection, + tools: [], + }, + }) + + await waitFor(() => { + expect(fetchModelToolList).toHaveBeenCalledTimes(1) + expect(fetchModelToolList).toHaveBeenCalledWith('vendor/provider-1') + }) + expect(await screen.findByText('Search Tool')).toBeInTheDocument() + }) + + test('should switch between info and setting tabs', async () => { + renderComponent() + await waitFor(() => { + expect(screen.getByTestId('mock-form')).toBeInTheDocument() + }) + + await userEvent.click(screen.getByText('tools.setBuiltInTools.parameters')) + expect(screen.getByText('Info Param')).toBeInTheDocument() + await userEvent.click(screen.getByText('tools.setBuiltInTools.setting')) + expect(screen.getByTestId('mock-form')).toBeInTheDocument() + }) + + test('should call onSave with updated values when save button clicked', async () => { + const { onSave } = renderComponent() + await waitFor(() => expect(screen.getByTestId('mock-form')).toBeInTheDocument()) + nextFormValue = { settingParam: 'updated' } + await userEvent.click(screen.getByRole('button', { name: 'update-form' })) + await userEvent.click(screen.getByRole('button', { name: 'common.operation.save' })) + expect(onSave).toHaveBeenCalledWith(expect.objectContaining({ settingParam: 'updated' })) + }) + + test('should keep save disabled until required field provided', async () => { + renderComponent({ + setting: {}, + }) + await waitFor(() => expect(screen.getByTestId('mock-form')).toBeInTheDocument()) + const saveButton = screen.getByRole('button', { name: 'common.operation.save' }) + expect(saveButton).toBeDisabled() + nextFormValue = { settingParam: 'filled' } + await userEvent.click(screen.getByRole('button', { name: 'update-form' })) + expect(saveButton).not.toBeDisabled() + }) + + test('should call onHide when cancel button is pressed', async () => { + const { onHide } = renderComponent() + await waitFor(() => expect(screen.getByTestId('mock-form')).toBeInTheDocument()) + await userEvent.click(screen.getByRole('button', { name: 'common.operation.cancel' })) + expect(onHide).toHaveBeenCalled() + }) + + test('should trigger authorization callback from plugin auth section', async () => { + const { onAuthorizationItemClick } = renderComponent() + await userEvent.click(screen.getByRole('button', { name: 'choose-plugin-credential' })) + expect(onAuthorizationItemClick).toHaveBeenCalledWith('credential-from-plugin') + }) + + test('should call onHide when back button is clicked', async () => { + const { onHide } = renderComponent({ + showBackButton: true, + }) + await userEvent.click(screen.getByText('plugin.detailPanel.operation.back')) + expect(onHide).toHaveBeenCalled() + }) + + test('should load workflow tools when workflow collection is provided', async () => { + fetchWorkflowToolList.mockResolvedValueOnce([createTool({ + name: 'workflow-tool', + })]) + renderComponent({ + collection: { + ...baseCollection, + type: CollectionType.workflow, + tools: [], + id: 'workflow-1', + } as any, + isBuiltIn: false, + isModel: false, + }) + + await waitFor(() => { + expect(fetchWorkflowToolList).toHaveBeenCalledWith('workflow-1') + }) + }) +})