2025-09-15 07:35:15 -07:00
|
|
|
import React from 'react';
|
|
|
|
|
import { Provider, createStore } from 'jotai';
|
|
|
|
|
import { renderHook, act, waitFor } from '@testing-library/react';
|
|
|
|
|
import { RecoilRoot, useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
|
|
|
|
import { Constants, LocalStorageKeys } from 'librechat-data-provider';
|
|
|
|
|
import { ephemeralAgentByConvoId } from '~/store';
|
|
|
|
|
import { setTimestamp } from '~/utils/timestamps';
|
|
|
|
|
import { useMCPSelect } from '../useMCPSelect';
|
2025-11-26 21:26:40 +01:00
|
|
|
import { MCPServerDefinition } from '../useMCPServerManager';
|
2025-09-15 07:35:15 -07:00
|
|
|
|
|
|
|
|
// Mock dependencies
|
|
|
|
|
jest.mock('~/utils/timestamps', () => ({
|
|
|
|
|
setTimestamp: jest.fn(),
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
jest.mock('lodash/isEqual', () => jest.fn((a, b) => JSON.stringify(a) === JSON.stringify(b)));
|
|
|
|
|
|
2025-11-26 21:26:40 +01:00
|
|
|
// Helper to create MCPServerDefinition objects
|
|
|
|
|
const createMCPServers = (serverNames: string[]): MCPServerDefinition[] => {
|
|
|
|
|
return serverNames.map((serverName) => ({
|
|
|
|
|
serverName,
|
|
|
|
|
config: {
|
|
|
|
|
url: 'http://mcp',
|
|
|
|
|
},
|
|
|
|
|
effectivePermissions: 15, // All permissions (VIEW=1, EDIT=2, DELETE=4, SHARE=8)
|
|
|
|
|
}));
|
|
|
|
|
};
|
2025-10-27 02:48:23 +01:00
|
|
|
|
|
|
|
|
const createWrapper = (mcpServers: string[] = []) => {
|
2025-09-15 07:35:15 -07:00
|
|
|
// Create a new Jotai store for each test to ensure clean state
|
|
|
|
|
const store = createStore();
|
2025-11-26 21:26:40 +01:00
|
|
|
const servers = createMCPServers(mcpServers);
|
2025-10-27 02:48:23 +01:00
|
|
|
|
2025-09-15 07:35:15 -07:00
|
|
|
const Wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
|
|
|
|
|
<RecoilRoot>
|
|
|
|
|
<Provider store={store}>{children}</Provider>
|
|
|
|
|
</RecoilRoot>
|
|
|
|
|
);
|
2025-11-26 21:26:40 +01:00
|
|
|
return { Wrapper, servers };
|
2025-09-15 07:35:15 -07:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
describe('useMCPSelect', () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
jest.clearAllMocks();
|
|
|
|
|
localStorage.clear();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Basic Functionality', () => {
|
|
|
|
|
it('should initialize with default values', () => {
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper, servers } = createWrapper();
|
|
|
|
|
const { result } = renderHook(() => useMCPSelect({ servers }), {
|
|
|
|
|
wrapper: Wrapper,
|
2025-09-15 07:35:15 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.current.mcpValues).toEqual([]);
|
|
|
|
|
expect(result.current.isPinned).toBe(true); // Default value from mcpPinnedAtom is true
|
|
|
|
|
expect(typeof result.current.setMCPValues).toBe('function');
|
|
|
|
|
expect(typeof result.current.setIsPinned).toBe('function');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should use conversationId when provided', () => {
|
|
|
|
|
const conversationId = 'test-convo-123';
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper, servers } = createWrapper();
|
|
|
|
|
const { result } = renderHook(() => useMCPSelect({ conversationId, servers }), {
|
|
|
|
|
wrapper: Wrapper,
|
2025-09-15 07:35:15 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.current.mcpValues).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should use NEW_CONVO constant when conversationId is null', () => {
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper, servers } = createWrapper();
|
|
|
|
|
const { result } = renderHook(() => useMCPSelect({ conversationId: null, servers }), {
|
|
|
|
|
wrapper: Wrapper,
|
2025-09-15 07:35:15 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.current.mcpValues).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('State Updates', () => {
|
|
|
|
|
it('should update mcpValues when setMCPValues is called', async () => {
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper, servers } = createWrapper(['value1', 'value2']);
|
|
|
|
|
const { result } = renderHook(() => useMCPSelect({ servers }), {
|
|
|
|
|
wrapper: Wrapper,
|
2025-09-15 07:35:15 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const newValues = ['value1', 'value2'];
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.setMCPValues(newValues);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(result.current.mcpValues).toEqual(newValues);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not update mcpValues if non-array is passed', () => {
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper, servers } = createWrapper();
|
|
|
|
|
const { result } = renderHook(() => useMCPSelect({ servers }), {
|
|
|
|
|
wrapper: Wrapper,
|
2025-09-15 07:35:15 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
// @ts-ignore - Testing invalid input
|
|
|
|
|
result.current.setMCPValues('not-an-array');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.current.mcpValues).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should update isPinned state', () => {
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper, servers } = createWrapper();
|
|
|
|
|
const { result } = renderHook(() => useMCPSelect({ servers }), {
|
|
|
|
|
wrapper: Wrapper,
|
2025-09-15 07:35:15 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Default is true
|
|
|
|
|
expect(result.current.isPinned).toBe(true);
|
|
|
|
|
|
|
|
|
|
// Toggle to false
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.setIsPinned(false);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.current.isPinned).toBe(false);
|
|
|
|
|
|
|
|
|
|
// Toggle back to true
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.setIsPinned(true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.current.isPinned).toBe(true);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Timestamp Management', () => {
|
|
|
|
|
it('should set timestamp when mcpValues is updated with values', async () => {
|
|
|
|
|
const conversationId = 'test-convo';
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper, servers } = createWrapper();
|
|
|
|
|
const { result } = renderHook(() => useMCPSelect({ conversationId, servers }), {
|
|
|
|
|
wrapper: Wrapper,
|
2025-09-15 07:35:15 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const newValues = ['value1', 'value2'];
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.setMCPValues(newValues);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
const expectedKey = `${LocalStorageKeys.LAST_MCP_}${conversationId}`;
|
|
|
|
|
expect(setTimestamp).toHaveBeenCalledWith(expectedKey);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not set timestamp when mcpValues is empty', async () => {
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper, servers } = createWrapper();
|
|
|
|
|
const { result } = renderHook(() => useMCPSelect({ servers }), {
|
|
|
|
|
wrapper: Wrapper,
|
2025-09-15 07:35:15 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.setMCPValues([]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(setTimestamp).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Race Conditions and Infinite Loops Prevention', () => {
|
|
|
|
|
it('should not create infinite loop when syncing between Jotai and Recoil states', async () => {
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper, servers } = createWrapper();
|
|
|
|
|
const { result, rerender } = renderHook(() => useMCPSelect({ servers }), {
|
|
|
|
|
wrapper: Wrapper,
|
2025-09-15 07:35:15 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
let renderCount = 0;
|
|
|
|
|
const maxRenders = 10;
|
|
|
|
|
|
|
|
|
|
// Track renders to detect infinite loops
|
|
|
|
|
const trackRender = () => {
|
|
|
|
|
renderCount++;
|
|
|
|
|
if (renderCount > maxRenders) {
|
|
|
|
|
throw new Error('Potential infinite loop detected');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Set initial value
|
|
|
|
|
act(() => {
|
|
|
|
|
trackRender();
|
|
|
|
|
result.current.setMCPValues(['initial']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Trigger multiple rerenders
|
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
|
|
|
rerender();
|
|
|
|
|
trackRender();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Should not exceed max renders
|
|
|
|
|
expect(renderCount).toBeLessThanOrEqual(maxRenders);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle rapid consecutive updates without race conditions', async () => {
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper, servers } = createWrapper();
|
|
|
|
|
const { result } = renderHook(() => useMCPSelect({ servers }), {
|
|
|
|
|
wrapper: Wrapper,
|
2025-09-15 07:35:15 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const updates = [
|
|
|
|
|
['value1'],
|
|
|
|
|
['value1', 'value2'],
|
|
|
|
|
['value1', 'value2', 'value3'],
|
|
|
|
|
['value4'],
|
|
|
|
|
[],
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// Rapid fire updates
|
|
|
|
|
act(() => {
|
|
|
|
|
updates.forEach((update) => {
|
|
|
|
|
result.current.setMCPValues(update);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
// Should settle on the last update
|
|
|
|
|
expect(result.current.mcpValues).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should maintain stable setter function reference', () => {
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper, servers } = createWrapper();
|
|
|
|
|
const { result, rerender } = renderHook(() => useMCPSelect({ servers }), {
|
|
|
|
|
wrapper: Wrapper,
|
2025-09-15 07:35:15 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const firstSetMCPValues = result.current.setMCPValues;
|
|
|
|
|
|
|
|
|
|
// Trigger multiple rerenders
|
|
|
|
|
rerender();
|
|
|
|
|
rerender();
|
|
|
|
|
rerender();
|
|
|
|
|
|
|
|
|
|
// Setter should remain the same reference (memoized)
|
|
|
|
|
expect(result.current.setMCPValues).toBe(firstSetMCPValues);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle switching conversation IDs without issues', async () => {
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper, servers } = createWrapper(['convo1-value', 'convo2-value']);
|
2025-09-15 07:35:15 -07:00
|
|
|
const { result, rerender } = renderHook(
|
2025-11-26 21:26:40 +01:00
|
|
|
({ conversationId }) => useMCPSelect({ conversationId, servers }),
|
2025-09-15 07:35:15 -07:00
|
|
|
{
|
2025-11-26 21:26:40 +01:00
|
|
|
wrapper: Wrapper,
|
2025-09-15 07:35:15 -07:00
|
|
|
initialProps: { conversationId: 'convo1' },
|
|
|
|
|
},
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
// Set values for first conversation
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.setMCPValues(['convo1-value']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(result.current.mcpValues).toEqual(['convo1-value']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Switch to different conversation
|
|
|
|
|
rerender({ conversationId: 'convo2' });
|
|
|
|
|
|
|
|
|
|
// Should have different state for new conversation
|
|
|
|
|
expect(result.current.mcpValues).toEqual([]);
|
|
|
|
|
|
|
|
|
|
// Set values for second conversation
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.setMCPValues(['convo2-value']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(result.current.mcpValues).toEqual(['convo2-value']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Switch back to first conversation
|
|
|
|
|
rerender({ conversationId: 'convo1' });
|
|
|
|
|
|
|
|
|
|
// Should maintain separate state
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(result.current.mcpValues).toEqual(['convo1-value']);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Ephemeral Agent Synchronization', () => {
|
|
|
|
|
it('should sync mcpValues when ephemeralAgent is updated externally', async () => {
|
|
|
|
|
// Create a shared wrapper for both hooks to share the same Recoil/Jotai context
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper, servers } = createWrapper(['external-value1', 'external-value2']);
|
2025-09-15 07:35:15 -07:00
|
|
|
|
|
|
|
|
// Create a component that uses both hooks to ensure they share state
|
|
|
|
|
const TestComponent = () => {
|
2025-11-26 21:26:40 +01:00
|
|
|
const mcpHook = useMCPSelect({ servers });
|
2025-09-15 07:35:15 -07:00
|
|
|
const [ephemeralAgent, setEphemeralAgent] = useRecoilState(
|
|
|
|
|
ephemeralAgentByConvoId(Constants.NEW_CONVO),
|
|
|
|
|
);
|
|
|
|
|
return { mcpHook, ephemeralAgent, setEphemeralAgent };
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-26 21:26:40 +01:00
|
|
|
const { result } = renderHook(() => TestComponent(), { wrapper: Wrapper });
|
2025-09-15 07:35:15 -07:00
|
|
|
|
|
|
|
|
// Simulate external update to ephemeralAgent (e.g., from another component)
|
|
|
|
|
const externalMcpValues = ['external-value1', 'external-value2'];
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.setEphemeralAgent({
|
|
|
|
|
mcp: externalMcpValues,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// The hook should sync with the external update
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(result.current.mcpHook.mcpValues).toEqual(externalMcpValues);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-27 02:48:23 +01:00
|
|
|
it('should filter out MCPs not in configured servers', async () => {
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper, servers } = createWrapper(['server1', 'server2']);
|
2025-10-27 02:48:23 +01:00
|
|
|
|
|
|
|
|
const TestComponent = () => {
|
2025-11-26 21:26:40 +01:00
|
|
|
const mcpHook = useMCPSelect({ servers });
|
2025-10-27 02:48:23 +01:00
|
|
|
const setEphemeralAgent = useSetRecoilState(ephemeralAgentByConvoId(Constants.NEW_CONVO));
|
|
|
|
|
return { mcpHook, setEphemeralAgent };
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-26 21:26:40 +01:00
|
|
|
const { result } = renderHook(() => TestComponent(), { wrapper: Wrapper });
|
2025-10-27 02:48:23 +01:00
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.setEphemeralAgent({
|
|
|
|
|
mcp: ['server1', 'removed-server', 'server2'],
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(result.current.mcpHook.mcpValues).toEqual(['server1', 'server2']);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should clear all MCPs when none are in configured servers', async () => {
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper, servers } = createWrapper(['server1', 'server2']);
|
2025-10-27 02:48:23 +01:00
|
|
|
|
|
|
|
|
const TestComponent = () => {
|
2025-11-26 21:26:40 +01:00
|
|
|
const mcpHook = useMCPSelect({ servers });
|
2025-10-27 02:48:23 +01:00
|
|
|
const setEphemeralAgent = useSetRecoilState(ephemeralAgentByConvoId(Constants.NEW_CONVO));
|
|
|
|
|
return { mcpHook, setEphemeralAgent };
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-26 21:26:40 +01:00
|
|
|
const { result } = renderHook(() => TestComponent(), { wrapper: Wrapper });
|
2025-10-27 02:48:23 +01:00
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.setEphemeralAgent({
|
|
|
|
|
mcp: ['removed1', 'removed2', 'removed3'],
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(result.current.mcpHook.mcpValues).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should keep all MCPs when all are in configured servers', async () => {
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper, servers } = createWrapper(['server1', 'server2', 'server3']);
|
2025-10-27 02:48:23 +01:00
|
|
|
|
|
|
|
|
const TestComponent = () => {
|
2025-11-26 21:26:40 +01:00
|
|
|
const mcpHook = useMCPSelect({ servers });
|
2025-10-27 02:48:23 +01:00
|
|
|
const setEphemeralAgent = useSetRecoilState(ephemeralAgentByConvoId(Constants.NEW_CONVO));
|
|
|
|
|
return { mcpHook, setEphemeralAgent };
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-26 21:26:40 +01:00
|
|
|
const { result } = renderHook(() => TestComponent(), { wrapper: Wrapper });
|
2025-10-27 02:48:23 +01:00
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.setEphemeralAgent({
|
|
|
|
|
mcp: ['server1', 'server2'],
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(result.current.mcpHook.mcpValues).toEqual(['server1', 'server2']);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-09-15 07:35:15 -07:00
|
|
|
it('should update ephemeralAgent when mcpValues changes through hook', async () => {
|
|
|
|
|
// Create a shared wrapper for both hooks
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper, servers } = createWrapper(['hook-value1', 'hook-value2']);
|
2025-09-15 07:35:15 -07:00
|
|
|
|
|
|
|
|
// Create a component that uses both the hook and accesses Recoil state
|
|
|
|
|
const TestComponent = () => {
|
2025-11-26 21:26:40 +01:00
|
|
|
const mcpHook = useMCPSelect({ servers });
|
2025-09-15 07:35:15 -07:00
|
|
|
const ephemeralAgent = useRecoilValue(ephemeralAgentByConvoId(Constants.NEW_CONVO));
|
|
|
|
|
return { mcpHook, ephemeralAgent };
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-26 21:26:40 +01:00
|
|
|
const { result } = renderHook(() => TestComponent(), { wrapper: Wrapper });
|
2025-09-15 07:35:15 -07:00
|
|
|
|
|
|
|
|
const newValues = ['hook-value1', 'hook-value2'];
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.mcpHook.setMCPValues(newValues);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Verify both mcpValues and ephemeralAgent are updated
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(result.current.mcpHook.mcpValues).toEqual(newValues);
|
|
|
|
|
expect(result.current.ephemeralAgent?.mcp).toEqual(newValues);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle empty ephemeralAgent.mcp array correctly', async () => {
|
|
|
|
|
// Create a shared wrapper
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper, servers } = createWrapper(['initial-value']);
|
2025-09-15 07:35:15 -07:00
|
|
|
|
|
|
|
|
// Create a component that uses both hooks
|
|
|
|
|
const TestComponent = () => {
|
2025-11-26 21:26:40 +01:00
|
|
|
const mcpHook = useMCPSelect({ servers });
|
2025-09-15 07:35:15 -07:00
|
|
|
const setEphemeralAgent = useSetRecoilState(ephemeralAgentByConvoId(Constants.NEW_CONVO));
|
|
|
|
|
return { mcpHook, setEphemeralAgent };
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-26 21:26:40 +01:00
|
|
|
const { result } = renderHook(() => TestComponent(), { wrapper: Wrapper });
|
2025-09-15 07:35:15 -07:00
|
|
|
|
|
|
|
|
// Set initial values
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.mcpHook.setMCPValues(['initial-value']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(result.current.mcpHook.mcpValues).toEqual(['initial-value']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Try to set empty array externally
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.setEphemeralAgent({
|
|
|
|
|
mcp: [],
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2025-10-27 20:33:51 -04:00
|
|
|
// Values should remain unchanged since empty mcp array doesn't trigger update
|
|
|
|
|
// (due to the condition: ephemeralAgent?.mcp && ephemeralAgent.mcp.length > 0)
|
|
|
|
|
expect(result.current.mcpHook.mcpValues).toEqual(['initial-value']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle ephemeralAgent with clear mcp value', async () => {
|
|
|
|
|
// Create a shared wrapper
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper, servers } = createWrapper(['server1', 'server2']);
|
2025-10-27 20:33:51 -04:00
|
|
|
|
|
|
|
|
// Create a component that uses both hooks
|
|
|
|
|
const TestComponent = () => {
|
2025-11-26 21:26:40 +01:00
|
|
|
const mcpHook = useMCPSelect({ servers });
|
2025-10-27 20:33:51 -04:00
|
|
|
const setEphemeralAgent = useSetRecoilState(ephemeralAgentByConvoId(Constants.NEW_CONVO));
|
|
|
|
|
return { mcpHook, setEphemeralAgent };
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-26 21:26:40 +01:00
|
|
|
const { result } = renderHook(() => TestComponent(), { wrapper: Wrapper });
|
2025-10-27 20:33:51 -04:00
|
|
|
|
|
|
|
|
// Set initial values
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.mcpHook.setMCPValues(['server1', 'server2']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(result.current.mcpHook.mcpValues).toEqual(['server1', 'server2']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Set ephemeralAgent with clear value
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.setEphemeralAgent({
|
|
|
|
|
mcp: [Constants.mcp_clear as string],
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// mcpValues should be cleared
|
2025-10-27 19:46:30 -04:00
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(result.current.mcpHook.mcpValues).toEqual([]);
|
|
|
|
|
});
|
2025-09-15 07:35:15 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should properly sync non-empty arrays from ephemeralAgent', async () => {
|
|
|
|
|
// Additional test to ensure non-empty arrays DO sync
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper, servers } = createWrapper([
|
|
|
|
|
'value1',
|
|
|
|
|
'value2',
|
|
|
|
|
'value3',
|
|
|
|
|
'value4',
|
|
|
|
|
'value5',
|
|
|
|
|
]);
|
2025-09-15 07:35:15 -07:00
|
|
|
|
|
|
|
|
const TestComponent = () => {
|
2025-11-26 21:26:40 +01:00
|
|
|
const mcpHook = useMCPSelect({ servers });
|
2025-09-15 07:35:15 -07:00
|
|
|
const setEphemeralAgent = useSetRecoilState(ephemeralAgentByConvoId(Constants.NEW_CONVO));
|
|
|
|
|
return { mcpHook, setEphemeralAgent };
|
|
|
|
|
};
|
|
|
|
|
|
2025-11-26 21:26:40 +01:00
|
|
|
const { result } = renderHook(() => TestComponent(), { wrapper: Wrapper });
|
2025-09-15 07:35:15 -07:00
|
|
|
|
|
|
|
|
// Set initial values through ephemeralAgent with non-empty array
|
|
|
|
|
const initialValues = ['value1', 'value2'];
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.setEphemeralAgent({
|
|
|
|
|
mcp: initialValues,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Should sync since it's non-empty
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(result.current.mcpHook.mcpValues).toEqual(initialValues);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Update with different non-empty values
|
|
|
|
|
const updatedValues = ['value3', 'value4', 'value5'];
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.setEphemeralAgent({
|
|
|
|
|
mcp: updatedValues,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Should sync the new values
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(result.current.mcpHook.mcpValues).toEqual(updatedValues);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Edge Cases', () => {
|
|
|
|
|
it('should handle undefined conversationId', () => {
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper, servers } = createWrapper(['test']);
|
|
|
|
|
const { result } = renderHook(() => useMCPSelect({ conversationId: undefined, servers }), {
|
|
|
|
|
wrapper: Wrapper,
|
2025-09-15 07:35:15 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.current.mcpValues).toEqual([]);
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.setMCPValues(['test']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(() => result.current).not.toThrow();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle empty string conversationId', () => {
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper, servers } = createWrapper();
|
|
|
|
|
const { result } = renderHook(() => useMCPSelect({ conversationId: '', servers }), {
|
|
|
|
|
wrapper: Wrapper,
|
2025-09-15 07:35:15 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(result.current.mcpValues).toEqual([]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle very large arrays without performance issues', async () => {
|
2025-10-27 02:48:23 +01:00
|
|
|
const largeArray = Array.from({ length: 1000 }, (_, i) => `value-${i}`);
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper, servers } = createWrapper(largeArray);
|
|
|
|
|
const { result } = renderHook(() => useMCPSelect({ servers }), {
|
|
|
|
|
wrapper: Wrapper,
|
2025-09-15 07:35:15 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const startTime = performance.now();
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.setMCPValues(largeArray);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const endTime = performance.now();
|
|
|
|
|
const executionTime = endTime - startTime;
|
|
|
|
|
|
|
|
|
|
// Should complete within reasonable time (< 100ms)
|
|
|
|
|
expect(executionTime).toBeLessThan(100);
|
|
|
|
|
|
|
|
|
|
await waitFor(() => {
|
|
|
|
|
expect(result.current.mcpValues).toEqual(largeArray);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should cleanup properly on unmount', () => {
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper, servers } = createWrapper();
|
|
|
|
|
const { unmount } = renderHook(() => useMCPSelect({ servers }), {
|
|
|
|
|
wrapper: Wrapper,
|
2025-09-15 07:35:15 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Should unmount without errors
|
|
|
|
|
expect(() => unmount()).not.toThrow();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Memory Leak Prevention', () => {
|
|
|
|
|
it('should not leak memory on repeated updates', async () => {
|
2025-10-27 02:48:23 +01:00
|
|
|
const values = Array.from({ length: 100 }, (_, i) => `value-${i}`);
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper, servers } = createWrapper(values);
|
|
|
|
|
const { result } = renderHook(() => useMCPSelect({ servers }), {
|
|
|
|
|
wrapper: Wrapper,
|
2025-09-15 07:35:15 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Perform many updates to test for memory leaks
|
|
|
|
|
for (let i = 0; i < 100; i++) {
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.setMCPValues([`value-${i}`]);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// If we get here without crashing, memory management is likely OK
|
|
|
|
|
expect(result.current.mcpValues).toEqual(['value-99']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle component remounting', () => {
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper, servers } = createWrapper();
|
|
|
|
|
const { result, unmount } = renderHook(() => useMCPSelect({ servers }), {
|
|
|
|
|
wrapper: Wrapper,
|
2025-09-15 07:35:15 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
act(() => {
|
|
|
|
|
result.current.setMCPValues(['before-unmount']);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
unmount();
|
|
|
|
|
|
|
|
|
|
// Remount
|
2025-11-26 21:26:40 +01:00
|
|
|
const { Wrapper: Wrapper2, servers: servers2 } = createWrapper();
|
|
|
|
|
const { result: newResult } = renderHook(() => useMCPSelect({ servers: servers2 }), {
|
|
|
|
|
wrapper: Wrapper2,
|
2025-09-15 07:35:15 -07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Should handle remounting gracefully
|
|
|
|
|
expect(newResult.current.mcpValues).toBeDefined();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|