mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
💄 style: improve local-system tool implement (#12022)
* improve local system ability * fix build * improve tools title render * fix tools * update * try to fix lint * update * refactor the LocalFileCtr.ts result * refactor the exector result
This commit is contained in:
@@ -12,11 +12,11 @@
|
||||
"main": "./dist/main/index.js",
|
||||
"scripts": {
|
||||
"build": "electron-vite build",
|
||||
"build-local": "npm run build && electron-builder --dir --config electron-builder.mjs --c.mac.notarize=false -c.mac.identity=null --c.asar=false",
|
||||
"build:linux": "npm run build && electron-builder --linux --config electron-builder.mjs --publish never",
|
||||
"build:mac": "npm run build && electron-builder --mac --config electron-builder.mjs --publish never",
|
||||
"build:mac:local": "npm run build && UPDATE_CHANNEL=nightly electron-builder --mac --config electron-builder.mjs --publish never",
|
||||
"build:win": "npm run build && electron-builder --win --config electron-builder.mjs --publish never",
|
||||
"build-local": "npm run build && electron-builder --dir --config electron-builder.mjs --c.mac.notarize=false -c.mac.identity=null --c.asar=false",
|
||||
"dev": "electron-vite dev",
|
||||
"dev:static": "cross-env DESKTOP_RENDERER_STATIC=1 npm run electron:dev",
|
||||
"electron:dev": "electron-vite dev",
|
||||
@@ -47,9 +47,6 @@
|
||||
"get-port-please": "^3.2.0",
|
||||
"superjson": "^2.2.6"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"node-mac-permissions": "^2.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
||||
"@electron-toolkit/eslint-config-ts": "^3.1.0",
|
||||
@@ -103,6 +100,9 @@
|
||||
"vitest": "^3.2.4",
|
||||
"zod": "^3.25.76"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"node-mac-permissions": "^2.5.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"onlyBuiltDependencies": [
|
||||
"@napi-rs/canvas",
|
||||
|
||||
@@ -218,8 +218,13 @@ export default class LocalFileCtr extends ControllerModule {
|
||||
}
|
||||
|
||||
@IpcMethod()
|
||||
async listLocalFiles({ path: dirPath }: ListLocalFileParams): Promise<FileResult[]> {
|
||||
logger.debug('Listing directory contents:', { dirPath });
|
||||
async listLocalFiles({
|
||||
path: dirPath,
|
||||
sortBy = 'modifiedTime',
|
||||
sortOrder = 'desc',
|
||||
limit = 100,
|
||||
}: ListLocalFileParams): Promise<{ files: FileResult[]; totalCount: number }> {
|
||||
logger.debug('Listing directory contents:', { dirPath, limit, sortBy, sortOrder });
|
||||
|
||||
const results: FileResult[] = [];
|
||||
try {
|
||||
@@ -256,22 +261,51 @@ export default class LocalFileCtr extends ControllerModule {
|
||||
}
|
||||
}
|
||||
|
||||
// Sort entries: folders first, then by name
|
||||
// Sort entries based on sortBy and sortOrder
|
||||
results.sort((a, b) => {
|
||||
if (a.isDirectory !== b.isDirectory) {
|
||||
return a.isDirectory ? -1 : 1; // Directories first
|
||||
let comparison = 0;
|
||||
|
||||
switch (sortBy) {
|
||||
case 'name': {
|
||||
comparison = (a.name || '').localeCompare(b.name || '');
|
||||
break;
|
||||
}
|
||||
case 'modifiedTime': {
|
||||
comparison = a.modifiedTime.getTime() - b.modifiedTime.getTime();
|
||||
break;
|
||||
}
|
||||
case 'createdTime': {
|
||||
comparison = a.createdTime.getTime() - b.createdTime.getTime();
|
||||
break;
|
||||
}
|
||||
case 'size': {
|
||||
comparison = a.size - b.size;
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
comparison = a.modifiedTime.getTime() - b.modifiedTime.getTime();
|
||||
}
|
||||
}
|
||||
// Add null/undefined checks for robustness if needed, though names should exist
|
||||
return (a.name || '').localeCompare(b.name || ''); // Then sort by name
|
||||
|
||||
return sortOrder === 'desc' ? -comparison : comparison;
|
||||
});
|
||||
|
||||
logger.debug('Directory listing successful', { dirPath, resultCount: results.length });
|
||||
return results;
|
||||
const totalCount = results.length;
|
||||
|
||||
// Apply limit
|
||||
const limitedResults = results.slice(0, limit);
|
||||
|
||||
logger.debug('Directory listing successful', {
|
||||
dirPath,
|
||||
resultCount: limitedResults.length,
|
||||
totalCount,
|
||||
});
|
||||
return { files: limitedResults, totalCount };
|
||||
} catch (error) {
|
||||
logger.error(`Failed to list directory ${dirPath}:`, error);
|
||||
// Rethrow or return an empty array/error object depending on desired behavior
|
||||
// For now, returning empty array on error listing directory itself
|
||||
return [];
|
||||
// For now, returning empty result on error listing directory itself
|
||||
return { files: [], totalCount: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ vi.mock('@/utils/logger', () => ({
|
||||
|
||||
// Mock file-loaders
|
||||
vi.mock('@lobechat/file-loaders', () => ({
|
||||
SYSTEM_FILES_TO_IGNORE: ['.DS_Store', 'Thumbs.db'],
|
||||
loadFile: vi.fn(),
|
||||
SYSTEM_FILES_TO_IGNORE: ['.DS_Store', 'Thumbs.db', '$RECYCLE.BIN'],
|
||||
}));
|
||||
|
||||
// Mock electron
|
||||
@@ -553,6 +553,514 @@ describe('LocalFileCtr', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('listLocalFiles', () => {
|
||||
it('should list directory contents successfully', async () => {
|
||||
vi.mocked(mockFsPromises.readdir).mockResolvedValue(['file1.txt', 'file2.txt', 'folder1']);
|
||||
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
||||
const name = (filePath as string).split('/').pop();
|
||||
if (name === 'folder1') {
|
||||
return {
|
||||
isDirectory: () => true,
|
||||
birthtime: new Date('2024-01-01'),
|
||||
mtime: new Date('2024-01-15'),
|
||||
atime: new Date('2024-01-20'),
|
||||
size: 4096,
|
||||
} as any;
|
||||
}
|
||||
return {
|
||||
isDirectory: () => false,
|
||||
birthtime: new Date('2024-01-02'),
|
||||
mtime: new Date('2024-01-10'),
|
||||
atime: new Date('2024-01-18'),
|
||||
size: 1024,
|
||||
} as any;
|
||||
});
|
||||
|
||||
const result = await localFileCtr.listLocalFiles({ path: '/test' });
|
||||
|
||||
expect(result.files).toHaveLength(3);
|
||||
expect(result.totalCount).toBe(3);
|
||||
expect(mockFsPromises.readdir).toHaveBeenCalledWith('/test');
|
||||
});
|
||||
|
||||
it('should filter out system files like .DS_Store and Thumbs.db', async () => {
|
||||
vi.mocked(mockFsPromises.readdir).mockResolvedValue([
|
||||
'file1.txt',
|
||||
'.DS_Store',
|
||||
'Thumbs.db',
|
||||
'folder1',
|
||||
]);
|
||||
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
||||
const name = (filePath as string).split('/').pop();
|
||||
if (name === 'folder1') {
|
||||
return {
|
||||
isDirectory: () => true,
|
||||
birthtime: new Date('2024-01-01'),
|
||||
mtime: new Date('2024-01-15'),
|
||||
atime: new Date('2024-01-20'),
|
||||
size: 4096,
|
||||
} as any;
|
||||
}
|
||||
return {
|
||||
isDirectory: () => false,
|
||||
birthtime: new Date('2024-01-02'),
|
||||
mtime: new Date('2024-01-10'),
|
||||
atime: new Date('2024-01-18'),
|
||||
size: 1024,
|
||||
} as any;
|
||||
});
|
||||
|
||||
const result = await localFileCtr.listLocalFiles({ path: '/test' });
|
||||
|
||||
// Should only contain file1.txt and folder1, not .DS_Store or Thumbs.db
|
||||
expect(result.files).toHaveLength(2);
|
||||
expect(result.totalCount).toBe(2);
|
||||
expect(result.files.map((r) => r.name)).not.toContain('.DS_Store');
|
||||
expect(result.files.map((r) => r.name)).not.toContain('Thumbs.db');
|
||||
expect(result.files.map((r) => r.name)).toContain('folder1');
|
||||
expect(result.files.map((r) => r.name)).toContain('file1.txt');
|
||||
});
|
||||
|
||||
it('should filter out $RECYCLE.BIN system folder', async () => {
|
||||
vi.mocked(mockFsPromises.readdir).mockResolvedValue(['file1.txt', '$RECYCLE.BIN', 'folder1']);
|
||||
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
||||
const name = (filePath as string).split('/').pop();
|
||||
const isDir = name === 'folder1' || name === '$RECYCLE.BIN';
|
||||
return {
|
||||
isDirectory: () => isDir,
|
||||
birthtime: new Date('2024-01-01'),
|
||||
mtime: new Date('2024-01-15'),
|
||||
atime: new Date('2024-01-20'),
|
||||
size: isDir ? 4096 : 1024,
|
||||
} as any;
|
||||
});
|
||||
|
||||
const result = await localFileCtr.listLocalFiles({ path: '/test' });
|
||||
|
||||
// Should not contain $RECYCLE.BIN
|
||||
expect(result.files).toHaveLength(2);
|
||||
expect(result.totalCount).toBe(2);
|
||||
expect(result.files.map((r) => r.name)).not.toContain('$RECYCLE.BIN');
|
||||
});
|
||||
|
||||
it('should sort by name ascending when specified', async () => {
|
||||
vi.mocked(mockFsPromises.readdir).mockResolvedValue(['zebra.txt', 'alpha.txt', 'apple.txt']);
|
||||
vi.mocked(mockFsPromises.stat).mockResolvedValue({
|
||||
isDirectory: () => false,
|
||||
birthtime: new Date('2024-01-01'),
|
||||
mtime: new Date('2024-01-15'),
|
||||
atime: new Date('2024-01-20'),
|
||||
size: 1024,
|
||||
} as any);
|
||||
|
||||
const result = await localFileCtr.listLocalFiles({
|
||||
path: '/test',
|
||||
sortBy: 'name',
|
||||
sortOrder: 'asc',
|
||||
});
|
||||
|
||||
expect(result.files.map((r) => r.name)).toEqual(['alpha.txt', 'apple.txt', 'zebra.txt']);
|
||||
});
|
||||
|
||||
it('should sort by modifiedTime descending by default', async () => {
|
||||
vi.mocked(mockFsPromises.readdir).mockResolvedValue(['old.txt', 'new.txt', 'mid.txt']);
|
||||
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
||||
const name = (filePath as string).split('/').pop();
|
||||
const dates: Record<string, Date> = {
|
||||
'new.txt': new Date('2024-01-20'),
|
||||
'mid.txt': new Date('2024-01-15'),
|
||||
'old.txt': new Date('2024-01-01'),
|
||||
};
|
||||
return {
|
||||
isDirectory: () => false,
|
||||
birthtime: new Date('2024-01-01'),
|
||||
mtime: dates[name!] || new Date('2024-01-01'),
|
||||
atime: new Date('2024-01-20'),
|
||||
size: 1024,
|
||||
} as any;
|
||||
});
|
||||
|
||||
const result = await localFileCtr.listLocalFiles({ path: '/test' });
|
||||
|
||||
// Default sort: modifiedTime descending (newest first)
|
||||
expect(result.files.map((r) => r.name)).toEqual(['new.txt', 'mid.txt', 'old.txt']);
|
||||
});
|
||||
|
||||
it('should sort by size ascending when specified', async () => {
|
||||
vi.mocked(mockFsPromises.readdir).mockResolvedValue(['large.txt', 'small.txt', 'medium.txt']);
|
||||
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
||||
const name = (filePath as string).split('/').pop();
|
||||
const sizes: Record<string, number> = {
|
||||
'large.txt': 10000,
|
||||
'medium.txt': 5000,
|
||||
'small.txt': 1000,
|
||||
};
|
||||
return {
|
||||
isDirectory: () => false,
|
||||
birthtime: new Date('2024-01-01'),
|
||||
mtime: new Date('2024-01-15'),
|
||||
atime: new Date('2024-01-20'),
|
||||
size: sizes[name!] || 1024,
|
||||
} as any;
|
||||
});
|
||||
|
||||
const result = await localFileCtr.listLocalFiles({
|
||||
path: '/test',
|
||||
sortBy: 'size',
|
||||
sortOrder: 'asc',
|
||||
});
|
||||
|
||||
expect(result.files.map((r) => r.name)).toEqual(['small.txt', 'medium.txt', 'large.txt']);
|
||||
});
|
||||
|
||||
it('should apply limit parameter', async () => {
|
||||
vi.mocked(mockFsPromises.readdir).mockResolvedValue([
|
||||
'file1.txt',
|
||||
'file2.txt',
|
||||
'file3.txt',
|
||||
'file4.txt',
|
||||
'file5.txt',
|
||||
]);
|
||||
vi.mocked(mockFsPromises.stat).mockResolvedValue({
|
||||
isDirectory: () => false,
|
||||
birthtime: new Date('2024-01-01'),
|
||||
mtime: new Date('2024-01-15'),
|
||||
atime: new Date('2024-01-20'),
|
||||
size: 1024,
|
||||
} as any);
|
||||
|
||||
const result = await localFileCtr.listLocalFiles({
|
||||
path: '/test',
|
||||
limit: 3,
|
||||
});
|
||||
|
||||
expect(result.files).toHaveLength(3);
|
||||
expect(result.totalCount).toBe(5); // Total is 5, but limited to 3
|
||||
});
|
||||
|
||||
it('should use default limit of 100', async () => {
|
||||
// Create 150 files
|
||||
const files = Array.from({ length: 150 }, (_, i) => `file${i}.txt`);
|
||||
vi.mocked(mockFsPromises.readdir).mockResolvedValue(files);
|
||||
vi.mocked(mockFsPromises.stat).mockResolvedValue({
|
||||
isDirectory: () => false,
|
||||
birthtime: new Date('2024-01-01'),
|
||||
mtime: new Date('2024-01-15'),
|
||||
atime: new Date('2024-01-20'),
|
||||
size: 1024,
|
||||
} as any);
|
||||
|
||||
const result = await localFileCtr.listLocalFiles({ path: '/test' });
|
||||
|
||||
expect(result.files).toHaveLength(100);
|
||||
expect(result.totalCount).toBe(150); // Total is 150, but limited to 100
|
||||
});
|
||||
|
||||
it('should sort by createdTime ascending when specified', async () => {
|
||||
vi.mocked(mockFsPromises.readdir).mockResolvedValue([
|
||||
'newest.txt',
|
||||
'oldest.txt',
|
||||
'middle.txt',
|
||||
]);
|
||||
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
||||
const name = (filePath as string).split('/').pop();
|
||||
const dates: Record<string, Date> = {
|
||||
'newest.txt': new Date('2024-03-01'),
|
||||
'middle.txt': new Date('2024-02-01'),
|
||||
'oldest.txt': new Date('2024-01-01'),
|
||||
};
|
||||
return {
|
||||
isDirectory: () => false,
|
||||
birthtime: dates[name!] || new Date('2024-01-01'),
|
||||
mtime: new Date('2024-01-15'),
|
||||
atime: new Date('2024-01-20'),
|
||||
size: 1024,
|
||||
} as any;
|
||||
});
|
||||
|
||||
const result = await localFileCtr.listLocalFiles({
|
||||
path: '/test',
|
||||
sortBy: 'createdTime',
|
||||
sortOrder: 'asc',
|
||||
});
|
||||
|
||||
expect(result.files.map((r) => r.name)).toEqual(['oldest.txt', 'middle.txt', 'newest.txt']);
|
||||
});
|
||||
|
||||
it('should sort by createdTime descending when specified', async () => {
|
||||
vi.mocked(mockFsPromises.readdir).mockResolvedValue([
|
||||
'newest.txt',
|
||||
'oldest.txt',
|
||||
'middle.txt',
|
||||
]);
|
||||
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
||||
const name = (filePath as string).split('/').pop();
|
||||
const dates: Record<string, Date> = {
|
||||
'newest.txt': new Date('2024-03-01'),
|
||||
'middle.txt': new Date('2024-02-01'),
|
||||
'oldest.txt': new Date('2024-01-01'),
|
||||
};
|
||||
return {
|
||||
isDirectory: () => false,
|
||||
birthtime: dates[name!] || new Date('2024-01-01'),
|
||||
mtime: new Date('2024-01-15'),
|
||||
atime: new Date('2024-01-20'),
|
||||
size: 1024,
|
||||
} as any;
|
||||
});
|
||||
|
||||
const result = await localFileCtr.listLocalFiles({
|
||||
path: '/test',
|
||||
sortBy: 'createdTime',
|
||||
sortOrder: 'desc',
|
||||
});
|
||||
|
||||
expect(result.files.map((r) => r.name)).toEqual(['newest.txt', 'middle.txt', 'oldest.txt']);
|
||||
});
|
||||
|
||||
it('should sort by name descending when specified', async () => {
|
||||
vi.mocked(mockFsPromises.readdir).mockResolvedValue(['alpha.txt', 'zebra.txt', 'middle.txt']);
|
||||
vi.mocked(mockFsPromises.stat).mockResolvedValue({
|
||||
isDirectory: () => false,
|
||||
birthtime: new Date('2024-01-01'),
|
||||
mtime: new Date('2024-01-15'),
|
||||
atime: new Date('2024-01-20'),
|
||||
size: 1024,
|
||||
} as any);
|
||||
|
||||
const result = await localFileCtr.listLocalFiles({
|
||||
path: '/test',
|
||||
sortBy: 'name',
|
||||
sortOrder: 'desc',
|
||||
});
|
||||
|
||||
expect(result.files.map((r) => r.name)).toEqual(['zebra.txt', 'middle.txt', 'alpha.txt']);
|
||||
});
|
||||
|
||||
it('should sort by size descending when specified', async () => {
|
||||
vi.mocked(mockFsPromises.readdir).mockResolvedValue(['small.txt', 'large.txt', 'medium.txt']);
|
||||
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
||||
const name = (filePath as string).split('/').pop();
|
||||
const sizes: Record<string, number> = {
|
||||
'large.txt': 10000,
|
||||
'medium.txt': 5000,
|
||||
'small.txt': 1000,
|
||||
};
|
||||
return {
|
||||
isDirectory: () => false,
|
||||
birthtime: new Date('2024-01-01'),
|
||||
mtime: new Date('2024-01-15'),
|
||||
atime: new Date('2024-01-20'),
|
||||
size: sizes[name!] || 1024,
|
||||
} as any;
|
||||
});
|
||||
|
||||
const result = await localFileCtr.listLocalFiles({
|
||||
path: '/test',
|
||||
sortBy: 'size',
|
||||
sortOrder: 'desc',
|
||||
});
|
||||
|
||||
expect(result.files.map((r) => r.name)).toEqual(['large.txt', 'medium.txt', 'small.txt']);
|
||||
});
|
||||
|
||||
it('should sort by modifiedTime ascending when specified', async () => {
|
||||
vi.mocked(mockFsPromises.readdir).mockResolvedValue(['old.txt', 'new.txt', 'mid.txt']);
|
||||
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
||||
const name = (filePath as string).split('/').pop();
|
||||
const dates: Record<string, Date> = {
|
||||
'new.txt': new Date('2024-01-20'),
|
||||
'mid.txt': new Date('2024-01-15'),
|
||||
'old.txt': new Date('2024-01-01'),
|
||||
};
|
||||
return {
|
||||
isDirectory: () => false,
|
||||
birthtime: new Date('2024-01-01'),
|
||||
mtime: dates[name!] || new Date('2024-01-01'),
|
||||
atime: new Date('2024-01-20'),
|
||||
size: 1024,
|
||||
} as any;
|
||||
});
|
||||
|
||||
const result = await localFileCtr.listLocalFiles({
|
||||
path: '/test',
|
||||
sortBy: 'modifiedTime',
|
||||
sortOrder: 'asc',
|
||||
});
|
||||
|
||||
expect(result.files.map((r) => r.name)).toEqual(['old.txt', 'mid.txt', 'new.txt']);
|
||||
});
|
||||
|
||||
it('should handle empty directory with sort options', async () => {
|
||||
vi.mocked(mockFsPromises.readdir).mockResolvedValue([]);
|
||||
|
||||
const result = await localFileCtr.listLocalFiles({
|
||||
path: '/empty',
|
||||
sortBy: 'name',
|
||||
sortOrder: 'asc',
|
||||
});
|
||||
|
||||
expect(result.files).toEqual([]);
|
||||
expect(result.totalCount).toBe(0);
|
||||
});
|
||||
|
||||
it('should apply limit after sorting', async () => {
|
||||
vi.mocked(mockFsPromises.readdir).mockResolvedValue([
|
||||
'file1.txt',
|
||||
'file2.txt',
|
||||
'file3.txt',
|
||||
'file4.txt',
|
||||
'file5.txt',
|
||||
]);
|
||||
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
||||
const name = (filePath as string).split('/').pop();
|
||||
const dates: Record<string, Date> = {
|
||||
'file1.txt': new Date('2024-01-01'),
|
||||
'file2.txt': new Date('2024-01-02'),
|
||||
'file3.txt': new Date('2024-01-03'),
|
||||
'file4.txt': new Date('2024-01-04'),
|
||||
'file5.txt': new Date('2024-01-05'),
|
||||
};
|
||||
return {
|
||||
isDirectory: () => false,
|
||||
birthtime: new Date('2024-01-01'),
|
||||
mtime: dates[name!] || new Date('2024-01-01'),
|
||||
atime: new Date('2024-01-20'),
|
||||
size: 1024,
|
||||
} as any;
|
||||
});
|
||||
|
||||
// Sort by modifiedTime desc (default) and limit to 3
|
||||
const result = await localFileCtr.listLocalFiles({
|
||||
path: '/test',
|
||||
limit: 3,
|
||||
});
|
||||
|
||||
// Should get the 3 newest files
|
||||
expect(result.files).toHaveLength(3);
|
||||
expect(result.totalCount).toBe(5); // Total is 5, but limited to 3
|
||||
expect(result.files.map((r) => r.name)).toEqual(['file5.txt', 'file4.txt', 'file3.txt']);
|
||||
});
|
||||
|
||||
it('should handle limit larger than file count', async () => {
|
||||
vi.mocked(mockFsPromises.readdir).mockResolvedValue(['file1.txt', 'file2.txt']);
|
||||
vi.mocked(mockFsPromises.stat).mockResolvedValue({
|
||||
isDirectory: () => false,
|
||||
birthtime: new Date('2024-01-01'),
|
||||
mtime: new Date('2024-01-15'),
|
||||
atime: new Date('2024-01-20'),
|
||||
size: 1024,
|
||||
} as any);
|
||||
|
||||
const result = await localFileCtr.listLocalFiles({
|
||||
path: '/test',
|
||||
limit: 1000,
|
||||
});
|
||||
|
||||
expect(result.files).toHaveLength(2);
|
||||
expect(result.totalCount).toBe(2);
|
||||
});
|
||||
|
||||
it('should return file metadata including size, times and type', async () => {
|
||||
const createdTime = new Date('2024-01-01');
|
||||
const modifiedTime = new Date('2024-01-15');
|
||||
const accessTime = new Date('2024-01-20');
|
||||
|
||||
vi.mocked(mockFsPromises.readdir).mockResolvedValue(['document.pdf']);
|
||||
vi.mocked(mockFsPromises.stat).mockResolvedValue({
|
||||
isDirectory: () => false,
|
||||
birthtime: createdTime,
|
||||
mtime: modifiedTime,
|
||||
atime: accessTime,
|
||||
size: 2048,
|
||||
} as any);
|
||||
|
||||
const result = await localFileCtr.listLocalFiles({ path: '/test' });
|
||||
|
||||
expect(result.files).toHaveLength(1);
|
||||
expect(result.totalCount).toBe(1);
|
||||
expect(result.files[0]).toEqual({
|
||||
name: 'document.pdf',
|
||||
path: '/test/document.pdf',
|
||||
isDirectory: false,
|
||||
size: 2048,
|
||||
type: 'pdf',
|
||||
createdTime,
|
||||
modifiedTime,
|
||||
lastAccessTime: accessTime,
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty result when directory read fails', async () => {
|
||||
vi.mocked(mockFsPromises.readdir).mockRejectedValue(new Error('Permission denied'));
|
||||
|
||||
const result = await localFileCtr.listLocalFiles({ path: '/protected' });
|
||||
|
||||
expect(result.files).toEqual([]);
|
||||
expect(result.totalCount).toBe(0);
|
||||
});
|
||||
|
||||
it('should skip files that cannot be stat', async () => {
|
||||
vi.mocked(mockFsPromises.readdir).mockResolvedValue(['good.txt', 'bad.txt']);
|
||||
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
||||
if ((filePath as string).includes('bad.txt')) {
|
||||
throw new Error('Cannot stat file');
|
||||
}
|
||||
return {
|
||||
isDirectory: () => false,
|
||||
birthtime: new Date('2024-01-01'),
|
||||
mtime: new Date('2024-01-15'),
|
||||
atime: new Date('2024-01-20'),
|
||||
size: 1024,
|
||||
} as any;
|
||||
});
|
||||
|
||||
const result = await localFileCtr.listLocalFiles({ path: '/test' });
|
||||
|
||||
// Should only contain good.txt, bad.txt should be skipped
|
||||
expect(result.files).toHaveLength(1);
|
||||
expect(result.totalCount).toBe(1);
|
||||
expect(result.files[0].name).toBe('good.txt');
|
||||
});
|
||||
|
||||
it('should handle directory type correctly', async () => {
|
||||
vi.mocked(mockFsPromises.readdir).mockResolvedValue(['my_folder']);
|
||||
vi.mocked(mockFsPromises.stat).mockResolvedValue({
|
||||
isDirectory: () => true,
|
||||
birthtime: new Date('2024-01-01'),
|
||||
mtime: new Date('2024-01-15'),
|
||||
atime: new Date('2024-01-20'),
|
||||
size: 4096,
|
||||
} as any);
|
||||
|
||||
const result = await localFileCtr.listLocalFiles({ path: '/test' });
|
||||
|
||||
expect(result.files).toHaveLength(1);
|
||||
expect(result.totalCount).toBe(1);
|
||||
expect(result.files[0].isDirectory).toBe(true);
|
||||
expect(result.files[0].type).toBe('directory');
|
||||
});
|
||||
|
||||
it('should handle files without extension', async () => {
|
||||
vi.mocked(mockFsPromises.readdir).mockResolvedValue(['Makefile', 'README']);
|
||||
vi.mocked(mockFsPromises.stat).mockResolvedValue({
|
||||
isDirectory: () => false,
|
||||
birthtime: new Date('2024-01-01'),
|
||||
mtime: new Date('2024-01-15'),
|
||||
atime: new Date('2024-01-20'),
|
||||
size: 512,
|
||||
} as any);
|
||||
|
||||
const result = await localFileCtr.listLocalFiles({ path: '/test' });
|
||||
|
||||
expect(result.files).toHaveLength(2);
|
||||
expect(result.totalCount).toBe(2);
|
||||
// Files without extension should have empty type
|
||||
expect(result.files[0].type).toBe('');
|
||||
expect(result.files[1].type).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleGrepContent', () => {
|
||||
it('should search content in a single file', async () => {
|
||||
vi.mocked(mockFsPromises.stat).mockResolvedValue({
|
||||
|
||||
Reference in New Issue
Block a user