LibreChat/client/src/hooks/Files/__tests__/useFileHandling.test.ts

286 lines
8.6 KiB
TypeScript
Raw Normal View History

2026-02-28 15:01:51 -05:00
import { renderHook, act } from '@testing-library/react';
import { Constants, EModelEndpoint, getEndpointFileConfig } from 'librechat-data-provider';
beforeAll(() => {
global.URL.createObjectURL = jest.fn(() => 'blob:mock-url');
global.URL.revokeObjectURL = jest.fn();
});
const mockShowToast = jest.fn();
const mockSetFilesLoading = jest.fn();
const mockMutate = jest.fn();
let mockConversation: Record<string, string | null | undefined> = {};
jest.mock('~/Providers/ChatContext', () => ({
useChatContext: jest.fn(() => ({
files: new Map(),
setFiles: jest.fn(),
setFilesLoading: mockSetFilesLoading,
conversation: mockConversation,
})),
}));
jest.mock('@librechat/client', () => ({
useToastContext: jest.fn(() => ({
showToast: mockShowToast,
})),
}));
jest.mock('recoil', () => ({
...jest.requireActual('recoil'),
useSetRecoilState: jest.fn(() => jest.fn()),
}));
jest.mock('~/store', () => ({
ephemeralAgentByConvoId: jest.fn(() => ({ key: 'mock' })),
}));
jest.mock('@tanstack/react-query', () => ({
useQueryClient: jest.fn(() => ({
getQueryData: jest.fn(),
refetchQueries: jest.fn(),
})),
}));
jest.mock('~/data-provider', () => ({
useGetFileConfig: jest.fn(() => ({ data: null })),
useUploadFileMutation: jest.fn((_opts: Record<string, unknown>) => ({
mutate: mockMutate,
})),
}));
jest.mock('~/hooks/useLocalize', () => {
const fn = jest.fn((key: string) => key);
fn.TranslationKeys = {};
return { __esModule: true, default: fn, TranslationKeys: {} };
});
jest.mock('../useDelayedUploadToast', () => ({
useDelayedUploadToast: jest.fn(() => ({
startUploadTimer: jest.fn(),
clearUploadTimer: jest.fn(),
})),
}));
jest.mock('~/utils/heicConverter', () => ({
processFileForUpload: jest.fn(async (file: File) => file),
}));
jest.mock('../useClientResize', () => ({
__esModule: true,
default: jest.fn(() => ({
resizeImageIfNeeded: jest.fn(async (file: File) => ({ file, resized: false })),
})),
}));
jest.mock('../useUpdateFiles', () => ({
__esModule: true,
default: jest.fn(() => ({
addFile: jest.fn(),
replaceFile: jest.fn(),
updateFileById: jest.fn(),
deleteFileById: jest.fn(),
})),
}));
jest.mock('~/utils', () => ({
logger: { log: jest.fn() },
validateFiles: jest.fn(() => true),
}));
const mockValidateFiles = jest.requireMock('~/utils').validateFiles;
describe('useFileHandling', () => {
beforeEach(() => {
jest.clearAllMocks();
mockConversation = {};
});
const loadHook = async () => (await import('../useFileHandling')).default;
describe('endpointOverride', () => {
it('uses conversation endpoint when no override is provided', async () => {
mockConversation = {
conversationId: 'convo-1',
endpoint: 'openAI',
endpointType: 'custom',
};
const useFileHandling = await loadHook();
const { result } = renderHook(() => useFileHandling());
const textFile = new File(['hello'], 'test.txt', { type: 'text/plain' });
await act(async () => {
await result.current.handleFiles([textFile]);
});
expect(mockValidateFiles).toHaveBeenCalledTimes(1);
const validateCall = mockValidateFiles.mock.calls[0][0];
const configResult = getEndpointFileConfig({
endpoint: 'openAI',
endpointType: 'custom',
fileConfig: null,
});
expect(validateCall.endpointFileConfig).toEqual(configResult);
});
it('uses endpointOverride for validation instead of conversation endpoint', async () => {
mockConversation = {
conversationId: 'convo-1',
endpoint: 'openAI',
endpointType: 'custom',
};
const useFileHandling = await loadHook();
const { result } = renderHook(() =>
useFileHandling({ endpointOverride: EModelEndpoint.agents }),
);
const textFile = new File(['hello'], 'test.txt', { type: 'text/plain' });
await act(async () => {
await result.current.handleFiles([textFile]);
});
expect(mockValidateFiles).toHaveBeenCalledTimes(1);
const validateCall = mockValidateFiles.mock.calls[0][0];
const agentsConfig = getEndpointFileConfig({
endpoint: EModelEndpoint.agents,
endpointType: EModelEndpoint.agents,
fileConfig: null,
});
expect(validateCall.endpointFileConfig).toEqual(agentsConfig);
});
it('falls back to conversation endpoint when endpointOverride is undefined', async () => {
mockConversation = {
conversationId: 'convo-1',
endpoint: 'anthropic',
endpointType: undefined,
};
const useFileHandling = await loadHook();
const { result } = renderHook(() => useFileHandling({ endpointOverride: undefined }));
const textFile = new File(['hello'], 'test.txt', { type: 'text/plain' });
await act(async () => {
await result.current.handleFiles([textFile]);
});
expect(mockValidateFiles).toHaveBeenCalledTimes(1);
const validateCall = mockValidateFiles.mock.calls[0][0];
const anthropicConfig = getEndpointFileConfig({
endpoint: 'anthropic',
endpointType: undefined,
fileConfig: null,
});
expect(validateCall.endpointFileConfig).toEqual(anthropicConfig);
});
it('sends correct endpoint in upload form data when override is set', async () => {
mockConversation = {
conversationId: 'convo-1',
endpoint: 'openAI',
endpointType: 'custom',
};
const useFileHandling = await loadHook();
const { result } = renderHook(() =>
useFileHandling({
endpointOverride: EModelEndpoint.agents,
additionalMetadata: { agent_id: 'agent-123' },
}),
);
const textFile = new File(['hello'], 'test.txt', { type: 'text/plain' });
await act(async () => {
await result.current.handleFiles([textFile]);
});
expect(mockMutate).toHaveBeenCalledTimes(1);
const formData: FormData = mockMutate.mock.calls[0][0];
expect(formData.get('endpoint')).toBe(EModelEndpoint.agents);
expect(formData.get('endpointType')).toBe(EModelEndpoint.agents);
});
it('does not enter assistants upload path when override is agents', async () => {
mockConversation = {
conversationId: 'convo-1',
endpoint: 'assistants',
endpointType: 'assistants',
};
const useFileHandling = await loadHook();
const { result } = renderHook(() =>
useFileHandling({
endpointOverride: EModelEndpoint.agents,
additionalMetadata: { agent_id: 'agent-123' },
}),
);
const textFile = new File(['hello'], 'test.txt', { type: 'text/plain' });
await act(async () => {
await result.current.handleFiles([textFile]);
});
expect(mockMutate).toHaveBeenCalledTimes(1);
const formData: FormData = mockMutate.mock.calls[0][0];
expect(formData.get('endpoint')).toBe(EModelEndpoint.agents);
expect(formData.get('message_file')).toBeNull();
expect(formData.get('version')).toBeNull();
expect(formData.get('model')).toBeNull();
expect(formData.get('assistant_id')).toBeNull();
});
it('enters assistants path without override when conversation is assistants', async () => {
mockConversation = {
conversationId: 'convo-1',
endpoint: 'assistants',
endpointType: 'assistants',
assistant_id: 'asst-456',
model: 'gpt-4',
};
const useFileHandling = await loadHook();
const { result } = renderHook(() => useFileHandling());
const textFile = new File(['hello'], 'test.txt', { type: 'text/plain' });
await act(async () => {
await result.current.handleFiles([textFile]);
});
expect(mockMutate).toHaveBeenCalledTimes(1);
const formData: FormData = mockMutate.mock.calls[0][0];
expect(formData.get('endpoint')).toBe('assistants');
expect(formData.get('message_file')).toBe('true');
});
it('falls back to "default" when no conversation endpoint and no override', async () => {
mockConversation = {
conversationId: Constants.NEW_CONVO,
endpoint: null,
endpointType: undefined,
};
const useFileHandling = await loadHook();
const { result } = renderHook(() => useFileHandling());
const textFile = new File(['hello'], 'test.txt', { type: 'text/plain' });
await act(async () => {
await result.current.handleFiles([textFile]);
});
expect(mockMutate).toHaveBeenCalledTimes(1);
const formData: FormData = mockMutate.mock.calls[0][0];
expect(formData.get('endpoint')).toBe('default');
});
});
});