@lobehub/lobehub 2.1.4 → 2.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +50 -0
- package/apps/desktop/package.json +4 -4
- package/apps/desktop/src/main/controllers/LocalFileCtr.ts +45 -11
- package/apps/desktop/src/main/controllers/__tests__/LocalFileCtr.test.ts +509 -1
- package/changelog/v2.json +18 -0
- package/locales/en-US/plugin.json +1 -0
- package/locales/zh-CN/plugin.json +1 -0
- package/package.json +1 -1
- package/packages/builtin-tool-cloud-sandbox/src/ExecutionRuntime/index.ts +4 -4
- package/packages/builtin-tool-local-system/package.json +1 -2
- package/packages/builtin-tool-local-system/src/client/Inspector/GlobLocalFiles/index.tsx +21 -22
- package/packages/builtin-tool-local-system/src/executor/index.ts +12 -3
- package/packages/builtin-tool-local-system/src/manifest.ts +18 -1
- package/packages/builtin-tool-local-system/src/systemRole.ts +8 -2
- package/packages/builtin-tool-local-system/src/types.ts +1 -0
- package/packages/const/src/file.ts +11 -1
- package/packages/const/src/index.ts +1 -0
- package/packages/const/src/version.ts +1 -1
- package/packages/electron-client-ipc/src/types/localSystem.ts +32 -0
- package/packages/file-loaders/src/blackList.ts +8 -1
- package/packages/prompts/src/prompts/fileSystem/formatFileList.test.ts +297 -5
- package/packages/prompts/src/prompts/fileSystem/formatFileList.ts +86 -3
- package/src/envs/file.ts +1 -1
- package/src/features/ChatInput/ActionBar/Tools/KlavisServerItem.tsx +28 -5
- package/src/features/ChatInput/ActionBar/Tools/LobehubSkillServerItem.tsx +32 -9
- package/src/features/ChatInput/ActionBar/Tools/useControls.tsx +16 -3
- package/src/features/Conversation/Messages/AssistantGroup/Tool/Inspector/ToolTitle.tsx +100 -32
- package/src/features/Conversation/Messages/AssistantGroup/Tool/Inspector/index.tsx +5 -0
- package/src/features/Conversation/Messages/Supervisor/components/ContentBlock.tsx +1 -1
- package/src/features/ProfileEditor/AgentTool.tsx +10 -3
- package/src/locales/default/plugin.ts +1 -0
- package/src/services/electron/localFileService.ts +2 -1
- package/packages/builtin-tool-local-system/src/ExecutionRuntime/index.ts +0 -466
- package/src/features/Conversation/Messages/Supervisor/components/MessageContent.tsx +0 -29
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,56 @@
|
|
|
2
2
|
|
|
3
3
|
# Changelog
|
|
4
4
|
|
|
5
|
+
### [Version 2.1.6](https://github.com/lobehub/lobe-chat/compare/v2.1.5...v2.1.6)
|
|
6
|
+
|
|
7
|
+
<sup>Released on **2026-02-01**</sup>
|
|
8
|
+
|
|
9
|
+
#### 💄 Styles
|
|
10
|
+
|
|
11
|
+
- **misc**: Improve local-system tool implement.
|
|
12
|
+
|
|
13
|
+
<br/>
|
|
14
|
+
|
|
15
|
+
<details>
|
|
16
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
17
|
+
|
|
18
|
+
#### Styles
|
|
19
|
+
|
|
20
|
+
- **misc**: Improve local-system tool implement, closes [#12022](https://github.com/lobehub/lobe-chat/issues/12022) ([5e203b8](https://github.com/lobehub/lobe-chat/commit/5e203b8))
|
|
21
|
+
|
|
22
|
+
</details>
|
|
23
|
+
|
|
24
|
+
<div align="right">
|
|
25
|
+
|
|
26
|
+
[](#readme-top)
|
|
27
|
+
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
### [Version 2.1.5](https://github.com/lobehub/lobe-chat/compare/v2.1.4...v2.1.5)
|
|
31
|
+
|
|
32
|
+
<sup>Released on **2026-01-31**</sup>
|
|
33
|
+
|
|
34
|
+
#### 🐛 Bug Fixes
|
|
35
|
+
|
|
36
|
+
- **misc**: Slove the group member agents cant set skills problem.
|
|
37
|
+
|
|
38
|
+
<br/>
|
|
39
|
+
|
|
40
|
+
<details>
|
|
41
|
+
<summary><kbd>Improvements and Fixes</kbd></summary>
|
|
42
|
+
|
|
43
|
+
#### What's fixed
|
|
44
|
+
|
|
45
|
+
- **misc**: Slove the group member agents cant set skills problem, closes [#12021](https://github.com/lobehub/lobe-chat/issues/12021) ([2302940](https://github.com/lobehub/lobe-chat/commit/2302940))
|
|
46
|
+
|
|
47
|
+
</details>
|
|
48
|
+
|
|
49
|
+
<div align="right">
|
|
50
|
+
|
|
51
|
+
[](#readme-top)
|
|
52
|
+
|
|
53
|
+
</div>
|
|
54
|
+
|
|
5
55
|
### [Version 2.1.4](https://github.com/lobehub/lobe-chat/compare/v2.1.3...v2.1.4)
|
|
6
56
|
|
|
7
57
|
<sup>Released on **2026-01-31**</sup>
|
|
@@ -12,11 +12,11 @@
|
|
|
12
12
|
"main": "./dist/main/index.js",
|
|
13
13
|
"scripts": {
|
|
14
14
|
"build": "electron-vite build",
|
|
15
|
-
"build-local": "npm run build && electron-builder --dir --config electron-builder.mjs --c.mac.notarize=false -c.mac.identity=null --c.asar=false",
|
|
16
15
|
"build:linux": "npm run build && electron-builder --linux --config electron-builder.mjs --publish never",
|
|
17
16
|
"build:mac": "npm run build && electron-builder --mac --config electron-builder.mjs --publish never",
|
|
18
17
|
"build:mac:local": "npm run build && UPDATE_CHANNEL=nightly electron-builder --mac --config electron-builder.mjs --publish never",
|
|
19
18
|
"build:win": "npm run build && electron-builder --win --config electron-builder.mjs --publish never",
|
|
19
|
+
"build-local": "npm run build && electron-builder --dir --config electron-builder.mjs --c.mac.notarize=false -c.mac.identity=null --c.asar=false",
|
|
20
20
|
"dev": "electron-vite dev",
|
|
21
21
|
"dev:static": "cross-env DESKTOP_RENDERER_STATIC=1 npm run electron:dev",
|
|
22
22
|
"electron:dev": "electron-vite dev",
|
|
@@ -47,9 +47,6 @@
|
|
|
47
47
|
"get-port-please": "^3.2.0",
|
|
48
48
|
"superjson": "^2.2.6"
|
|
49
49
|
},
|
|
50
|
-
"optionalDependencies": {
|
|
51
|
-
"node-mac-permissions": "^2.5.0"
|
|
52
|
-
},
|
|
53
50
|
"devDependencies": {
|
|
54
51
|
"@electron-toolkit/eslint-config-prettier": "^3.0.0",
|
|
55
52
|
"@electron-toolkit/eslint-config-ts": "^3.1.0",
|
|
@@ -103,6 +100,9 @@
|
|
|
103
100
|
"vitest": "^3.2.4",
|
|
104
101
|
"zod": "^3.25.76"
|
|
105
102
|
},
|
|
103
|
+
"optionalDependencies": {
|
|
104
|
+
"node-mac-permissions": "^2.5.0"
|
|
105
|
+
},
|
|
106
106
|
"pnpm": {
|
|
107
107
|
"onlyBuiltDependencies": [
|
|
108
108
|
"@napi-rs/canvas",
|
|
@@ -218,8 +218,13 @@ export default class LocalFileCtr extends ControllerModule {
|
|
|
218
218
|
}
|
|
219
219
|
|
|
220
220
|
@IpcMethod()
|
|
221
|
-
async listLocalFiles({
|
|
222
|
-
|
|
221
|
+
async listLocalFiles({
|
|
222
|
+
path: dirPath,
|
|
223
|
+
sortBy = 'modifiedTime',
|
|
224
|
+
sortOrder = 'desc',
|
|
225
|
+
limit = 100,
|
|
226
|
+
}: ListLocalFileParams): Promise<{ files: FileResult[]; totalCount: number }> {
|
|
227
|
+
logger.debug('Listing directory contents:', { dirPath, limit, sortBy, sortOrder });
|
|
223
228
|
|
|
224
229
|
const results: FileResult[] = [];
|
|
225
230
|
try {
|
|
@@ -256,22 +261,51 @@ export default class LocalFileCtr extends ControllerModule {
|
|
|
256
261
|
}
|
|
257
262
|
}
|
|
258
263
|
|
|
259
|
-
// Sort entries
|
|
264
|
+
// Sort entries based on sortBy and sortOrder
|
|
260
265
|
results.sort((a, b) => {
|
|
261
|
-
|
|
262
|
-
|
|
266
|
+
let comparison = 0;
|
|
267
|
+
|
|
268
|
+
switch (sortBy) {
|
|
269
|
+
case 'name': {
|
|
270
|
+
comparison = (a.name || '').localeCompare(b.name || '');
|
|
271
|
+
break;
|
|
272
|
+
}
|
|
273
|
+
case 'modifiedTime': {
|
|
274
|
+
comparison = a.modifiedTime.getTime() - b.modifiedTime.getTime();
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
277
|
+
case 'createdTime': {
|
|
278
|
+
comparison = a.createdTime.getTime() - b.createdTime.getTime();
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
case 'size': {
|
|
282
|
+
comparison = a.size - b.size;
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
default: {
|
|
286
|
+
comparison = a.modifiedTime.getTime() - b.modifiedTime.getTime();
|
|
287
|
+
}
|
|
263
288
|
}
|
|
264
|
-
|
|
265
|
-
return
|
|
289
|
+
|
|
290
|
+
return sortOrder === 'desc' ? -comparison : comparison;
|
|
266
291
|
});
|
|
267
292
|
|
|
268
|
-
|
|
269
|
-
|
|
293
|
+
const totalCount = results.length;
|
|
294
|
+
|
|
295
|
+
// Apply limit
|
|
296
|
+
const limitedResults = results.slice(0, limit);
|
|
297
|
+
|
|
298
|
+
logger.debug('Directory listing successful', {
|
|
299
|
+
dirPath,
|
|
300
|
+
resultCount: limitedResults.length,
|
|
301
|
+
totalCount,
|
|
302
|
+
});
|
|
303
|
+
return { files: limitedResults, totalCount };
|
|
270
304
|
} catch (error) {
|
|
271
305
|
logger.error(`Failed to list directory ${dirPath}:`, error);
|
|
272
306
|
// Rethrow or return an empty array/error object depending on desired behavior
|
|
273
|
-
// For now, returning empty
|
|
274
|
-
return [];
|
|
307
|
+
// For now, returning empty result on error listing directory itself
|
|
308
|
+
return { files: [], totalCount: 0 };
|
|
275
309
|
}
|
|
276
310
|
}
|
|
277
311
|
|
|
@@ -20,8 +20,8 @@ vi.mock('@/utils/logger', () => ({
|
|
|
20
20
|
|
|
21
21
|
// Mock file-loaders
|
|
22
22
|
vi.mock('@lobechat/file-loaders', () => ({
|
|
23
|
-
SYSTEM_FILES_TO_IGNORE: ['.DS_Store', 'Thumbs.db'],
|
|
24
23
|
loadFile: vi.fn(),
|
|
24
|
+
SYSTEM_FILES_TO_IGNORE: ['.DS_Store', 'Thumbs.db', '$RECYCLE.BIN'],
|
|
25
25
|
}));
|
|
26
26
|
|
|
27
27
|
// Mock electron
|
|
@@ -553,6 +553,514 @@ describe('LocalFileCtr', () => {
|
|
|
553
553
|
});
|
|
554
554
|
});
|
|
555
555
|
|
|
556
|
+
describe('listLocalFiles', () => {
|
|
557
|
+
it('should list directory contents successfully', async () => {
|
|
558
|
+
vi.mocked(mockFsPromises.readdir).mockResolvedValue(['file1.txt', 'file2.txt', 'folder1']);
|
|
559
|
+
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
|
560
|
+
const name = (filePath as string).split('/').pop();
|
|
561
|
+
if (name === 'folder1') {
|
|
562
|
+
return {
|
|
563
|
+
isDirectory: () => true,
|
|
564
|
+
birthtime: new Date('2024-01-01'),
|
|
565
|
+
mtime: new Date('2024-01-15'),
|
|
566
|
+
atime: new Date('2024-01-20'),
|
|
567
|
+
size: 4096,
|
|
568
|
+
} as any;
|
|
569
|
+
}
|
|
570
|
+
return {
|
|
571
|
+
isDirectory: () => false,
|
|
572
|
+
birthtime: new Date('2024-01-02'),
|
|
573
|
+
mtime: new Date('2024-01-10'),
|
|
574
|
+
atime: new Date('2024-01-18'),
|
|
575
|
+
size: 1024,
|
|
576
|
+
} as any;
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
const result = await localFileCtr.listLocalFiles({ path: '/test' });
|
|
580
|
+
|
|
581
|
+
expect(result.files).toHaveLength(3);
|
|
582
|
+
expect(result.totalCount).toBe(3);
|
|
583
|
+
expect(mockFsPromises.readdir).toHaveBeenCalledWith('/test');
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
it('should filter out system files like .DS_Store and Thumbs.db', async () => {
|
|
587
|
+
vi.mocked(mockFsPromises.readdir).mockResolvedValue([
|
|
588
|
+
'file1.txt',
|
|
589
|
+
'.DS_Store',
|
|
590
|
+
'Thumbs.db',
|
|
591
|
+
'folder1',
|
|
592
|
+
]);
|
|
593
|
+
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
|
594
|
+
const name = (filePath as string).split('/').pop();
|
|
595
|
+
if (name === 'folder1') {
|
|
596
|
+
return {
|
|
597
|
+
isDirectory: () => true,
|
|
598
|
+
birthtime: new Date('2024-01-01'),
|
|
599
|
+
mtime: new Date('2024-01-15'),
|
|
600
|
+
atime: new Date('2024-01-20'),
|
|
601
|
+
size: 4096,
|
|
602
|
+
} as any;
|
|
603
|
+
}
|
|
604
|
+
return {
|
|
605
|
+
isDirectory: () => false,
|
|
606
|
+
birthtime: new Date('2024-01-02'),
|
|
607
|
+
mtime: new Date('2024-01-10'),
|
|
608
|
+
atime: new Date('2024-01-18'),
|
|
609
|
+
size: 1024,
|
|
610
|
+
} as any;
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
const result = await localFileCtr.listLocalFiles({ path: '/test' });
|
|
614
|
+
|
|
615
|
+
// Should only contain file1.txt and folder1, not .DS_Store or Thumbs.db
|
|
616
|
+
expect(result.files).toHaveLength(2);
|
|
617
|
+
expect(result.totalCount).toBe(2);
|
|
618
|
+
expect(result.files.map((r) => r.name)).not.toContain('.DS_Store');
|
|
619
|
+
expect(result.files.map((r) => r.name)).not.toContain('Thumbs.db');
|
|
620
|
+
expect(result.files.map((r) => r.name)).toContain('folder1');
|
|
621
|
+
expect(result.files.map((r) => r.name)).toContain('file1.txt');
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
it('should filter out $RECYCLE.BIN system folder', async () => {
|
|
625
|
+
vi.mocked(mockFsPromises.readdir).mockResolvedValue(['file1.txt', '$RECYCLE.BIN', 'folder1']);
|
|
626
|
+
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
|
627
|
+
const name = (filePath as string).split('/').pop();
|
|
628
|
+
const isDir = name === 'folder1' || name === '$RECYCLE.BIN';
|
|
629
|
+
return {
|
|
630
|
+
isDirectory: () => isDir,
|
|
631
|
+
birthtime: new Date('2024-01-01'),
|
|
632
|
+
mtime: new Date('2024-01-15'),
|
|
633
|
+
atime: new Date('2024-01-20'),
|
|
634
|
+
size: isDir ? 4096 : 1024,
|
|
635
|
+
} as any;
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
const result = await localFileCtr.listLocalFiles({ path: '/test' });
|
|
639
|
+
|
|
640
|
+
// Should not contain $RECYCLE.BIN
|
|
641
|
+
expect(result.files).toHaveLength(2);
|
|
642
|
+
expect(result.totalCount).toBe(2);
|
|
643
|
+
expect(result.files.map((r) => r.name)).not.toContain('$RECYCLE.BIN');
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
it('should sort by name ascending when specified', async () => {
|
|
647
|
+
vi.mocked(mockFsPromises.readdir).mockResolvedValue(['zebra.txt', 'alpha.txt', 'apple.txt']);
|
|
648
|
+
vi.mocked(mockFsPromises.stat).mockResolvedValue({
|
|
649
|
+
isDirectory: () => false,
|
|
650
|
+
birthtime: new Date('2024-01-01'),
|
|
651
|
+
mtime: new Date('2024-01-15'),
|
|
652
|
+
atime: new Date('2024-01-20'),
|
|
653
|
+
size: 1024,
|
|
654
|
+
} as any);
|
|
655
|
+
|
|
656
|
+
const result = await localFileCtr.listLocalFiles({
|
|
657
|
+
path: '/test',
|
|
658
|
+
sortBy: 'name',
|
|
659
|
+
sortOrder: 'asc',
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
expect(result.files.map((r) => r.name)).toEqual(['alpha.txt', 'apple.txt', 'zebra.txt']);
|
|
663
|
+
});
|
|
664
|
+
|
|
665
|
+
it('should sort by modifiedTime descending by default', async () => {
|
|
666
|
+
vi.mocked(mockFsPromises.readdir).mockResolvedValue(['old.txt', 'new.txt', 'mid.txt']);
|
|
667
|
+
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
|
668
|
+
const name = (filePath as string).split('/').pop();
|
|
669
|
+
const dates: Record<string, Date> = {
|
|
670
|
+
'new.txt': new Date('2024-01-20'),
|
|
671
|
+
'mid.txt': new Date('2024-01-15'),
|
|
672
|
+
'old.txt': new Date('2024-01-01'),
|
|
673
|
+
};
|
|
674
|
+
return {
|
|
675
|
+
isDirectory: () => false,
|
|
676
|
+
birthtime: new Date('2024-01-01'),
|
|
677
|
+
mtime: dates[name!] || new Date('2024-01-01'),
|
|
678
|
+
atime: new Date('2024-01-20'),
|
|
679
|
+
size: 1024,
|
|
680
|
+
} as any;
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
const result = await localFileCtr.listLocalFiles({ path: '/test' });
|
|
684
|
+
|
|
685
|
+
// Default sort: modifiedTime descending (newest first)
|
|
686
|
+
expect(result.files.map((r) => r.name)).toEqual(['new.txt', 'mid.txt', 'old.txt']);
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
it('should sort by size ascending when specified', async () => {
|
|
690
|
+
vi.mocked(mockFsPromises.readdir).mockResolvedValue(['large.txt', 'small.txt', 'medium.txt']);
|
|
691
|
+
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
|
692
|
+
const name = (filePath as string).split('/').pop();
|
|
693
|
+
const sizes: Record<string, number> = {
|
|
694
|
+
'large.txt': 10000,
|
|
695
|
+
'medium.txt': 5000,
|
|
696
|
+
'small.txt': 1000,
|
|
697
|
+
};
|
|
698
|
+
return {
|
|
699
|
+
isDirectory: () => false,
|
|
700
|
+
birthtime: new Date('2024-01-01'),
|
|
701
|
+
mtime: new Date('2024-01-15'),
|
|
702
|
+
atime: new Date('2024-01-20'),
|
|
703
|
+
size: sizes[name!] || 1024,
|
|
704
|
+
} as any;
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
const result = await localFileCtr.listLocalFiles({
|
|
708
|
+
path: '/test',
|
|
709
|
+
sortBy: 'size',
|
|
710
|
+
sortOrder: 'asc',
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
expect(result.files.map((r) => r.name)).toEqual(['small.txt', 'medium.txt', 'large.txt']);
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
it('should apply limit parameter', async () => {
|
|
717
|
+
vi.mocked(mockFsPromises.readdir).mockResolvedValue([
|
|
718
|
+
'file1.txt',
|
|
719
|
+
'file2.txt',
|
|
720
|
+
'file3.txt',
|
|
721
|
+
'file4.txt',
|
|
722
|
+
'file5.txt',
|
|
723
|
+
]);
|
|
724
|
+
vi.mocked(mockFsPromises.stat).mockResolvedValue({
|
|
725
|
+
isDirectory: () => false,
|
|
726
|
+
birthtime: new Date('2024-01-01'),
|
|
727
|
+
mtime: new Date('2024-01-15'),
|
|
728
|
+
atime: new Date('2024-01-20'),
|
|
729
|
+
size: 1024,
|
|
730
|
+
} as any);
|
|
731
|
+
|
|
732
|
+
const result = await localFileCtr.listLocalFiles({
|
|
733
|
+
path: '/test',
|
|
734
|
+
limit: 3,
|
|
735
|
+
});
|
|
736
|
+
|
|
737
|
+
expect(result.files).toHaveLength(3);
|
|
738
|
+
expect(result.totalCount).toBe(5); // Total is 5, but limited to 3
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
it('should use default limit of 100', async () => {
|
|
742
|
+
// Create 150 files
|
|
743
|
+
const files = Array.from({ length: 150 }, (_, i) => `file${i}.txt`);
|
|
744
|
+
vi.mocked(mockFsPromises.readdir).mockResolvedValue(files);
|
|
745
|
+
vi.mocked(mockFsPromises.stat).mockResolvedValue({
|
|
746
|
+
isDirectory: () => false,
|
|
747
|
+
birthtime: new Date('2024-01-01'),
|
|
748
|
+
mtime: new Date('2024-01-15'),
|
|
749
|
+
atime: new Date('2024-01-20'),
|
|
750
|
+
size: 1024,
|
|
751
|
+
} as any);
|
|
752
|
+
|
|
753
|
+
const result = await localFileCtr.listLocalFiles({ path: '/test' });
|
|
754
|
+
|
|
755
|
+
expect(result.files).toHaveLength(100);
|
|
756
|
+
expect(result.totalCount).toBe(150); // Total is 150, but limited to 100
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
it('should sort by createdTime ascending when specified', async () => {
|
|
760
|
+
vi.mocked(mockFsPromises.readdir).mockResolvedValue([
|
|
761
|
+
'newest.txt',
|
|
762
|
+
'oldest.txt',
|
|
763
|
+
'middle.txt',
|
|
764
|
+
]);
|
|
765
|
+
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
|
766
|
+
const name = (filePath as string).split('/').pop();
|
|
767
|
+
const dates: Record<string, Date> = {
|
|
768
|
+
'newest.txt': new Date('2024-03-01'),
|
|
769
|
+
'middle.txt': new Date('2024-02-01'),
|
|
770
|
+
'oldest.txt': new Date('2024-01-01'),
|
|
771
|
+
};
|
|
772
|
+
return {
|
|
773
|
+
isDirectory: () => false,
|
|
774
|
+
birthtime: dates[name!] || new Date('2024-01-01'),
|
|
775
|
+
mtime: new Date('2024-01-15'),
|
|
776
|
+
atime: new Date('2024-01-20'),
|
|
777
|
+
size: 1024,
|
|
778
|
+
} as any;
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
const result = await localFileCtr.listLocalFiles({
|
|
782
|
+
path: '/test',
|
|
783
|
+
sortBy: 'createdTime',
|
|
784
|
+
sortOrder: 'asc',
|
|
785
|
+
});
|
|
786
|
+
|
|
787
|
+
expect(result.files.map((r) => r.name)).toEqual(['oldest.txt', 'middle.txt', 'newest.txt']);
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
it('should sort by createdTime descending when specified', async () => {
|
|
791
|
+
vi.mocked(mockFsPromises.readdir).mockResolvedValue([
|
|
792
|
+
'newest.txt',
|
|
793
|
+
'oldest.txt',
|
|
794
|
+
'middle.txt',
|
|
795
|
+
]);
|
|
796
|
+
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
|
797
|
+
const name = (filePath as string).split('/').pop();
|
|
798
|
+
const dates: Record<string, Date> = {
|
|
799
|
+
'newest.txt': new Date('2024-03-01'),
|
|
800
|
+
'middle.txt': new Date('2024-02-01'),
|
|
801
|
+
'oldest.txt': new Date('2024-01-01'),
|
|
802
|
+
};
|
|
803
|
+
return {
|
|
804
|
+
isDirectory: () => false,
|
|
805
|
+
birthtime: dates[name!] || new Date('2024-01-01'),
|
|
806
|
+
mtime: new Date('2024-01-15'),
|
|
807
|
+
atime: new Date('2024-01-20'),
|
|
808
|
+
size: 1024,
|
|
809
|
+
} as any;
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
const result = await localFileCtr.listLocalFiles({
|
|
813
|
+
path: '/test',
|
|
814
|
+
sortBy: 'createdTime',
|
|
815
|
+
sortOrder: 'desc',
|
|
816
|
+
});
|
|
817
|
+
|
|
818
|
+
expect(result.files.map((r) => r.name)).toEqual(['newest.txt', 'middle.txt', 'oldest.txt']);
|
|
819
|
+
});
|
|
820
|
+
|
|
821
|
+
it('should sort by name descending when specified', async () => {
|
|
822
|
+
vi.mocked(mockFsPromises.readdir).mockResolvedValue(['alpha.txt', 'zebra.txt', 'middle.txt']);
|
|
823
|
+
vi.mocked(mockFsPromises.stat).mockResolvedValue({
|
|
824
|
+
isDirectory: () => false,
|
|
825
|
+
birthtime: new Date('2024-01-01'),
|
|
826
|
+
mtime: new Date('2024-01-15'),
|
|
827
|
+
atime: new Date('2024-01-20'),
|
|
828
|
+
size: 1024,
|
|
829
|
+
} as any);
|
|
830
|
+
|
|
831
|
+
const result = await localFileCtr.listLocalFiles({
|
|
832
|
+
path: '/test',
|
|
833
|
+
sortBy: 'name',
|
|
834
|
+
sortOrder: 'desc',
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
expect(result.files.map((r) => r.name)).toEqual(['zebra.txt', 'middle.txt', 'alpha.txt']);
|
|
838
|
+
});
|
|
839
|
+
|
|
840
|
+
it('should sort by size descending when specified', async () => {
|
|
841
|
+
vi.mocked(mockFsPromises.readdir).mockResolvedValue(['small.txt', 'large.txt', 'medium.txt']);
|
|
842
|
+
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
|
843
|
+
const name = (filePath as string).split('/').pop();
|
|
844
|
+
const sizes: Record<string, number> = {
|
|
845
|
+
'large.txt': 10000,
|
|
846
|
+
'medium.txt': 5000,
|
|
847
|
+
'small.txt': 1000,
|
|
848
|
+
};
|
|
849
|
+
return {
|
|
850
|
+
isDirectory: () => false,
|
|
851
|
+
birthtime: new Date('2024-01-01'),
|
|
852
|
+
mtime: new Date('2024-01-15'),
|
|
853
|
+
atime: new Date('2024-01-20'),
|
|
854
|
+
size: sizes[name!] || 1024,
|
|
855
|
+
} as any;
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
const result = await localFileCtr.listLocalFiles({
|
|
859
|
+
path: '/test',
|
|
860
|
+
sortBy: 'size',
|
|
861
|
+
sortOrder: 'desc',
|
|
862
|
+
});
|
|
863
|
+
|
|
864
|
+
expect(result.files.map((r) => r.name)).toEqual(['large.txt', 'medium.txt', 'small.txt']);
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
it('should sort by modifiedTime ascending when specified', async () => {
|
|
868
|
+
vi.mocked(mockFsPromises.readdir).mockResolvedValue(['old.txt', 'new.txt', 'mid.txt']);
|
|
869
|
+
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
|
870
|
+
const name = (filePath as string).split('/').pop();
|
|
871
|
+
const dates: Record<string, Date> = {
|
|
872
|
+
'new.txt': new Date('2024-01-20'),
|
|
873
|
+
'mid.txt': new Date('2024-01-15'),
|
|
874
|
+
'old.txt': new Date('2024-01-01'),
|
|
875
|
+
};
|
|
876
|
+
return {
|
|
877
|
+
isDirectory: () => false,
|
|
878
|
+
birthtime: new Date('2024-01-01'),
|
|
879
|
+
mtime: dates[name!] || new Date('2024-01-01'),
|
|
880
|
+
atime: new Date('2024-01-20'),
|
|
881
|
+
size: 1024,
|
|
882
|
+
} as any;
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
const result = await localFileCtr.listLocalFiles({
|
|
886
|
+
path: '/test',
|
|
887
|
+
sortBy: 'modifiedTime',
|
|
888
|
+
sortOrder: 'asc',
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
expect(result.files.map((r) => r.name)).toEqual(['old.txt', 'mid.txt', 'new.txt']);
|
|
892
|
+
});
|
|
893
|
+
|
|
894
|
+
it('should handle empty directory with sort options', async () => {
|
|
895
|
+
vi.mocked(mockFsPromises.readdir).mockResolvedValue([]);
|
|
896
|
+
|
|
897
|
+
const result = await localFileCtr.listLocalFiles({
|
|
898
|
+
path: '/empty',
|
|
899
|
+
sortBy: 'name',
|
|
900
|
+
sortOrder: 'asc',
|
|
901
|
+
});
|
|
902
|
+
|
|
903
|
+
expect(result.files).toEqual([]);
|
|
904
|
+
expect(result.totalCount).toBe(0);
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
it('should apply limit after sorting', async () => {
|
|
908
|
+
vi.mocked(mockFsPromises.readdir).mockResolvedValue([
|
|
909
|
+
'file1.txt',
|
|
910
|
+
'file2.txt',
|
|
911
|
+
'file3.txt',
|
|
912
|
+
'file4.txt',
|
|
913
|
+
'file5.txt',
|
|
914
|
+
]);
|
|
915
|
+
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
|
916
|
+
const name = (filePath as string).split('/').pop();
|
|
917
|
+
const dates: Record<string, Date> = {
|
|
918
|
+
'file1.txt': new Date('2024-01-01'),
|
|
919
|
+
'file2.txt': new Date('2024-01-02'),
|
|
920
|
+
'file3.txt': new Date('2024-01-03'),
|
|
921
|
+
'file4.txt': new Date('2024-01-04'),
|
|
922
|
+
'file5.txt': new Date('2024-01-05'),
|
|
923
|
+
};
|
|
924
|
+
return {
|
|
925
|
+
isDirectory: () => false,
|
|
926
|
+
birthtime: new Date('2024-01-01'),
|
|
927
|
+
mtime: dates[name!] || new Date('2024-01-01'),
|
|
928
|
+
atime: new Date('2024-01-20'),
|
|
929
|
+
size: 1024,
|
|
930
|
+
} as any;
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
// Sort by modifiedTime desc (default) and limit to 3
|
|
934
|
+
const result = await localFileCtr.listLocalFiles({
|
|
935
|
+
path: '/test',
|
|
936
|
+
limit: 3,
|
|
937
|
+
});
|
|
938
|
+
|
|
939
|
+
// Should get the 3 newest files
|
|
940
|
+
expect(result.files).toHaveLength(3);
|
|
941
|
+
expect(result.totalCount).toBe(5); // Total is 5, but limited to 3
|
|
942
|
+
expect(result.files.map((r) => r.name)).toEqual(['file5.txt', 'file4.txt', 'file3.txt']);
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
it('should handle limit larger than file count', async () => {
|
|
946
|
+
vi.mocked(mockFsPromises.readdir).mockResolvedValue(['file1.txt', 'file2.txt']);
|
|
947
|
+
vi.mocked(mockFsPromises.stat).mockResolvedValue({
|
|
948
|
+
isDirectory: () => false,
|
|
949
|
+
birthtime: new Date('2024-01-01'),
|
|
950
|
+
mtime: new Date('2024-01-15'),
|
|
951
|
+
atime: new Date('2024-01-20'),
|
|
952
|
+
size: 1024,
|
|
953
|
+
} as any);
|
|
954
|
+
|
|
955
|
+
const result = await localFileCtr.listLocalFiles({
|
|
956
|
+
path: '/test',
|
|
957
|
+
limit: 1000,
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
expect(result.files).toHaveLength(2);
|
|
961
|
+
expect(result.totalCount).toBe(2);
|
|
962
|
+
});
|
|
963
|
+
|
|
964
|
+
it('should return file metadata including size, times and type', async () => {
|
|
965
|
+
const createdTime = new Date('2024-01-01');
|
|
966
|
+
const modifiedTime = new Date('2024-01-15');
|
|
967
|
+
const accessTime = new Date('2024-01-20');
|
|
968
|
+
|
|
969
|
+
vi.mocked(mockFsPromises.readdir).mockResolvedValue(['document.pdf']);
|
|
970
|
+
vi.mocked(mockFsPromises.stat).mockResolvedValue({
|
|
971
|
+
isDirectory: () => false,
|
|
972
|
+
birthtime: createdTime,
|
|
973
|
+
mtime: modifiedTime,
|
|
974
|
+
atime: accessTime,
|
|
975
|
+
size: 2048,
|
|
976
|
+
} as any);
|
|
977
|
+
|
|
978
|
+
const result = await localFileCtr.listLocalFiles({ path: '/test' });
|
|
979
|
+
|
|
980
|
+
expect(result.files).toHaveLength(1);
|
|
981
|
+
expect(result.totalCount).toBe(1);
|
|
982
|
+
expect(result.files[0]).toEqual({
|
|
983
|
+
name: 'document.pdf',
|
|
984
|
+
path: '/test/document.pdf',
|
|
985
|
+
isDirectory: false,
|
|
986
|
+
size: 2048,
|
|
987
|
+
type: 'pdf',
|
|
988
|
+
createdTime,
|
|
989
|
+
modifiedTime,
|
|
990
|
+
lastAccessTime: accessTime,
|
|
991
|
+
});
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
it('should return empty result when directory read fails', async () => {
|
|
995
|
+
vi.mocked(mockFsPromises.readdir).mockRejectedValue(new Error('Permission denied'));
|
|
996
|
+
|
|
997
|
+
const result = await localFileCtr.listLocalFiles({ path: '/protected' });
|
|
998
|
+
|
|
999
|
+
expect(result.files).toEqual([]);
|
|
1000
|
+
expect(result.totalCount).toBe(0);
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
it('should skip files that cannot be stat', async () => {
|
|
1004
|
+
vi.mocked(mockFsPromises.readdir).mockResolvedValue(['good.txt', 'bad.txt']);
|
|
1005
|
+
vi.mocked(mockFsPromises.stat).mockImplementation(async (filePath) => {
|
|
1006
|
+
if ((filePath as string).includes('bad.txt')) {
|
|
1007
|
+
throw new Error('Cannot stat file');
|
|
1008
|
+
}
|
|
1009
|
+
return {
|
|
1010
|
+
isDirectory: () => false,
|
|
1011
|
+
birthtime: new Date('2024-01-01'),
|
|
1012
|
+
mtime: new Date('2024-01-15'),
|
|
1013
|
+
atime: new Date('2024-01-20'),
|
|
1014
|
+
size: 1024,
|
|
1015
|
+
} as any;
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
const result = await localFileCtr.listLocalFiles({ path: '/test' });
|
|
1019
|
+
|
|
1020
|
+
// Should only contain good.txt, bad.txt should be skipped
|
|
1021
|
+
expect(result.files).toHaveLength(1);
|
|
1022
|
+
expect(result.totalCount).toBe(1);
|
|
1023
|
+
expect(result.files[0].name).toBe('good.txt');
|
|
1024
|
+
});
|
|
1025
|
+
|
|
1026
|
+
it('should handle directory type correctly', async () => {
|
|
1027
|
+
vi.mocked(mockFsPromises.readdir).mockResolvedValue(['my_folder']);
|
|
1028
|
+
vi.mocked(mockFsPromises.stat).mockResolvedValue({
|
|
1029
|
+
isDirectory: () => true,
|
|
1030
|
+
birthtime: new Date('2024-01-01'),
|
|
1031
|
+
mtime: new Date('2024-01-15'),
|
|
1032
|
+
atime: new Date('2024-01-20'),
|
|
1033
|
+
size: 4096,
|
|
1034
|
+
} as any);
|
|
1035
|
+
|
|
1036
|
+
const result = await localFileCtr.listLocalFiles({ path: '/test' });
|
|
1037
|
+
|
|
1038
|
+
expect(result.files).toHaveLength(1);
|
|
1039
|
+
expect(result.totalCount).toBe(1);
|
|
1040
|
+
expect(result.files[0].isDirectory).toBe(true);
|
|
1041
|
+
expect(result.files[0].type).toBe('directory');
|
|
1042
|
+
});
|
|
1043
|
+
|
|
1044
|
+
it('should handle files without extension', async () => {
|
|
1045
|
+
vi.mocked(mockFsPromises.readdir).mockResolvedValue(['Makefile', 'README']);
|
|
1046
|
+
vi.mocked(mockFsPromises.stat).mockResolvedValue({
|
|
1047
|
+
isDirectory: () => false,
|
|
1048
|
+
birthtime: new Date('2024-01-01'),
|
|
1049
|
+
mtime: new Date('2024-01-15'),
|
|
1050
|
+
atime: new Date('2024-01-20'),
|
|
1051
|
+
size: 512,
|
|
1052
|
+
} as any);
|
|
1053
|
+
|
|
1054
|
+
const result = await localFileCtr.listLocalFiles({ path: '/test' });
|
|
1055
|
+
|
|
1056
|
+
expect(result.files).toHaveLength(2);
|
|
1057
|
+
expect(result.totalCount).toBe(2);
|
|
1058
|
+
// Files without extension should have empty type
|
|
1059
|
+
expect(result.files[0].type).toBe('');
|
|
1060
|
+
expect(result.files[1].type).toBe('');
|
|
1061
|
+
});
|
|
1062
|
+
});
|
|
1063
|
+
|
|
556
1064
|
describe('handleGrepContent', () => {
|
|
557
1065
|
it('should search content in a single file', async () => {
|
|
558
1066
|
vi.mocked(mockFsPromises.stat).mockResolvedValue({
|