UNPKG

@lobehub/chat

Version:

Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.

911 lines (721 loc) • 31.3 kB
import { act, renderHook, waitFor } from '@testing-library/react'; import { mutate } from 'swr'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { message } from '@/components/AntdStaticMethods'; import { FILE_UPLOAD_BLACKLIST, MAX_UPLOAD_FILE_COUNT } from '@/const/file'; import { lambdaClient } from '@/libs/trpc/client'; import { fileService } from '@/services/file'; import { ragService } from '@/services/rag'; import { FileListItem } from '@/types/files'; import { UploadFileItem } from '@/types/files/upload'; import { unzipFile } from '@/utils/unzipFile'; import { useFileStore as useStore } from '../../store'; vi.mock('zustand/traditional'); // Mock i18next translation function vi.mock('i18next', () => ({ t: (key: string, options?: any) => { // Return a mock translation string that includes the options for verification if (key === 'uploadDock.fileQueueInfo' && options?.count !== undefined) { return `Uploading ${options.count} files, ${options.remaining} queued`; } return key; }, })); // Mock message vi.mock('@/components/AntdStaticMethods', () => ({ message: { info: vi.fn(), warning: vi.fn(), }, })); // Mock unzipFile vi.mock('@/utils/unzipFile', () => ({ unzipFile: vi.fn(), })); // Mock p-map to run sequentially for easier testing vi.mock('p-map', () => ({ default: vi.fn(async (items, mapper) => { return Promise.all(items.map(mapper)); }), })); // Mock SWR vi.mock('swr', async () => { const actual = await vi.importActual('swr'); return { ...actual, mutate: vi.fn(), }; }); // Mock lambdaClient vi.mock('@/libs/trpc/client', () => ({ lambdaClient: { file: { getFileItemById: { query: vi.fn() }, getFiles: { query: vi.fn() }, removeFileAsyncTask: { mutate: vi.fn() }, }, }, })); beforeEach(() => { vi.clearAllMocks(); useStore.setState( { creatingChunkingTaskIds: [], creatingEmbeddingTaskIds: [], dockUploadFileList: [], fileList: [], queryListParams: undefined, }, false, ); }); afterEach(() => { vi.restoreAllMocks(); }); describe('FileManagerActions', () => { describe('dispatchDockFileList', () => { it('should update dockUploadFileList with new value', () => { const { result } = renderHook(() => useStore()); act(() => { result.current.dispatchDockFileList({ atStart: true, files: [{ file: new File([], 'test.txt'), id: 'file-1', status: 'pending' }], type: 'addFiles', }); }); expect(result.current.dockUploadFileList).toHaveLength(1); expect(result.current.dockUploadFileList[0].id).toBe('file-1'); }); it('should not update state if reducer returns same value', () => { const { result } = renderHook(() => useStore()); const initialList = result.current.dockUploadFileList; // This tests the early return when value hasn't changed act(() => { useStore.setState({ dockUploadFileList: initialList }); }); expect(result.current.dockUploadFileList).toBe(initialList); }); it('should handle updateFileStatus dispatch', () => { const { result } = renderHook(() => useStore()); act(() => { useStore.setState({ dockUploadFileList: [ { file: new File([], 'test.txt'), id: 'file-1', status: 'pending' }, ] as UploadFileItem[], }); }); act(() => { result.current.dispatchDockFileList({ id: 'file-1', status: 'success', type: 'updateFileStatus', }); }); expect(result.current.dockUploadFileList[0].status).toBe('success'); }); it('should handle removeFile dispatch', () => { const { result } = renderHook(() => useStore()); act(() => { useStore.setState({ dockUploadFileList: [ { file: new File([], 'test.txt'), id: 'file-1', status: 'pending' }, ] as UploadFileItem[], }); }); act(() => { result.current.dispatchDockFileList({ id: 'file-1', type: 'removeFile', }); }); expect(result.current.dockUploadFileList).toHaveLength(0); }); }); describe('embeddingChunks', () => { it('should toggle embedding ids and create tasks', async () => { const { result } = renderHook(() => useStore()); const createTaskSpy = vi .spyOn(ragService, 'createEmbeddingChunksTask') .mockResolvedValue(undefined as any); const refreshSpy = vi.spyOn(result.current, 'refreshFileList').mockResolvedValue(); const toggleSpy = vi.spyOn(result.current, 'toggleEmbeddingIds'); await act(async () => { await result.current.embeddingChunks(['file-1', 'file-2']); }); expect(toggleSpy).toHaveBeenCalledWith(['file-1', 'file-2']); expect(createTaskSpy).toHaveBeenCalledTimes(2); expect(createTaskSpy).toHaveBeenCalledWith('file-1'); expect(createTaskSpy).toHaveBeenCalledWith('file-2'); expect(refreshSpy).toHaveBeenCalled(); expect(toggleSpy).toHaveBeenCalledWith(['file-1', 'file-2'], false); }); it('should handle errors gracefully and still complete', async () => { const { result } = renderHook(() => useStore()); const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); vi.spyOn(ragService, 'createEmbeddingChunksTask').mockRejectedValue(new Error('Task failed')); const refreshSpy = vi.spyOn(result.current, 'refreshFileList').mockResolvedValue(); const toggleSpy = vi.spyOn(result.current, 'toggleEmbeddingIds'); await act(async () => { await result.current.embeddingChunks(['file-1']); }); expect(consoleErrorSpy).toHaveBeenCalled(); expect(refreshSpy).toHaveBeenCalled(); expect(toggleSpy).toHaveBeenCalledWith(['file-1'], false); }); }); describe('parseFilesToChunks', () => { it('should toggle parsing ids and create parse tasks', async () => { const { result } = renderHook(() => useStore()); const createTaskSpy = vi .spyOn(ragService, 'createParseFileTask') .mockResolvedValue(undefined as any); const refreshSpy = vi.spyOn(result.current, 'refreshFileList').mockResolvedValue(); const toggleSpy = vi.spyOn(result.current, 'toggleParsingIds'); await act(async () => { await result.current.parseFilesToChunks(['file-1', 'file-2']); }); expect(toggleSpy).toHaveBeenCalledWith(['file-1', 'file-2']); expect(createTaskSpy).toHaveBeenCalledTimes(2); expect(createTaskSpy).toHaveBeenCalledWith('file-1', undefined); expect(createTaskSpy).toHaveBeenCalledWith('file-2', undefined); expect(refreshSpy).toHaveBeenCalled(); expect(toggleSpy).toHaveBeenCalledWith(['file-1', 'file-2'], false); }); it('should pass skipExist parameter to createParseFileTask', async () => { const { result } = renderHook(() => useStore()); const createTaskSpy = vi .spyOn(ragService, 'createParseFileTask') .mockResolvedValue(undefined as any); vi.spyOn(result.current, 'refreshFileList').mockResolvedValue(); await act(async () => { await result.current.parseFilesToChunks(['file-1'], { skipExist: true }); }); expect(createTaskSpy).toHaveBeenCalledWith('file-1', true); }); it('should handle errors gracefully', async () => { const { result } = renderHook(() => useStore()); const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); vi.spyOn(ragService, 'createParseFileTask').mockRejectedValue(new Error('Parse failed')); const refreshSpy = vi.spyOn(result.current, 'refreshFileList').mockResolvedValue(); const toggleSpy = vi.spyOn(result.current, 'toggleParsingIds'); await act(async () => { await result.current.parseFilesToChunks(['file-1']); }); expect(consoleErrorSpy).toHaveBeenCalled(); expect(refreshSpy).toHaveBeenCalled(); expect(toggleSpy).toHaveBeenCalledWith(['file-1'], false); }); }); describe('pushDockFileList', () => { it('should filter blacklisted files and upload', async () => { const { result } = renderHook(() => useStore()); const validFile = new File(['content'], 'valid.txt', { type: 'text/plain' }); const blacklistedFile = new File(['content'], FILE_UPLOAD_BLACKLIST[0], { type: 'text/plain', }); const uploadSpy = vi .spyOn(result.current, 'uploadWithProgress') .mockResolvedValue({ id: 'file-1', url: 'http://example.com/file-1' }); const refreshSpy = vi.spyOn(result.current, 'refreshFileList').mockResolvedValue(); const dispatchSpy = vi.spyOn(result.current, 'dispatchDockFileList'); const parseSpy = vi.spyOn(result.current, 'parseFilesToChunks').mockResolvedValue(); await act(async () => { await result.current.pushDockFileList([validFile, blacklistedFile]); }); // Should only dispatch for the valid file expect(dispatchSpy).toHaveBeenCalledWith({ atStart: true, files: [{ file: validFile, id: validFile.name, status: 'pending' }], type: 'addFiles', }); expect(uploadSpy).toHaveBeenCalledTimes(1); expect(uploadSpy).toHaveBeenCalledWith({ file: validFile, knowledgeBaseId: undefined, onStatusUpdate: expect.any(Function), }); expect(refreshSpy).toHaveBeenCalled(); // Should auto-parse text files expect(parseSpy).toHaveBeenCalledWith(['file-1'], { skipExist: false }); }); it('should upload files with knowledgeBaseId', async () => { const { result } = renderHook(() => useStore()); const file = new File(['content'], 'test.txt', { type: 'text/plain' }); const uploadSpy = vi .spyOn(result.current, 'uploadWithProgress') .mockResolvedValue({ id: 'file-1', url: 'http://example.com/file-1' }); vi.spyOn(result.current, 'refreshFileList').mockResolvedValue(); vi.spyOn(result.current, 'parseFilesToChunks').mockResolvedValue(); await act(async () => { await result.current.pushDockFileList([file], 'kb-123'); }); expect(uploadSpy).toHaveBeenCalledWith({ file, knowledgeBaseId: 'kb-123', onStatusUpdate: expect.any(Function), }); }); it('should call onStatusUpdate during upload', async () => { const { result } = renderHook(() => useStore()); const file = new File(['content'], 'test.txt', { type: 'text/plain' }); const uploadSpy = vi .spyOn(result.current, 'uploadWithProgress') .mockImplementation(async ({ onStatusUpdate }) => { onStatusUpdate?.({ id: file.name, type: 'updateFile', value: { status: 'uploading' } }); return { id: 'file-1', url: 'http://example.com/file-1' }; }); vi.spyOn(result.current, 'refreshFileList').mockResolvedValue(); vi.spyOn(result.current, 'parseFilesToChunks').mockResolvedValue(); const dispatchSpy = vi.spyOn(result.current, 'dispatchDockFileList'); await act(async () => { await result.current.pushDockFileList([file]); }); expect(uploadSpy).toHaveBeenCalled(); expect(dispatchSpy).toHaveBeenCalled(); }); it('should handle empty file list', async () => { const { result } = renderHook(() => useStore()); const uploadSpy = vi.spyOn(result.current, 'uploadWithProgress'); const refreshSpy = vi.spyOn(result.current, 'refreshFileList'); const parseSpy = vi.spyOn(result.current, 'parseFilesToChunks'); await act(async () => { await result.current.pushDockFileList([]); }); expect(uploadSpy).not.toHaveBeenCalled(); expect(refreshSpy).not.toHaveBeenCalled(); expect(parseSpy).not.toHaveBeenCalled(); }); it('should auto-embed files that support chunking', async () => { const { result } = renderHook(() => useStore()); const textFile = new File(['text content'], 'doc.txt', { type: 'text/plain' }); const pdfFile = new File(['pdf content'], 'doc.pdf', { type: 'application/pdf' }); vi.spyOn(result.current, 'uploadWithProgress') .mockResolvedValueOnce({ id: 'file-1', url: 'http://example.com/file-1' }) .mockResolvedValueOnce({ id: 'file-2', url: 'http://example.com/file-2' }); vi.spyOn(result.current, 'refreshFileList').mockResolvedValue(); const parseSpy = vi.spyOn(result.current, 'parseFilesToChunks').mockResolvedValue(); await act(async () => { await result.current.pushDockFileList([textFile, pdfFile]); }); // Should auto-parse both files that support chunking expect(parseSpy).toHaveBeenCalledWith(['file-1', 'file-2'], { skipExist: false }); }); it('should skip auto-embed for unsupported file types (images/videos/audio)', async () => { const { result } = renderHook(() => useStore()); const imageFile = new File(['image content'], 'image.png', { type: 'image/png' }); const videoFile = new File(['video content'], 'video.mp4', { type: 'video/mp4' }); const audioFile = new File(['audio content'], 'audio.mp3', { type: 'audio/mpeg' }); vi.spyOn(result.current, 'uploadWithProgress') .mockResolvedValueOnce({ id: 'file-1', url: 'http://example.com/file-1' }) .mockResolvedValueOnce({ id: 'file-2', url: 'http://example.com/file-2' }) .mockResolvedValueOnce({ id: 'file-3', url: 'http://example.com/file-3' }); vi.spyOn(result.current, 'refreshFileList').mockResolvedValue(); const parseSpy = vi.spyOn(result.current, 'parseFilesToChunks').mockResolvedValue(); await act(async () => { await result.current.pushDockFileList([imageFile, videoFile, audioFile]); }); // Should not auto-parse unsupported files expect(parseSpy).not.toHaveBeenCalled(); }); it('should auto-embed only supported files in mixed upload', async () => { const { result } = renderHook(() => useStore()); const textFile = new File(['text content'], 'doc.txt', { type: 'text/plain' }); const imageFile = new File(['image content'], 'image.png', { type: 'image/png' }); const pdfFile = new File(['pdf content'], 'doc.pdf', { type: 'application/pdf' }); vi.spyOn(result.current, 'uploadWithProgress') .mockResolvedValueOnce({ id: 'file-1', url: 'http://example.com/file-1' }) .mockResolvedValueOnce({ id: 'file-2', url: 'http://example.com/file-2' }) .mockResolvedValueOnce({ id: 'file-3', url: 'http://example.com/file-3' }); vi.spyOn(result.current, 'refreshFileList').mockResolvedValue(); const parseSpy = vi.spyOn(result.current, 'parseFilesToChunks').mockResolvedValue(); await act(async () => { await result.current.pushDockFileList([textFile, imageFile, pdfFile]); }); // Should only auto-parse text and pdf files, skip image expect(parseSpy).toHaveBeenCalledWith(['file-1', 'file-3'], { skipExist: false }); }); it('should skip auto-embed when upload fails', async () => { const { result } = renderHook(() => useStore()); const textFile = new File(['text content'], 'doc.txt', { type: 'text/plain' }); vi.spyOn(result.current, 'uploadWithProgress').mockResolvedValue(undefined); vi.spyOn(result.current, 'refreshFileList').mockResolvedValue(); const parseSpy = vi.spyOn(result.current, 'parseFilesToChunks').mockResolvedValue(); await act(async () => { await result.current.pushDockFileList([textFile]); }); // Should not auto-parse when upload returns undefined expect(parseSpy).not.toHaveBeenCalled(); }); it('should enforce file count limit and queue excess files', async () => { const { result } = renderHook(() => useStore()); // Create more files than the limit const totalFiles = MAX_UPLOAD_FILE_COUNT + 5; const files = Array.from( { length: totalFiles }, (_, i) => new File(['content'], `file-${i}.txt`, { type: 'text/plain' }), ); vi.spyOn(result.current, 'uploadWithProgress').mockResolvedValue({ id: 'file-1', url: 'http://example.com/file-1', }); vi.spyOn(result.current, 'refreshFileList').mockResolvedValue(); vi.spyOn(result.current, 'parseFilesToChunks').mockResolvedValue(); const dispatchSpy = vi.spyOn(result.current, 'dispatchDockFileList'); await act(async () => { await result.current.pushDockFileList(files); }); // Should add all files to dock (not just first MAX_UPLOAD_FILE_COUNT) expect(dispatchSpy).toHaveBeenCalledWith({ atStart: true, files: expect.arrayContaining([ expect.objectContaining({ file: expect.any(File), status: 'pending' }), ]), type: 'addFiles', }); // Verify all files were dispatched const dispatchCall = dispatchSpy.mock.calls.find((call) => call[0].type === 'addFiles'); expect(dispatchCall?.[0]).toHaveProperty('files'); if (dispatchCall && 'files' in dispatchCall[0]) { expect(dispatchCall[0].files).toHaveLength(totalFiles); } }); it('should extract ZIP files and upload contents', async () => { const { result } = renderHook(() => useStore()); const zipFile = new File(['zip content'], 'archive.zip', { type: 'application/zip' }); const extractedFiles = [ new File(['file1'], 'file1.txt', { type: 'text/plain' }), new File(['file2'], 'file2.txt', { type: 'text/plain' }), ]; vi.mocked(unzipFile).mockResolvedValue(extractedFiles); vi.spyOn(result.current, 'uploadWithProgress').mockResolvedValue({ id: 'file-1', url: 'http://example.com/file-1', }); vi.spyOn(result.current, 'refreshFileList').mockResolvedValue(); vi.spyOn(result.current, 'parseFilesToChunks').mockResolvedValue(); const dispatchSpy = vi.spyOn(result.current, 'dispatchDockFileList'); await act(async () => { await result.current.pushDockFileList([zipFile]); }); // Should extract ZIP file expect(unzipFile).toHaveBeenCalledWith(zipFile); // Should upload extracted files expect(dispatchSpy).toHaveBeenCalledWith({ atStart: true, files: extractedFiles.map((file) => ({ file, id: file.name, status: 'pending' })), type: 'addFiles', }); }); it('should handle ZIP extraction errors gracefully', async () => { const { result } = renderHook(() => useStore()); const zipFile = new File(['zip content'], 'archive.zip', { type: 'application/zip' }); vi.mocked(unzipFile).mockRejectedValue(new Error('Extraction failed')); const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); vi.spyOn(result.current, 'uploadWithProgress').mockResolvedValue({ id: 'file-1', url: 'http://example.com/file-1', }); vi.spyOn(result.current, 'refreshFileList').mockResolvedValue(); vi.spyOn(result.current, 'parseFilesToChunks').mockResolvedValue(); const dispatchSpy = vi.spyOn(result.current, 'dispatchDockFileList'); await act(async () => { await result.current.pushDockFileList([zipFile]); }); // Should log error expect(consoleErrorSpy).toHaveBeenCalled(); // Should fallback to uploading the ZIP file itself expect(dispatchSpy).toHaveBeenCalledWith({ atStart: true, files: [{ file: zipFile, id: zipFile.name, status: 'pending' }], type: 'addFiles', }); }); }); describe('reEmbeddingChunks', () => { it('should skip if already creating task', async () => { const { result } = renderHook(() => useStore()); act(() => { useStore.setState({ creatingEmbeddingTaskIds: ['file-1'] }); }); const toggleSpy = vi.spyOn(result.current, 'toggleEmbeddingIds'); await act(async () => { await result.current.reEmbeddingChunks('file-1'); }); expect(toggleSpy).not.toHaveBeenCalled(); expect(lambdaClient.file.removeFileAsyncTask.mutate).not.toHaveBeenCalled(); }); it('should remove old task and create new embedding task', async () => { const { result } = renderHook(() => useStore()); const toggleSpy = vi.spyOn(result.current, 'toggleEmbeddingIds'); vi.mocked(lambdaClient.file.removeFileAsyncTask.mutate).mockResolvedValue(undefined as any); const createTaskSpy = vi .spyOn(ragService, 'createEmbeddingChunksTask') .mockResolvedValue(undefined as any); const refreshSpy = vi.spyOn(result.current, 'refreshFileList').mockResolvedValue(); await act(async () => { await result.current.reEmbeddingChunks('file-1'); }); expect(toggleSpy).toHaveBeenCalledWith(['file-1']); expect(lambdaClient.file.removeFileAsyncTask.mutate).toHaveBeenCalledWith({ id: 'file-1', type: 'embedding', }); expect(createTaskSpy).toHaveBeenCalledWith('file-1'); expect(refreshSpy).toHaveBeenCalledTimes(2); expect(toggleSpy).toHaveBeenCalledWith(['file-1'], false); }); }); describe('reParseFile', () => { it('should toggle parsing and retry parse', async () => { const { result } = renderHook(() => useStore()); const toggleSpy = vi.spyOn(result.current, 'toggleParsingIds'); const retrySpy = vi.spyOn(ragService, 'retryParseFile').mockResolvedValue(undefined as any); const refreshSpy = vi.spyOn(result.current, 'refreshFileList').mockResolvedValue(); await act(async () => { await result.current.reParseFile('file-1'); }); expect(toggleSpy).toHaveBeenCalledWith(['file-1']); expect(retrySpy).toHaveBeenCalledWith('file-1'); expect(refreshSpy).toHaveBeenCalled(); expect(toggleSpy).toHaveBeenCalledWith(['file-1'], false); }); }); describe('refreshFileList', () => { it('should call mutate with correct key', async () => { const { result } = renderHook(() => useStore()); const params = { category: 'all' }; act(() => { useStore.setState({ queryListParams: params }); }); await act(async () => { await result.current.refreshFileList(); }); expect(mutate).toHaveBeenCalledWith(['useFetchFileManage', params]); }); it('should call mutate with undefined params', async () => { const { result } = renderHook(() => useStore()); await act(async () => { await result.current.refreshFileList(); }); expect(mutate).toHaveBeenCalledWith(['useFetchFileManage', undefined]); }); }); describe('removeAllFiles', () => { it('should call fileService.removeAllFiles', async () => { const { result } = renderHook(() => useStore()); const removeSpy = vi.spyOn(fileService, 'removeAllFiles').mockResolvedValue(undefined); await act(async () => { await result.current.removeAllFiles(); }); expect(removeSpy).toHaveBeenCalled(); }); }); describe('removeFileItem', () => { it('should remove file and refresh list', async () => { const { result } = renderHook(() => useStore()); const removeSpy = vi.spyOn(fileService, 'removeFile').mockResolvedValue(undefined); const refreshSpy = vi.spyOn(result.current, 'refreshFileList').mockResolvedValue(); await act(async () => { await result.current.removeFileItem('file-1'); }); expect(removeSpy).toHaveBeenCalledWith('file-1'); expect(refreshSpy).toHaveBeenCalled(); }); }); describe('removeFiles', () => { it('should remove multiple files and refresh list', async () => { const { result } = renderHook(() => useStore()); const removeSpy = vi.spyOn(fileService, 'removeFiles').mockResolvedValue(undefined); const refreshSpy = vi.spyOn(result.current, 'refreshFileList').mockResolvedValue(); await act(async () => { await result.current.removeFiles(['file-1', 'file-2']); }); expect(removeSpy).toHaveBeenCalledWith(['file-1', 'file-2']); expect(refreshSpy).toHaveBeenCalled(); }); }); describe('toggleEmbeddingIds', () => { it('should add ids when loading is true', () => { const { result } = renderHook(() => useStore()); act(() => { result.current.toggleEmbeddingIds(['file-1', 'file-2'], true); }); expect(result.current.creatingEmbeddingTaskIds).toEqual(['file-1', 'file-2']); }); it('should remove ids when loading is false', () => { const { result } = renderHook(() => useStore()); act(() => { useStore.setState({ creatingEmbeddingTaskIds: ['file-1', 'file-2', 'file-3'] }); }); act(() => { result.current.toggleEmbeddingIds(['file-1', 'file-2'], false); }); expect(result.current.creatingEmbeddingTaskIds).toEqual(['file-3']); }); it('should toggle ids when loading is undefined', () => { const { result } = renderHook(() => useStore()); act(() => { useStore.setState({ creatingEmbeddingTaskIds: ['file-1'] }); }); act(() => { result.current.toggleEmbeddingIds(['file-1', 'file-2']); }); expect(result.current.creatingEmbeddingTaskIds).toEqual(['file-2']); }); it('should handle empty initial state', () => { const { result } = renderHook(() => useStore()); act(() => { result.current.toggleEmbeddingIds(['file-1'], true); }); expect(result.current.creatingEmbeddingTaskIds).toEqual(['file-1']); }); it('should not duplicate ids', () => { const { result } = renderHook(() => useStore()); act(() => { useStore.setState({ creatingEmbeddingTaskIds: ['file-1'] }); }); act(() => { result.current.toggleEmbeddingIds(['file-1'], true); }); expect(result.current.creatingEmbeddingTaskIds).toEqual(['file-1']); }); }); describe('toggleParsingIds', () => { it('should add ids when loading is true', () => { const { result } = renderHook(() => useStore()); act(() => { result.current.toggleParsingIds(['file-1', 'file-2'], true); }); expect(result.current.creatingChunkingTaskIds).toEqual(['file-1', 'file-2']); }); it('should remove ids when loading is false', () => { const { result } = renderHook(() => useStore()); act(() => { useStore.setState({ creatingChunkingTaskIds: ['file-1', 'file-2', 'file-3'] }); }); act(() => { result.current.toggleParsingIds(['file-1', 'file-2'], false); }); expect(result.current.creatingChunkingTaskIds).toEqual(['file-3']); }); it('should toggle ids when loading is undefined', () => { const { result } = renderHook(() => useStore()); act(() => { useStore.setState({ creatingChunkingTaskIds: ['file-1'] }); }); act(() => { result.current.toggleParsingIds(['file-1', 'file-2']); }); expect(result.current.creatingChunkingTaskIds).toEqual(['file-2']); }); it('should handle empty initial state', () => { const { result } = renderHook(() => useStore()); act(() => { result.current.toggleParsingIds(['file-1'], true); }); expect(result.current.creatingChunkingTaskIds).toEqual(['file-1']); }); it('should not duplicate ids', () => { const { result } = renderHook(() => useStore()); act(() => { useStore.setState({ creatingChunkingTaskIds: ['file-1'] }); }); act(() => { result.current.toggleParsingIds(['file-1'], true); }); expect(result.current.creatingChunkingTaskIds).toEqual(['file-1']); }); }); describe('useFetchFileItem', () => { it('should not fetch when id is undefined', () => { const { result } = renderHook(() => useStore()); renderHook(() => result.current.useFetchFileItem(undefined)); expect(lambdaClient.file.getFileItemById.query).not.toHaveBeenCalled(); }); it('should fetch file item when id is provided', async () => { const { result } = renderHook(() => useStore()); const mockFile: FileListItem = { chunkCount: null, chunkingError: null, createdAt: new Date(), embeddingError: null, fileType: 'text/plain', finishEmbedding: false, id: 'file-1', name: 'test.txt', size: 100, updatedAt: new Date(), url: 'http://example.com/test.txt', }; vi.mocked(lambdaClient.file.getFileItemById.query).mockResolvedValue(mockFile); const { result: swrResult } = renderHook(() => result.current.useFetchFileItem('file-1')); await waitFor(() => { expect(swrResult.current.data).toEqual(mockFile); }); }); }); describe('useFetchFileManage', () => { it('should fetch file list with params', async () => { const { result } = renderHook(() => useStore()); const mockFiles: FileListItem[] = [ { chunkCount: null, chunkingError: null, createdAt: new Date(), embeddingError: null, fileType: 'text/plain', finishEmbedding: false, id: 'file-1', name: 'test1.txt', size: 100, updatedAt: new Date(), url: 'http://example.com/test1.txt', }, { chunkCount: null, chunkingError: null, createdAt: new Date(), embeddingError: null, fileType: 'text/plain', finishEmbedding: false, id: 'file-2', name: 'test2.txt', size: 200, updatedAt: new Date(), url: 'http://example.com/test2.txt', }, ]; vi.mocked(lambdaClient.file.getFiles.query).mockResolvedValue(mockFiles); const params = { category: 'all' as any }; const { result: swrResult } = renderHook(() => result.current.useFetchFileManage(params)); await waitFor(() => { expect(swrResult.current.data).toEqual(mockFiles); }); }); it('should update store state on successful fetch', async () => { const { result } = renderHook(() => useStore()); const mockFiles: FileListItem[] = [ { chunkCount: null, chunkingError: null, createdAt: new Date(), embeddingError: null, fileType: 'text/plain', finishEmbedding: false, id: 'file-1', name: 'test.txt', size: 100, updatedAt: new Date(), url: 'http://example.com/test.txt', }, ]; vi.mocked(lambdaClient.file.getFiles.query).mockResolvedValue(mockFiles); const params = { category: 'all' as any }; renderHook(() => result.current.useFetchFileManage(params)); await waitFor(() => { expect(result.current.fileList).toEqual(mockFiles); expect(result.current.queryListParams).toEqual(params); }); }); }); });