// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. ========= import { beforeEach, describe, expect, it, Mock, vi } from 'vitest'; // Mock modules with inline factories to avoid vitest hoisting issues. vi.mock('electron', () => { const dialogMocks = { showOpenDialog: vi.fn(), showSaveDialog: vi.fn(), }; return { dialog: dialogMocks }; }); vi.mock('node:fs', () => { const fsMocks = { existsSync: vi.fn(), readFileSync: vi.fn(), writeFileSync: vi.fn(), createReadStream: vi.fn(), mkdirSync: vi.fn(), }; return { default: fsMocks, existsSync: fsMocks.existsSync, readFileSync: fsMocks.readFileSync, writeFileSync: fsMocks.writeFileSync, createReadStream: fsMocks.createReadStream, mkdirSync: fsMocks.mkdirSync, }; }); vi.mock('fs/promises', () => ({ readFile: vi.fn(), writeFile: vi.fn(), stat: vi.fn(), rm: vi.fn(), })); import { dialog } from 'electron'; import * as fsp from 'fs/promises'; import fs from 'node:fs'; import path from 'node:path'; describe('File Operations and Utilities', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('select-file IPC handler', () => { it('should handle successful file selection', async () => { const mockResult = { canceled: false, filePaths: ['/path/to/file1.txt', '/path/to/file2.pdf'], }; (dialog.showOpenDialog as Mock).mockResolvedValue(mockResult); const result = await dialog.showOpenDialog({} as any, { properties: ['openFile', 'multiSelections'], }); expect(result.canceled).toBe(false); expect(result.filePaths).toHaveLength(2); expect(result.filePaths[0]).toContain('.txt'); expect(result.filePaths[1]).toContain('.pdf'); }); it('should handle cancelled file selection', async () => { const mockResult = { canceled: true, filePaths: [], }; (dialog.showOpenDialog as Mock).mockResolvedValue(mockResult); const result = await dialog.showOpenDialog({} as any, { properties: ['openFile', 'multiSelections'], }); expect(result.canceled).toBe(true); expect(result.filePaths).toHaveLength(0); }); it('should handle file selection with filters', async () => { const options = { properties: ['openFile'] as const, filters: [ { name: 'Text Files', extensions: ['txt', 'md'] }, { name: 'PDF Files', extensions: ['pdf'] }, { name: 'All Files', extensions: ['*'] }, ], }; expect(options.filters).toHaveLength(3); expect(options.filters[0].extensions).toContain('txt'); expect(options.filters[1].extensions).toContain('pdf'); }); it('should process successful file selection result', () => { const result = { canceled: false, filePaths: ['/path/to/selected/file.txt'], }; if (!result.canceled && result.filePaths.length > 0) { const firstFile = result.filePaths[0]; const fileName = path.basename(firstFile); const fileExt = path.extname(firstFile); expect(fileName).toBe('file.txt'); expect(fileExt).toBe('.txt'); } }); }); describe('read-file IPC handler', () => { it('should successfully read file content', async () => { const mockContent = 'This is the file content\nWith multiple lines'; (fsp.readFile as Mock).mockResolvedValue(mockContent); const content = await fsp.readFile('/path/to/file.txt', 'utf-8'); expect(content).toBe(mockContent); expect(content).toContain('multiple lines'); }); it('should handle file read errors', async () => { const error = new Error('ENOENT: no such file or directory'); (fsp.readFile as Mock).mockRejectedValue(error); try { await fsp.readFile('/nonexistent/file.txt', 'utf-8'); } catch (e) { expect(e).toBeInstanceOf(Error); expect((e as Error).message).toContain('no such file or directory'); } }); it('should handle different file encodings', async () => { const mockContent = Buffer.from('Binary content'); (fsp.readFile as Mock).mockResolvedValue(mockContent); const content = await fsp.readFile('/path/to/binary.bin'); expect(Buffer.isBuffer(content)).toBe(true); }); it('should validate file path', () => { const filePath = path.normalize('/path/to/file.txt'); const isAbsolute = path.isAbsolute(filePath); const normalizedPath = path.normalize(filePath); expect(isAbsolute).toBe(true); expect(normalizedPath).toBe(filePath); }); }); describe('reveal-in-folder IPC handler', () => { it('should handle valid file path', () => { const filePath = '/Users/test/Documents/file.txt'; const isValid = path.isAbsolute(filePath) && filePath.length > 0; expect(isValid).toBe(true); }); it('should handle invalid file path', () => { const filePath = ''; const isValid = path.isAbsolute(filePath) && filePath.length > 0; expect(isValid).toBe(false); }); it('should normalize file path', () => { const filePath = '/Users/test/../test/Documents/./file.txt'; const normalized = path.normalize(filePath); expect(normalized).toBe(path.normalize('/Users/test/Documents/file.txt')); }); it('should extract directory from file path', () => { const filePath = '/Users/test/Documents/file.txt'; const directory = path.dirname(filePath); expect(path.normalize(directory)).toBe( path.normalize('/Users/test/Documents') ); }); }); describe('File System Utilities', () => { it('should check file existence', () => { (fs.existsSync as Mock).mockReturnValue(true); const exists = fs.existsSync('/path/to/file.txt'); expect(exists).toBe(true); }); it('should handle non-existent files', () => { (fs.existsSync as Mock).mockReturnValue(false); const exists = fs.existsSync('/path/to/nonexistent.txt'); expect(exists).toBe(false); }); it('should create directory path', () => { const dirPath = '/path/to/new/directory'; const mockMkdirSync = vi.fn(); vi.mocked(fs).mkdirSync = mockMkdirSync; fs.mkdirSync(dirPath, { recursive: true }); expect(mockMkdirSync).toHaveBeenCalledWith(dirPath, { recursive: true }); }); it('should handle path operations', () => { const filePath = '/Users/test/Documents/file.txt'; const basename = path.basename(filePath); const dirname = path.dirname(filePath); const extname = path.extname(filePath); const parsed = path.parse(filePath); expect(basename).toBe('file.txt'); expect(path.normalize(dirname)).toBe( path.normalize('/Users/test/Documents') ); expect(extname).toBe('.txt'); expect(parsed.name).toBe('file'); expect(parsed.ext).toBe('.txt'); }); }); describe('File Validation', () => { it('should validate file extension', () => { const allowedExtensions = ['.txt', '.md', '.json', '.pdf']; const filePath = '/path/to/document.pdf'; const fileExt = path.extname(filePath); const isAllowed = allowedExtensions.includes(fileExt); expect(isAllowed).toBe(true); }); it('should reject invalid file extension', () => { const allowedExtensions = ['.txt', '.md', '.json']; const filePath = '/path/to/executable.exe'; const fileExt = path.extname(filePath); const isAllowed = allowedExtensions.includes(fileExt); expect(isAllowed).toBe(false); }); it('should validate file size', () => { const maxSize = 10 * 1024 * 1024; // 10MB const mockStats = { size: 5 * 1024 * 1024 }; // 5MB const isValidSize = mockStats.size <= maxSize; expect(isValidSize).toBe(true); }); it('should reject files that are too large', () => { const maxSize = 10 * 1024 * 1024; // 10MB const mockStats = { size: 20 * 1024 * 1024 }; // 20MB const isValidSize = mockStats.size <= maxSize; expect(isValidSize).toBe(false); }); }); describe('File Content Processing', () => { it('should process text file content', () => { const content = 'Line 1\nLine 2\nLine 3'; const lines = content.split('\n'); expect(lines).toHaveLength(3); expect(lines[0]).toBe('Line 1'); expect(lines[2]).toBe('Line 3'); }); it('should handle empty file content', () => { const content = ''; const lines = content.split('\n'); expect(lines).toHaveLength(1); expect(lines[0]).toBe(''); }); it('should process CSV-like content', () => { const content = 'name,age,email\nJohn,30,john@example.com\nJane,25,jane@example.com'; const lines = content.split('\n'); const headers = lines[0].split(','); expect(headers).toEqual(['name', 'age', 'email']); expect(lines).toHaveLength(3); }); it('should handle binary file detection', () => { const textContent = 'This is regular text content'; const binaryContent = Buffer.from([0x00, 0x01, 0x02, 0xff]); const isText = typeof textContent === 'string'; const isBinary = Buffer.isBuffer(binaryContent); expect(isText).toBe(true); expect(isBinary).toBe(true); }); }); describe('File Stream Operations', () => { it('should create readable stream', () => { const mockCreateReadStream = vi.fn().mockReturnValue({ pipe: vi.fn(), on: vi.fn(), destroy: vi.fn(), }); vi.mocked(fs).createReadStream = mockCreateReadStream; const stream = fs.createReadStream('/path/to/file.txt'); expect(mockCreateReadStream).toHaveBeenCalledWith('/path/to/file.txt'); expect(stream.pipe).toBeDefined(); expect(stream.on).toBeDefined(); }); it('should handle stream errors', () => { const mockStream = { on: vi.fn((event, callback) => { if (event === 'error') { setTimeout(() => callback(new Error('Stream error')), 0); } }), destroy: vi.fn(), }; let errorReceived = false; mockStream.on('error', (error: Error) => { errorReceived = true; expect(error.message).toBe('Stream error'); }); setTimeout(() => { expect(errorReceived).toBe(true); }, 10); }); it('should cleanup stream resources', () => { const mockStream = { destroy: vi.fn(), on: vi.fn(), }; // Simulate cleanup if (mockStream && typeof mockStream.destroy === 'function') { mockStream.destroy(); } expect(mockStream.destroy).toHaveBeenCalled(); }); }); describe('Project Management', () => { beforeEach(() => { vi.clearAllMocks(); }); describe('Project Structure Creation', () => { it('should create project directory structure', () => { const _email = 'test@example.com'; const projectId = 'xyz123'; const expectedPath = '/home/test/eigent/test/project_xyz123'; (fs.existsSync as Mock).mockReturnValue(false); (fs.mkdirSync as Mock).mockImplementation(() => {}); // Mock path operations const mockPath = { join: vi.fn((...args) => args.join('/')), }; const result = mockPath.join( '/home', 'test', 'eigent', 'test', `project_${projectId}` ); expect(result).toBe(expectedPath); }); it('should handle existing project directory', () => { const _email = 'test@example.com'; const _projectId = 'existing123'; (fs.existsSync as Mock).mockReturnValue(true); const mockMkdirSync = vi.fn(); vi.mocked(fs).mkdirSync = mockMkdirSync; // Should not create directory if it exists if (!fs.existsSync('/path/to/project')) { fs.mkdirSync('/path/to/project', { recursive: true }); } expect(mockMkdirSync).not.toHaveBeenCalled(); }); it('should validate project ID format', () => { const validProjectIds = ['xyz123', 'project_1', 'test-project']; const invalidProjectIds = [ '', 'project with spaces', 'project/with/slashes', ]; validProjectIds.forEach((id) => { const isValid = /^[a-zA-Z0-9_-]+$/.test(id); expect(isValid).toBe(true); }); invalidProjectIds.forEach((id) => { const isValid = /^[a-zA-Z0-9_-]+$/.test(id) && id.length > 0; expect(isValid).toBe(false); }); }); }); describe('Project-based Task Organization', () => { it('should find task in project structure', () => { const taskId = '1760964010356-4844'; const projectStructure = { project_xyz: { [`task_${taskId}`]: true, }, project_abc: { task_other: true, }, }; // Simulate finding task in project_xyz const foundInProject = 'project_xyz' in projectStructure && `task_${taskId}` in projectStructure['project_xyz']; expect(foundInProject).toBe(true); }); it('should fall back to legacy structure when task not found in projects', () => { const taskId = 'legacy-task-123'; const hasProjectStructure = false; if (!hasProjectStructure) { // Should look in legacy location const legacyPath = `/home/user/eigent/user/task_${taskId}`; expect(legacyPath).toContain('task_legacy-task-123'); } }); it('should handle task lookup with project ID provided', () => { const taskId = 'task123'; const projectId = 'xyz'; const projectBasedPath = `/home/user/eigent/user/project_${projectId}/task_${taskId}`; expect(projectBasedPath).toBe( '/home/user/eigent/user/project_xyz/task_task123' ); }); }); describe('Task Migration', () => { it('should move task from legacy to project structure', () => { const taskId = '1760964010356-4844'; const projectId = 'xyz'; const sourcePath = `/home/user/eigent/user/task_${taskId}`; const destPath = `/home/user/eigent/user/project_${projectId}/task_${taskId}`; (fs.existsSync as Mock).mockImplementation((path: string) => { return path === sourcePath; }); const mockRenameSync = vi.fn(); const mockMkdirSync = vi.fn(); vi.mocked(fs).renameSync = mockRenameSync; vi.mocked(fs).mkdirSync = mockMkdirSync; // Simulate move operation if (fs.existsSync(sourcePath)) { const projectDir = `/home/user/eigent/user/project_${projectId}`; if (!fs.existsSync(projectDir)) { mockMkdirSync(projectDir, { recursive: true }); } mockRenameSync(sourcePath, destPath); } expect(mockMkdirSync).toHaveBeenCalledWith( `/home/user/eigent/user/project_${projectId}`, { recursive: true } ); expect(mockRenameSync).toHaveBeenCalledWith(sourcePath, destPath); }); it('should handle log files during task migration', () => { const taskId = 'test123'; const projectId = 'xyz'; const sourceLogPath = `/home/.eigent/user/task_${taskId}`; const destLogPath = `/home/.eigent/user/project_${projectId}/task_${taskId}`; (fs.existsSync as Mock).mockReturnValue(true); const mockRenameSync = vi.fn(); const mockMkdirSync = vi.fn(); vi.mocked(fs).renameSync = mockRenameSync; vi.mocked(fs).mkdirSync = mockMkdirSync; // Simulate log migration const destLogDir = `/home/.eigent/user/project_${projectId}`; mockMkdirSync(destLogDir, { recursive: true }); mockRenameSync(sourceLogPath, destLogPath); expect(mockMkdirSync).toHaveBeenCalledWith(destLogDir, { recursive: true, }); expect(mockRenameSync).toHaveBeenCalledWith(sourceLogPath, destLogPath); }); it('should handle missing source files gracefully', () => { const taskId = 'nonexistent123'; const _projectId = 'xyz'; (fs.existsSync as Mock).mockReturnValue(false); const mockRenameSync = vi.fn(); vi.mocked(fs).renameSync = mockRenameSync; // Should not attempt to move non-existent files const sourcePath = `/home/user/eigent/user/task_${taskId}`; if (fs.existsSync(sourcePath)) { fs.renameSync(sourcePath, '/dest/path'); } expect(mockRenameSync).not.toHaveBeenCalled(); }); }); describe('Project Listing and Statistics', () => { it('should list all projects with task counts', () => { const mockProjects = [ { id: 'xyz', name: 'Project xyz', path: '/home/eigent/user/project_xyz', taskCount: 5, createdAt: new Date('2025-10-20'), }, { id: 'abc', name: 'Project abc', path: '/home/eigent/user/project_abc', taskCount: 3, createdAt: new Date('2025-10-19'), }, ]; // Sort by creation date (newest first) const sortedProjects = mockProjects.sort( (a, b) => b.createdAt.getTime() - a.createdAt.getTime() ); expect(sortedProjects[0].id).toBe('xyz'); expect(sortedProjects[0].taskCount).toBe(5); expect(sortedProjects[1].id).toBe('abc'); }); it('should count tasks in project correctly', () => { const mockProjectContents = [ 'task_1760964010356-4844', 'task_1760960521025-5106', 'task_1760913987942-682', 'other_file.txt', 'readme.md', ]; const taskCount = mockProjectContents.filter((item) => item.startsWith('task_') ).length; expect(taskCount).toBe(3); }); it('should handle empty projects', () => { const emptyProjectContents: string[] = []; const taskCount = emptyProjectContents.filter((item) => item.startsWith('task_') ).length; expect(taskCount).toBe(0); }); }); describe('Backward Compatibility', () => { it('should support legacy getFileList calls without projectId', () => { const email = 'test@example.com'; const taskId = 'legacy123'; // Should work with 2 parameters (legacy) const legacyCall = { email, taskId, projectId: undefined, }; expect(legacyCall.projectId).toBeUndefined(); expect(legacyCall.email).toBe(email); expect(legacyCall.taskId).toBe(taskId); }); it('should support new getFileList calls with projectId', () => { const email = 'test@example.com'; const taskId = 'new123'; const projectId = 'xyz'; // Should work with 3 parameters (new) const newCall = { email, taskId, projectId, }; expect(newCall.projectId).toBe(projectId); expect(newCall.email).toBe(email); expect(newCall.taskId).toBe(taskId); }); it('should maintain existing directory structure for legacy tasks', () => { const _email = 'test@example.com'; const taskId = 'existing123'; const legacyPath = `/home/eigent/test/task_${taskId}`; // Should still be able to access legacy paths expect(legacyPath).toBe('/home/eigent/test/task_existing123'); }); it('should handle mixed legacy and project-based structures', () => { const userDirectoryContents = [ 'task_legacy1', // Legacy task 'task_legacy2', // Legacy task 'project_xyz', // New project 'project_abc', // New project 'other_folder', // Other content ]; const legacyTasks = userDirectoryContents.filter((item) => item.startsWith('task_') ); const projects = userDirectoryContents.filter((item) => item.startsWith('project_') ); expect(legacyTasks).toHaveLength(2); expect(projects).toHaveLength(2); }); }); describe('Project File Listing', () => { it('should list all files in a project across all tasks', () => { const _projectStructure = { task_123: { 'file1.txt': true, subfolder: { 'file2.js': true, }, }, task_456: { 'file3.py': true, 'data.json': true, }, task_789: { 'readme.md': true, }, }; const expectedFiles = [ { path: '/project/task_123/file1.txt', task_id: '123', project_id: 'xyz', }, { path: '/project/task_123/subfolder/file2.js', task_id: '123', project_id: 'xyz', }, { path: '/project/task_456/file3.py', task_id: '456', project_id: 'xyz', }, { path: '/project/task_456/data.json', task_id: '456', project_id: 'xyz', }, { path: '/project/task_789/readme.md', task_id: '789', project_id: 'xyz', }, ]; // Should return sorted list by task_id then by file path const sortedFiles = expectedFiles.sort((a, b) => { if (a.task_id !== b.task_id) { return a.task_id.localeCompare(b.task_id); } return a.path.localeCompare(b.path); }); expect(sortedFiles[0].task_id).toBe('123'); expect(sortedFiles[sortedFiles.length - 1].task_id).toBe('789'); }); it('should handle empty project directories', () => { const emptyProjectContents: string[] = []; const taskDirs = emptyProjectContents.filter((entry) => entry.startsWith('task_') ); expect(taskDirs).toHaveLength(0); }); it('should enrich files with task and project context', () => { const mockFile = { name: 'test.txt', type: 'txt', path: '/project/task_123/test.txt', isFolder: false, relativePath: '', }; const enrichedFile = { ...mockFile, task_id: '123', project_id: 'xyz', relativePath: 'task_123/test.txt', }; expect(enrichedFile.task_id).toBe('123'); expect(enrichedFile.project_id).toBe('xyz'); expect(enrichedFile.relativePath).toBe('task_123/test.txt'); }); it('should filter non-task directories', () => { const projectContents = [ 'task_123', 'task_456', 'not_a_task', 'another_folder', 'task_789', ]; const taskDirs = projectContents.filter((entry) => entry.startsWith('task_') ); expect(taskDirs).toHaveLength(3); expect(taskDirs).toContain('task_123'); expect(taskDirs).toContain('task_456'); expect(taskDirs).toContain('task_789'); expect(taskDirs).not.toContain('not_a_task'); }); it('should handle projects with mixed file types', () => { const fileTypes = [ { name: 'document.pdf', type: 'pdf' }, { name: 'script.py', type: 'py' }, { name: 'data.json', type: 'json' }, { name: 'image.png', type: 'png' }, { name: 'folder', type: 'folder', isFolder: true }, ]; fileTypes.forEach((file) => { if (file.isFolder) { expect(file.type).toBe('folder'); } else { expect(file.type).toBe(file.name.split('.').pop()); } }); }); it('should sort files by task ID then by path', () => { const unsortedFiles = [ { task_id: '789', path: '/project/task_789/a.txt' }, { task_id: '123', path: '/project/task_123/z.txt' }, { task_id: '456', path: '/project/task_456/m.txt' }, { task_id: '123', path: '/project/task_123/a.txt' }, ]; const sortedFiles = unsortedFiles.sort((a, b) => { if (a.task_id !== b.task_id) { return a.task_id.localeCompare(b.task_id); } return a.path.localeCompare(b.path); }); expect(sortedFiles[0].task_id).toBe('123'); expect(sortedFiles[0].path).toContain('a.txt'); expect(sortedFiles[1].task_id).toBe('123'); expect(sortedFiles[1].path).toContain('z.txt'); expect(sortedFiles[2].task_id).toBe('456'); expect(sortedFiles[3].task_id).toBe('789'); }); }); describe('Error Handling', () => { it('should handle file system errors gracefully', () => { const mockReaddirSync = vi.fn().mockImplementation(() => { throw new Error('Permission denied'); }); vi.mocked(fs).readdirSync = mockReaddirSync; let errorOccurred = false; try { fs.readdirSync('/restricted/path'); } catch (error) { errorOccurred = true; expect(error).toBeInstanceOf(Error); } expect(errorOccurred).toBe(true); }); it('should handle invalid email addresses', () => { const invalidEmails = ['', 'invalid', 'test@', '@domain.com']; invalidEmails.forEach((email) => { const safeEmail = email .split('@')[0] .replace(/[\\/*?:"<>|\s]/g, '_') .replace(/^\.+|\.+$/g, ''); // Should either be empty or sanitized expect( safeEmail.length === 0 || /^[a-zA-Z0-9_]+$/.test(safeEmail) ).toBe(true); }); }); it('should handle invalid project/task IDs', () => { const invalidIds = [ '', 'id with spaces', 'id/with/slashes', 'id:with:colons', ]; invalidIds.forEach((id) => { const isValid = /^[a-zA-Z0-9_-]+$/.test(id) && id.length > 0; expect(isValid).toBe(false); }); }); it('should handle non-existent project directories', () => { (fs.existsSync as Mock).mockReturnValue(false); const projectExists = fs.existsSync('/nonexistent/project'); expect(projectExists).toBe(false); // Should return empty array for non-existent projects const result: any[] = []; expect(result).toHaveLength(0); }); it('should handle corrupted project structures', () => { const mockStats = vi.fn(); mockStats.mockImplementation((path: string) => { if (path.includes('corrupted')) { throw new Error('EACCES: permission denied'); } return { isDirectory: () => true }; }); (fs.statSync as Mock) = mockStats; let errorOccurred = false; try { fs.statSync('/corrupted/path'); } catch (error) { errorOccurred = true; expect(error).toBeInstanceOf(Error); } expect(errorOccurred).toBe(true); }); }); }); });