mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-23 21:06:50 +00:00
Co-authored-by: a7m-1st <Ahmed.jimi.awelkeir500@gmail.com> Co-authored-by: eigent-ai <camel@eigent.ai> Co-authored-by: Wendong-Fan <133094783+Wendong-Fan@users.noreply.github.com> Co-authored-by: Wendong-Fan <w3ndong.fan@gmail.com>
906 lines
28 KiB
TypeScript
906 lines
28 KiB
TypeScript
// ========= 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);
|
|
});
|
|
});
|
|
});
|
|
});
|