mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-10 04:00:24 +00:00
843 lines
26 KiB
TypeScript
843 lines
26 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, Mock } 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 fs from "node:fs";
|
|
import * as fsp from "fs/promises";
|
|
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);
|
|
});
|
|
});
|
|
});
|
|
});
|