import { renderHook, act } from '@testing-library/react'; import { useFormContext, useWatch } from 'react-hook-form'; import type { AgentToolType } from 'librechat-data-provider'; import useMCPToolOptions from '../useMCPToolOptions'; jest.mock('react-hook-form', () => ({ useFormContext: jest.fn(), useWatch: jest.fn(), })); const mockSetValue = jest.fn(); const mockGetValues = jest.fn(); const createMockTool = (toolId: string): AgentToolType => ({ tool_id: toolId, metadata: { name: toolId, description: `Description for ${toolId}` }, }); describe('useMCPToolOptions', () => { beforeEach(() => { jest.clearAllMocks(); (useFormContext as jest.Mock).mockReturnValue({ getValues: mockGetValues, setValue: mockSetValue, control: {}, }); (useWatch as jest.Mock).mockReturnValue(undefined); mockGetValues.mockReturnValue({}); }); describe('isToolDeferred', () => { it('should return false when tool_options is undefined', () => { (useWatch as jest.Mock).mockReturnValue(undefined); const { result } = renderHook(() => useMCPToolOptions()); expect(result.current.isToolDeferred('tool1')).toBe(false); }); it('should return false when tool has no options', () => { (useWatch as jest.Mock).mockReturnValue({}); const { result } = renderHook(() => useMCPToolOptions()); expect(result.current.isToolDeferred('tool1')).toBe(false); }); it('should return false when defer_loading is not set', () => { (useWatch as jest.Mock).mockReturnValue({ tool1: { allowed_callers: ['direct'] }, }); const { result } = renderHook(() => useMCPToolOptions()); expect(result.current.isToolDeferred('tool1')).toBe(false); }); it('should return true when defer_loading is true', () => { (useWatch as jest.Mock).mockReturnValue({ tool1: { defer_loading: true }, }); const { result } = renderHook(() => useMCPToolOptions()); expect(result.current.isToolDeferred('tool1')).toBe(true); }); it('should return false when defer_loading is false', () => { (useWatch as jest.Mock).mockReturnValue({ tool1: { defer_loading: false }, }); const { result } = renderHook(() => useMCPToolOptions()); expect(result.current.isToolDeferred('tool1')).toBe(false); }); }); describe('isToolProgrammatic', () => { it('should return false when tool_options is undefined', () => { (useWatch as jest.Mock).mockReturnValue(undefined); const { result } = renderHook(() => useMCPToolOptions()); expect(result.current.isToolProgrammatic('tool1')).toBe(false); }); it('should return false when tool has no options', () => { (useWatch as jest.Mock).mockReturnValue({}); const { result } = renderHook(() => useMCPToolOptions()); expect(result.current.isToolProgrammatic('tool1')).toBe(false); }); it('should return false when allowed_callers does not include code_execution', () => { (useWatch as jest.Mock).mockReturnValue({ tool1: { allowed_callers: ['direct'] }, }); const { result } = renderHook(() => useMCPToolOptions()); expect(result.current.isToolProgrammatic('tool1')).toBe(false); }); it('should return true when allowed_callers includes code_execution', () => { (useWatch as jest.Mock).mockReturnValue({ tool1: { allowed_callers: ['code_execution'] }, }); const { result } = renderHook(() => useMCPToolOptions()); expect(result.current.isToolProgrammatic('tool1')).toBe(true); }); it('should return true when allowed_callers includes both direct and code_execution', () => { (useWatch as jest.Mock).mockReturnValue({ tool1: { allowed_callers: ['direct', 'code_execution'] }, }); const { result } = renderHook(() => useMCPToolOptions()); expect(result.current.isToolProgrammatic('tool1')).toBe(true); }); }); describe('toggleToolDefer', () => { it('should enable defer_loading for a tool with no existing options', () => { mockGetValues.mockReturnValue({}); const { result } = renderHook(() => useMCPToolOptions()); act(() => { result.current.toggleToolDefer('tool1'); }); expect(mockSetValue).toHaveBeenCalledWith( 'tool_options', { tool1: { defer_loading: true } }, { shouldDirty: true }, ); }); it('should enable defer_loading while preserving other options', () => { mockGetValues.mockReturnValue({ tool1: { allowed_callers: ['code_execution'] }, }); const { result } = renderHook(() => useMCPToolOptions()); act(() => { result.current.toggleToolDefer('tool1'); }); expect(mockSetValue).toHaveBeenCalledWith( 'tool_options', { tool1: { allowed_callers: ['code_execution'], defer_loading: true } }, { shouldDirty: true }, ); }); it('should disable defer_loading and preserve other options', () => { mockGetValues.mockReturnValue({ tool1: { defer_loading: true, allowed_callers: ['code_execution'] }, }); const { result } = renderHook(() => useMCPToolOptions()); act(() => { result.current.toggleToolDefer('tool1'); }); expect(mockSetValue).toHaveBeenCalledWith( 'tool_options', { tool1: { allowed_callers: ['code_execution'] } }, { shouldDirty: true }, ); }); it('should remove tool entry entirely when disabling defer_loading and no other options exist', () => { mockGetValues.mockReturnValue({ tool1: { defer_loading: true }, }); const { result } = renderHook(() => useMCPToolOptions()); act(() => { result.current.toggleToolDefer('tool1'); }); expect(mockSetValue).toHaveBeenCalledWith('tool_options', {}, { shouldDirty: true }); }); it('should preserve other tools when toggling', () => { mockGetValues.mockReturnValue({ tool1: { defer_loading: true }, tool2: { defer_loading: true }, }); const { result } = renderHook(() => useMCPToolOptions()); act(() => { result.current.toggleToolDefer('tool1'); }); expect(mockSetValue).toHaveBeenCalledWith( 'tool_options', { tool2: { defer_loading: true } }, { shouldDirty: true }, ); }); }); describe('toggleToolProgrammatic', () => { it('should enable programmatic calling for a tool with no existing options', () => { mockGetValues.mockReturnValue({}); const { result } = renderHook(() => useMCPToolOptions()); act(() => { result.current.toggleToolProgrammatic('tool1'); }); expect(mockSetValue).toHaveBeenCalledWith( 'tool_options', { tool1: { allowed_callers: ['code_execution'] } }, { shouldDirty: true }, ); }); it('should enable programmatic calling while preserving defer_loading', () => { mockGetValues.mockReturnValue({ tool1: { defer_loading: true }, }); const { result } = renderHook(() => useMCPToolOptions()); act(() => { result.current.toggleToolProgrammatic('tool1'); }); expect(mockSetValue).toHaveBeenCalledWith( 'tool_options', { tool1: { defer_loading: true, allowed_callers: ['code_execution'] } }, { shouldDirty: true }, ); }); it('should disable programmatic calling and preserve defer_loading', () => { mockGetValues.mockReturnValue({ tool1: { defer_loading: true, allowed_callers: ['code_execution'] }, }); const { result } = renderHook(() => useMCPToolOptions()); act(() => { result.current.toggleToolProgrammatic('tool1'); }); expect(mockSetValue).toHaveBeenCalledWith( 'tool_options', { tool1: { defer_loading: true } }, { shouldDirty: true }, ); }); it('should remove tool entry entirely when disabling programmatic and no other options exist', () => { mockGetValues.mockReturnValue({ tool1: { allowed_callers: ['code_execution'] }, }); const { result } = renderHook(() => useMCPToolOptions()); act(() => { result.current.toggleToolProgrammatic('tool1'); }); expect(mockSetValue).toHaveBeenCalledWith('tool_options', {}, { shouldDirty: true }); }); it('should preserve other tools when toggling', () => { mockGetValues.mockReturnValue({ tool1: { allowed_callers: ['code_execution'] }, tool2: { defer_loading: true }, }); const { result } = renderHook(() => useMCPToolOptions()); act(() => { result.current.toggleToolProgrammatic('tool1'); }); expect(mockSetValue).toHaveBeenCalledWith( 'tool_options', { tool2: { defer_loading: true } }, { shouldDirty: true }, ); }); }); describe('areAllToolsDeferred', () => { it('should return false for empty tools array', () => { (useWatch as jest.Mock).mockReturnValue({}); const { result } = renderHook(() => useMCPToolOptions()); expect(result.current.areAllToolsDeferred([])).toBe(false); }); it('should return false when no tools are deferred', () => { (useWatch as jest.Mock).mockReturnValue({}); const { result } = renderHook(() => useMCPToolOptions()); const tools = [createMockTool('tool1'), createMockTool('tool2')]; expect(result.current.areAllToolsDeferred(tools)).toBe(false); }); it('should return false when some tools are deferred', () => { (useWatch as jest.Mock).mockReturnValue({ tool1: { defer_loading: true }, }); const { result } = renderHook(() => useMCPToolOptions()); const tools = [createMockTool('tool1'), createMockTool('tool2')]; expect(result.current.areAllToolsDeferred(tools)).toBe(false); }); it('should return true when all tools are deferred', () => { (useWatch as jest.Mock).mockReturnValue({ tool1: { defer_loading: true }, tool2: { defer_loading: true }, }); const { result } = renderHook(() => useMCPToolOptions()); const tools = [createMockTool('tool1'), createMockTool('tool2')]; expect(result.current.areAllToolsDeferred(tools)).toBe(true); }); }); describe('areAllToolsProgrammatic', () => { it('should return false for empty tools array', () => { (useWatch as jest.Mock).mockReturnValue({}); const { result } = renderHook(() => useMCPToolOptions()); expect(result.current.areAllToolsProgrammatic([])).toBe(false); }); it('should return false when no tools are programmatic', () => { (useWatch as jest.Mock).mockReturnValue({}); const { result } = renderHook(() => useMCPToolOptions()); const tools = [createMockTool('tool1'), createMockTool('tool2')]; expect(result.current.areAllToolsProgrammatic(tools)).toBe(false); }); it('should return false when some tools are programmatic', () => { (useWatch as jest.Mock).mockReturnValue({ tool1: { allowed_callers: ['code_execution'] }, }); const { result } = renderHook(() => useMCPToolOptions()); const tools = [createMockTool('tool1'), createMockTool('tool2')]; expect(result.current.areAllToolsProgrammatic(tools)).toBe(false); }); it('should return true when all tools are programmatic', () => { (useWatch as jest.Mock).mockReturnValue({ tool1: { allowed_callers: ['code_execution'] }, tool2: { allowed_callers: ['code_execution'] }, }); const { result } = renderHook(() => useMCPToolOptions()); const tools = [createMockTool('tool1'), createMockTool('tool2')]; expect(result.current.areAllToolsProgrammatic(tools)).toBe(true); }); }); describe('toggleDeferAll', () => { it('should do nothing for empty tools array', () => { const { result } = renderHook(() => useMCPToolOptions()); act(() => { result.current.toggleDeferAll([]); }); expect(mockSetValue).not.toHaveBeenCalled(); }); it('should defer all tools when none are deferred', () => { (useWatch as jest.Mock).mockReturnValue({}); mockGetValues.mockReturnValue({}); const { result } = renderHook(() => useMCPToolOptions()); const tools = [createMockTool('tool1'), createMockTool('tool2')]; act(() => { result.current.toggleDeferAll(tools); }); expect(mockSetValue).toHaveBeenCalledWith( 'tool_options', { tool1: { defer_loading: true }, tool2: { defer_loading: true }, }, { shouldDirty: true }, ); }); it('should undefer all tools when all are deferred', () => { (useWatch as jest.Mock).mockReturnValue({ tool1: { defer_loading: true }, tool2: { defer_loading: true }, }); mockGetValues.mockReturnValue({ tool1: { defer_loading: true }, tool2: { defer_loading: true }, }); const { result } = renderHook(() => useMCPToolOptions()); const tools = [createMockTool('tool1'), createMockTool('tool2')]; act(() => { result.current.toggleDeferAll(tools); }); expect(mockSetValue).toHaveBeenCalledWith('tool_options', {}, { shouldDirty: true }); }); it('should defer all when some are deferred (brings to consistent state)', () => { (useWatch as jest.Mock).mockReturnValue({ tool1: { defer_loading: true }, }); mockGetValues.mockReturnValue({ tool1: { defer_loading: true }, }); const { result } = renderHook(() => useMCPToolOptions()); const tools = [createMockTool('tool1'), createMockTool('tool2')]; act(() => { result.current.toggleDeferAll(tools); }); expect(mockSetValue).toHaveBeenCalledWith( 'tool_options', { tool1: { defer_loading: true }, tool2: { defer_loading: true }, }, { shouldDirty: true }, ); }); it('should preserve other options when deferring', () => { (useWatch as jest.Mock).mockReturnValue({}); mockGetValues.mockReturnValue({ tool1: { allowed_callers: ['code_execution'] }, }); const { result } = renderHook(() => useMCPToolOptions()); const tools = [createMockTool('tool1'), createMockTool('tool2')]; act(() => { result.current.toggleDeferAll(tools); }); expect(mockSetValue).toHaveBeenCalledWith( 'tool_options', { tool1: { allowed_callers: ['code_execution'], defer_loading: true }, tool2: { defer_loading: true }, }, { shouldDirty: true }, ); }); it('should preserve other options when undeferring', () => { (useWatch as jest.Mock).mockReturnValue({ tool1: { defer_loading: true, allowed_callers: ['code_execution'] }, tool2: { defer_loading: true }, }); mockGetValues.mockReturnValue({ tool1: { defer_loading: true, allowed_callers: ['code_execution'] }, tool2: { defer_loading: true }, }); const { result } = renderHook(() => useMCPToolOptions()); const tools = [createMockTool('tool1'), createMockTool('tool2')]; act(() => { result.current.toggleDeferAll(tools); }); expect(mockSetValue).toHaveBeenCalledWith( 'tool_options', { tool1: { allowed_callers: ['code_execution'] } }, { shouldDirty: true }, ); }); }); describe('toggleProgrammaticAll', () => { it('should do nothing for empty tools array', () => { const { result } = renderHook(() => useMCPToolOptions()); act(() => { result.current.toggleProgrammaticAll([]); }); expect(mockSetValue).not.toHaveBeenCalled(); }); it('should make all tools programmatic when none are', () => { (useWatch as jest.Mock).mockReturnValue({}); mockGetValues.mockReturnValue({}); const { result } = renderHook(() => useMCPToolOptions()); const tools = [createMockTool('tool1'), createMockTool('tool2')]; act(() => { result.current.toggleProgrammaticAll(tools); }); expect(mockSetValue).toHaveBeenCalledWith( 'tool_options', { tool1: { allowed_callers: ['code_execution'] }, tool2: { allowed_callers: ['code_execution'] }, }, { shouldDirty: true }, ); }); it('should remove programmatic from all tools when all are programmatic', () => { (useWatch as jest.Mock).mockReturnValue({ tool1: { allowed_callers: ['code_execution'] }, tool2: { allowed_callers: ['code_execution'] }, }); mockGetValues.mockReturnValue({ tool1: { allowed_callers: ['code_execution'] }, tool2: { allowed_callers: ['code_execution'] }, }); const { result } = renderHook(() => useMCPToolOptions()); const tools = [createMockTool('tool1'), createMockTool('tool2')]; act(() => { result.current.toggleProgrammaticAll(tools); }); expect(mockSetValue).toHaveBeenCalledWith('tool_options', {}, { shouldDirty: true }); }); it('should make all programmatic when some are (brings to consistent state)', () => { (useWatch as jest.Mock).mockReturnValue({ tool1: { allowed_callers: ['code_execution'] }, }); mockGetValues.mockReturnValue({ tool1: { allowed_callers: ['code_execution'] }, }); const { result } = renderHook(() => useMCPToolOptions()); const tools = [createMockTool('tool1'), createMockTool('tool2')]; act(() => { result.current.toggleProgrammaticAll(tools); }); expect(mockSetValue).toHaveBeenCalledWith( 'tool_options', { tool1: { allowed_callers: ['code_execution'] }, tool2: { allowed_callers: ['code_execution'] }, }, { shouldDirty: true }, ); }); it('should preserve defer_loading when making programmatic', () => { (useWatch as jest.Mock).mockReturnValue({}); mockGetValues.mockReturnValue({ tool1: { defer_loading: true }, }); const { result } = renderHook(() => useMCPToolOptions()); const tools = [createMockTool('tool1'), createMockTool('tool2')]; act(() => { result.current.toggleProgrammaticAll(tools); }); expect(mockSetValue).toHaveBeenCalledWith( 'tool_options', { tool1: { defer_loading: true, allowed_callers: ['code_execution'] }, tool2: { allowed_callers: ['code_execution'] }, }, { shouldDirty: true }, ); }); it('should preserve defer_loading when removing programmatic', () => { (useWatch as jest.Mock).mockReturnValue({ tool1: { defer_loading: true, allowed_callers: ['code_execution'] }, tool2: { allowed_callers: ['code_execution'] }, }); mockGetValues.mockReturnValue({ tool1: { defer_loading: true, allowed_callers: ['code_execution'] }, tool2: { allowed_callers: ['code_execution'] }, }); const { result } = renderHook(() => useMCPToolOptions()); const tools = [createMockTool('tool1'), createMockTool('tool2')]; act(() => { result.current.toggleProgrammaticAll(tools); }); expect(mockSetValue).toHaveBeenCalledWith( 'tool_options', { tool1: { defer_loading: true } }, { shouldDirty: true }, ); }); }); describe('formToolOptions', () => { it('should return undefined when useWatch returns undefined', () => { (useWatch as jest.Mock).mockReturnValue(undefined); const { result } = renderHook(() => useMCPToolOptions()); expect(result.current.formToolOptions).toBeUndefined(); }); it('should return the tool options from useWatch', () => { const toolOptions = { tool1: { defer_loading: true }, tool2: { allowed_callers: ['code_execution'] }, }; (useWatch as jest.Mock).mockReturnValue(toolOptions); const { result } = renderHook(() => useMCPToolOptions()); expect(result.current.formToolOptions).toEqual(toolOptions); }); }); });