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.

393 lines (313 loc) • 11.7 kB
import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { App } from '@/core/App'; import LocalFileCtr from '../LocalFileCtr'; // Mock logger vi.mock('@/utils/logger', () => ({ createLogger: () => ({ debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn(), }), })); // Mock file-loaders vi.mock('@lobechat/file-loaders', () => ({ SYSTEM_FILES_TO_IGNORE: ['.DS_Store', 'Thumbs.db'], loadFile: vi.fn(), })); // Mock electron vi.mock('electron', () => ({ shell: { openPath: vi.fn(), }, })); // Mock fast-glob vi.mock('fast-glob', () => ({ default: vi.fn(), })); // Mock node:fs/promises and node:fs vi.mock('node:fs/promises', () => ({ stat: vi.fn(), readdir: vi.fn(), rename: vi.fn(), access: vi.fn(), writeFile: vi.fn(), readFile: vi.fn(), mkdir: vi.fn(), })); vi.mock('node:fs', () => ({ Stats: class Stats {}, constants: { F_OK: 0, }, stat: vi.fn(), readdir: vi.fn(), rename: vi.fn(), access: vi.fn(), writeFile: vi.fn(), readFile: vi.fn(), })); // Mock FileSearchService const mockSearchService = { search: vi.fn(), }; // Mock makeSureDirExist vi.mock('@/utils/file-system', () => ({ makeSureDirExist: vi.fn(), })); const mockApp = { getService: vi.fn(() => mockSearchService), } as unknown as App; describe('LocalFileCtr', () => { let localFileCtr: LocalFileCtr; let mockShell: any; let mockFg: any; let mockLoadFile: any; let mockFsPromises: any; beforeEach(async () => { vi.clearAllMocks(); // Import mocks mockShell = (await import('electron')).shell; mockFg = (await import('fast-glob')).default; mockLoadFile = (await import('@lobechat/file-loaders')).loadFile; mockFsPromises = await import('node:fs/promises'); localFileCtr = new LocalFileCtr(mockApp); }); describe('handleOpenLocalFile', () => { it('should open file successfully', async () => { vi.mocked(mockShell.openPath).mockResolvedValue(''); const result = await localFileCtr.handleOpenLocalFile({ path: '/test/file.txt' }); expect(result).toEqual({ success: true }); expect(mockShell.openPath).toHaveBeenCalledWith('/test/file.txt'); }); it('should return error when opening file fails', async () => { const error = new Error('Failed to open'); vi.mocked(mockShell.openPath).mockRejectedValue(error); const result = await localFileCtr.handleOpenLocalFile({ path: '/test/file.txt' }); expect(result).toEqual({ success: false, error: 'Failed to open' }); }); }); describe('handleOpenLocalFolder', () => { it('should open directory when isDirectory is true', async () => { vi.mocked(mockShell.openPath).mockResolvedValue(''); const result = await localFileCtr.handleOpenLocalFolder({ path: '/test/folder', isDirectory: true, }); expect(result).toEqual({ success: true }); expect(mockShell.openPath).toHaveBeenCalledWith('/test/folder'); }); it('should open parent directory when isDirectory is false', async () => { vi.mocked(mockShell.openPath).mockResolvedValue(''); const result = await localFileCtr.handleOpenLocalFolder({ path: '/test/folder/file.txt', isDirectory: false, }); expect(result).toEqual({ success: true }); expect(mockShell.openPath).toHaveBeenCalledWith('/test/folder'); }); it('should return error when opening folder fails', async () => { const error = new Error('Failed to open folder'); vi.mocked(mockShell.openPath).mockRejectedValue(error); const result = await localFileCtr.handleOpenLocalFolder({ path: '/test/folder', isDirectory: true, }); expect(result).toEqual({ success: false, error: 'Failed to open folder' }); }); }); describe('readFile', () => { it('should read file successfully with default location', async () => { const mockFileContent = 'line1\nline2\nline3\nline4\nline5'; vi.mocked(mockLoadFile).mockResolvedValue({ content: mockFileContent, filename: 'test.txt', fileType: 'txt', createdTime: new Date('2024-01-01'), modifiedTime: new Date('2024-01-02'), }); const result = await localFileCtr.readFile({ path: '/test/file.txt' }); expect(result.filename).toBe('test.txt'); expect(result.fileType).toBe('txt'); expect(result.totalLineCount).toBe(5); expect(result.content).toBe(mockFileContent); }); it('should read file with custom location range', async () => { const mockFileContent = 'line1\nline2\nline3\nline4\nline5'; vi.mocked(mockLoadFile).mockResolvedValue({ content: mockFileContent, filename: 'test.txt', fileType: 'txt', createdTime: new Date('2024-01-01'), modifiedTime: new Date('2024-01-02'), }); const result = await localFileCtr.readFile({ path: '/test/file.txt', loc: [1, 3] }); expect(result.content).toBe('line2\nline3'); expect(result.lineCount).toBe(2); expect(result.totalLineCount).toBe(5); }); it('should handle file read error', async () => { vi.mocked(mockLoadFile).mockRejectedValue(new Error('File not found')); const result = await localFileCtr.readFile({ path: '/test/missing.txt' }); expect(result.content).toContain('Error accessing or processing file'); expect(result.lineCount).toBe(0); expect(result.charCount).toBe(0); }); }); describe('readFiles', () => { it('should read multiple files successfully', async () => { vi.mocked(mockLoadFile).mockResolvedValue({ content: 'file content', filename: 'test.txt', fileType: 'txt', createdTime: new Date('2024-01-01'), modifiedTime: new Date('2024-01-02'), }); const result = await localFileCtr.readFiles({ paths: ['/test/file1.txt', '/test/file2.txt'], }); expect(result).toHaveLength(2); expect(mockLoadFile).toHaveBeenCalledTimes(2); }); }); describe('handleWriteFile', () => { it('should write file successfully', async () => { vi.mocked(mockFsPromises.mkdir).mockResolvedValue(undefined); vi.mocked(mockFsPromises.writeFile).mockResolvedValue(undefined); const result = await localFileCtr.handleWriteFile({ path: '/test/file.txt', content: 'test content', }); expect(result).toEqual({ success: true }); }); it('should return error when path is empty', async () => { const result = await localFileCtr.handleWriteFile({ path: '', content: 'test content', }); expect(result).toEqual({ success: false, error: 'Path cannot be empty' }); }); it('should return error when content is undefined', async () => { const result = await localFileCtr.handleWriteFile({ path: '/test/file.txt', content: undefined as any, }); expect(result).toEqual({ success: false, error: 'Content cannot be empty' }); }); it('should handle write error', async () => { vi.mocked(mockFsPromises.mkdir).mockResolvedValue(undefined); vi.mocked(mockFsPromises.writeFile).mockRejectedValue(new Error('Write failed')); const result = await localFileCtr.handleWriteFile({ path: '/test/file.txt', content: 'test content', }); expect(result).toEqual({ success: false, error: 'Failed to write file: Write failed' }); }); }); describe('handleRenameFile', () => { it('should rename file successfully', async () => { vi.mocked(mockFsPromises.rename).mockResolvedValue(undefined); const result = await localFileCtr.handleRenameFile({ path: '/test/old.txt', newName: 'new.txt', }); expect(result).toEqual({ success: true, newPath: '/test/new.txt' }); expect(mockFsPromises.rename).toHaveBeenCalledWith('/test/old.txt', '/test/new.txt'); }); it('should skip rename when paths are identical', async () => { const result = await localFileCtr.handleRenameFile({ path: '/test/file.txt', newName: 'file.txt', }); expect(result).toEqual({ success: true, newPath: '/test/file.txt' }); expect(mockFsPromises.rename).not.toHaveBeenCalled(); }); it('should reject invalid new name with path separators', async () => { const result = await localFileCtr.handleRenameFile({ path: '/test/old.txt', newName: '../new.txt', }); expect(result.success).toBe(false); expect(result.error).toContain('Invalid new name'); }); it('should reject invalid new name with special characters', async () => { const result = await localFileCtr.handleRenameFile({ path: '/test/old.txt', newName: 'new:file.txt', }); expect(result.success).toBe(false); expect(result.error).toContain('Invalid new name'); }); it('should handle file not found error', async () => { const error: any = new Error('File not found'); error.code = 'ENOENT'; vi.mocked(mockFsPromises.rename).mockRejectedValue(error); const result = await localFileCtr.handleRenameFile({ path: '/test/old.txt', newName: 'new.txt', }); expect(result.success).toBe(false); expect(result.error).toContain('File or directory not found'); }); it('should handle file already exists error', async () => { const error: any = new Error('File exists'); error.code = 'EEXIST'; vi.mocked(mockFsPromises.rename).mockRejectedValue(error); const result = await localFileCtr.handleRenameFile({ path: '/test/old.txt', newName: 'new.txt', }); expect(result.success).toBe(false); expect(result.error).toContain('already exists'); }); }); describe('handleLocalFilesSearch', () => { it('should search files successfully', async () => { const mockResults = [ { name: 'test.txt', path: '/test/test.txt', isDirectory: false, size: 100, type: 'txt', }, ]; mockSearchService.search.mockResolvedValue(mockResults); const result = await localFileCtr.handleLocalFilesSearch({ keywords: 'test' }); expect(result).toEqual(mockResults); expect(mockSearchService.search).toHaveBeenCalledWith('test', { limit: 30 }); }); it('should return empty array on search error', async () => { mockSearchService.search.mockRejectedValue(new Error('Search failed')); const result = await localFileCtr.handleLocalFilesSearch({ keywords: 'test' }); expect(result).toEqual([]); }); }); describe('handleGlobFiles', () => { it('should glob files successfully', async () => { const mockFiles = [ { path: '/test/file1.txt', stats: { mtime: new Date('2024-01-02') } }, { path: '/test/file2.txt', stats: { mtime: new Date('2024-01-01') } }, ]; vi.mocked(mockFg).mockResolvedValue(mockFiles); const result = await localFileCtr.handleGlobFiles({ pattern: '*.txt', path: '/test', }); expect(result.success).toBe(true); expect(result.files).toEqual(['/test/file1.txt', '/test/file2.txt']); expect(result.total_files).toBe(2); }); it('should handle glob error', async () => { vi.mocked(mockFg).mockRejectedValue(new Error('Glob failed')); const result = await localFileCtr.handleGlobFiles({ pattern: '*.txt', }); expect(result).toEqual({ success: false, files: [], total_files: 0, }); }); }); });