@lobehub/lobehub 2.0.0-next.142 → 2.0.0-next.143

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.
Files changed (54) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/apps/desktop/package.json +1 -0
  3. package/apps/desktop/src/main/core/ui/__tests__/MenuManager.test.ts +320 -0
  4. package/apps/desktop/src/main/core/ui/__tests__/Tray.test.ts +518 -0
  5. package/apps/desktop/src/main/core/ui/__tests__/TrayManager.test.ts +360 -0
  6. package/apps/desktop/src/main/menus/impls/BaseMenuPlatform.test.ts +49 -0
  7. package/apps/desktop/src/main/menus/impls/linux.test.ts +552 -0
  8. package/apps/desktop/src/main/menus/impls/macOS.test.ts +464 -0
  9. package/apps/desktop/src/main/menus/impls/windows.test.ts +429 -0
  10. package/apps/desktop/src/main/modules/fileSearch/__tests__/macOS.integration.test.ts +2 -2
  11. package/apps/desktop/src/main/services/__tests__/fileSearchSrv.test.ts +402 -0
  12. package/apps/desktop/src/main/utils/__tests__/file-system.test.ts +91 -0
  13. package/apps/desktop/src/main/utils/__tests__/logger.test.ts +229 -0
  14. package/apps/desktop/src/preload/electronApi.test.ts +142 -0
  15. package/apps/desktop/src/preload/invoke.test.ts +145 -0
  16. package/apps/desktop/src/preload/routeInterceptor.test.ts +374 -0
  17. package/apps/desktop/src/preload/streamer.test.ts +365 -0
  18. package/apps/desktop/vitest.config.mts +1 -0
  19. package/changelog/v1.json +9 -0
  20. package/locales/ar/marketAuth.json +13 -0
  21. package/locales/bg-BG/marketAuth.json +13 -0
  22. package/locales/de-DE/marketAuth.json +13 -0
  23. package/locales/en-US/marketAuth.json +13 -0
  24. package/locales/es-ES/marketAuth.json +13 -0
  25. package/locales/fa-IR/marketAuth.json +13 -0
  26. package/locales/fr-FR/marketAuth.json +13 -0
  27. package/locales/it-IT/marketAuth.json +13 -0
  28. package/locales/ja-JP/marketAuth.json +13 -0
  29. package/locales/ko-KR/marketAuth.json +13 -0
  30. package/locales/nl-NL/marketAuth.json +13 -0
  31. package/locales/pl-PL/marketAuth.json +13 -0
  32. package/locales/pt-BR/marketAuth.json +13 -0
  33. package/locales/ru-RU/marketAuth.json +13 -0
  34. package/locales/tr-TR/marketAuth.json +13 -0
  35. package/locales/vi-VN/marketAuth.json +13 -0
  36. package/locales/zh-CN/marketAuth.json +13 -0
  37. package/locales/zh-TW/marketAuth.json +13 -0
  38. package/package.json +1 -1
  39. package/packages/database/src/models/user.ts +2 -0
  40. package/packages/types/src/discover/mcp.ts +2 -1
  41. package/packages/types/src/tool/plugin.ts +2 -1
  42. package/src/app/[variants]/(main)/chat/settings/features/SmartAgentActionButton/MarketPublishButton.tsx +0 -2
  43. package/src/app/[variants]/(main)/discover/(detail)/mcp/features/Sidebar/ActionButton/index.tsx +33 -7
  44. package/src/features/PluginStore/McpList/List/Action.tsx +20 -1
  45. package/src/layout/AuthProvider/MarketAuth/MarketAuthConfirmModal.tsx +158 -0
  46. package/src/layout/AuthProvider/MarketAuth/MarketAuthProvider.tsx +130 -14
  47. package/src/libs/mcp/types.ts +8 -0
  48. package/src/locales/default/marketAuth.ts +13 -0
  49. package/src/server/routers/lambda/market/index.ts +85 -2
  50. package/src/server/services/discover/index.ts +45 -4
  51. package/src/services/discover.ts +1 -1
  52. package/src/services/mcp.ts +18 -3
  53. package/src/store/tool/slices/mcpStore/action.test.ts +141 -0
  54. package/src/store/tool/slices/mcpStore/action.ts +153 -11
@@ -0,0 +1,402 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import type { App } from '@/core/App';
4
+ import { FileSearchImpl } from '@/modules/fileSearch';
5
+ import type { FileResult, SearchOptions } from '@/types/fileSearch';
6
+
7
+ import FileSearchService from '../fileSearchSrv';
8
+
9
+ // Mock the fileSearch module
10
+ vi.mock('@/modules/fileSearch', () => {
11
+ const MockFileSearchImpl = vi.fn().mockImplementation(() => ({
12
+ search: vi.fn(),
13
+ checkSearchServiceStatus: vi.fn(),
14
+ updateSearchIndex: vi.fn(),
15
+ }));
16
+
17
+ return {
18
+ FileSearchImpl: vi.fn(),
19
+ createFileSearchModule: vi.fn(() => new MockFileSearchImpl()),
20
+ };
21
+ });
22
+
23
+ // Mock logger
24
+ vi.mock('@/utils/logger', () => ({
25
+ createLogger: () => ({
26
+ debug: vi.fn(),
27
+ info: vi.fn(),
28
+ warn: vi.fn(),
29
+ error: vi.fn(),
30
+ }),
31
+ }));
32
+
33
+ describe('FileSearchService', () => {
34
+ let fileSearchService: FileSearchService;
35
+ let mockApp: App;
36
+ let mockImpl: {
37
+ search: ReturnType<typeof vi.fn>;
38
+ checkSearchServiceStatus: ReturnType<typeof vi.fn>;
39
+ updateSearchIndex: ReturnType<typeof vi.fn>;
40
+ };
41
+
42
+ beforeEach(async () => {
43
+ vi.clearAllMocks();
44
+
45
+ // Setup mock app
46
+ mockApp = {} as unknown as App;
47
+
48
+ fileSearchService = new FileSearchService(mockApp);
49
+
50
+ // Get the mock implementation instance
51
+ mockImpl = (fileSearchService as any).impl;
52
+ });
53
+
54
+ describe('search', () => {
55
+ it('should perform search with query and default options', async () => {
56
+ const mockResults: FileResult[] = [
57
+ {
58
+ name: 'test.txt',
59
+ path: '/home/user/test.txt',
60
+ type: 'text/plain',
61
+ size: 1024,
62
+ isDirectory: false,
63
+ createdTime: new Date('2024-01-01'),
64
+ modifiedTime: new Date('2024-01-02'),
65
+ lastAccessTime: new Date('2024-01-03'),
66
+ },
67
+ ];
68
+
69
+ mockImpl.search.mockResolvedValue(mockResults);
70
+
71
+ const result = await fileSearchService.search('test');
72
+
73
+ expect(mockImpl.search).toHaveBeenCalledWith({ keywords: 'test' });
74
+ expect(result).toEqual(mockResults);
75
+ });
76
+
77
+ it('should perform search with query and custom options', async () => {
78
+ const mockResults: FileResult[] = [
79
+ {
80
+ name: 'document.pdf',
81
+ path: '/home/user/documents/document.pdf',
82
+ type: 'application/pdf',
83
+ size: 2048,
84
+ isDirectory: false,
85
+ createdTime: new Date('2024-02-01'),
86
+ modifiedTime: new Date('2024-02-02'),
87
+ lastAccessTime: new Date('2024-02-03'),
88
+ },
89
+ ];
90
+
91
+ const options: Omit<SearchOptions, 'keywords'> = {
92
+ limit: 10,
93
+ fileTypes: ['public.pdf'],
94
+ onlyIn: '/home/user/documents',
95
+ };
96
+
97
+ mockImpl.search.mockResolvedValue(mockResults);
98
+
99
+ const result = await fileSearchService.search('document', options);
100
+
101
+ expect(mockImpl.search).toHaveBeenCalledWith({
102
+ keywords: 'document',
103
+ limit: 10,
104
+ fileTypes: ['public.pdf'],
105
+ onlyIn: '/home/user/documents',
106
+ });
107
+ expect(result).toEqual(mockResults);
108
+ });
109
+
110
+ it('should perform search with date filters', async () => {
111
+ const mockResults: FileResult[] = [];
112
+ const createdAfter = new Date('2024-01-01');
113
+ const createdBefore = new Date('2024-12-31');
114
+
115
+ mockImpl.search.mockResolvedValue(mockResults);
116
+
117
+ await fileSearchService.search('test', {
118
+ createdAfter,
119
+ createdBefore,
120
+ });
121
+
122
+ expect(mockImpl.search).toHaveBeenCalledWith({
123
+ keywords: 'test',
124
+ createdAfter,
125
+ createdBefore,
126
+ });
127
+ });
128
+
129
+ it('should perform search with content filter', async () => {
130
+ const mockResults: FileResult[] = [];
131
+
132
+ mockImpl.search.mockResolvedValue(mockResults);
133
+
134
+ await fileSearchService.search('test', {
135
+ contentContains: 'specific text',
136
+ });
137
+
138
+ expect(mockImpl.search).toHaveBeenCalledWith({
139
+ keywords: 'test',
140
+ contentContains: 'specific text',
141
+ });
142
+ });
143
+
144
+ it('should perform search with sorting options', async () => {
145
+ const mockResults: FileResult[] = [];
146
+
147
+ mockImpl.search.mockResolvedValue(mockResults);
148
+
149
+ await fileSearchService.search('test', {
150
+ sortBy: 'date',
151
+ sortDirection: 'desc',
152
+ });
153
+
154
+ expect(mockImpl.search).toHaveBeenCalledWith({
155
+ keywords: 'test',
156
+ sortBy: 'date',
157
+ sortDirection: 'desc',
158
+ });
159
+ });
160
+
161
+ it('should perform search with exclude filter', async () => {
162
+ const mockResults: FileResult[] = [];
163
+
164
+ mockImpl.search.mockResolvedValue(mockResults);
165
+
166
+ await fileSearchService.search('test', {
167
+ exclude: ['/node_modules', '/dist'],
168
+ });
169
+
170
+ expect(mockImpl.search).toHaveBeenCalledWith({
171
+ keywords: 'test',
172
+ exclude: ['/node_modules', '/dist'],
173
+ });
174
+ });
175
+
176
+ it('should return empty array when no results found', async () => {
177
+ mockImpl.search.mockResolvedValue([]);
178
+
179
+ const result = await fileSearchService.search('nonexistent');
180
+
181
+ expect(result).toEqual([]);
182
+ });
183
+
184
+ it('should return results with metadata when detailed option is enabled', async () => {
185
+ const mockResults: FileResult[] = [
186
+ {
187
+ name: 'image.jpg',
188
+ path: '/home/user/images/image.jpg',
189
+ type: 'image/jpeg',
190
+ size: 4096,
191
+ isDirectory: false,
192
+ createdTime: new Date('2024-03-01'),
193
+ modifiedTime: new Date('2024-03-02'),
194
+ lastAccessTime: new Date('2024-03-03'),
195
+ metadata: {
196
+ width: 1920,
197
+ height: 1080,
198
+ orientation: 'landscape',
199
+ },
200
+ },
201
+ ];
202
+
203
+ mockImpl.search.mockResolvedValue(mockResults);
204
+
205
+ const result = await fileSearchService.search('image', { detailed: true });
206
+
207
+ expect(mockImpl.search).toHaveBeenCalledWith({
208
+ keywords: 'image',
209
+ detailed: true,
210
+ });
211
+ expect(result[0].metadata).toBeDefined();
212
+ expect(result[0].metadata?.width).toBe(1920);
213
+ });
214
+
215
+ it('should handle search errors gracefully', async () => {
216
+ mockImpl.search.mockRejectedValue(new Error('Search service unavailable'));
217
+
218
+ await expect(fileSearchService.search('test')).rejects.toThrow('Search service unavailable');
219
+ });
220
+
221
+ it('should perform search with all available options', async () => {
222
+ const mockResults: FileResult[] = [];
223
+ const allOptions: Omit<SearchOptions, 'keywords'> = {
224
+ limit: 50,
225
+ fileTypes: ['public.image', 'public.movie'],
226
+ onlyIn: '/home/user/media',
227
+ exclude: ['/home/user/media/temp'],
228
+ contentContains: 'vacation',
229
+ createdAfter: new Date('2024-01-01'),
230
+ createdBefore: new Date('2024-12-31'),
231
+ modifiedAfter: new Date('2024-06-01'),
232
+ modifiedBefore: new Date('2024-12-31'),
233
+ sortBy: 'size',
234
+ sortDirection: 'desc',
235
+ detailed: true,
236
+ liveUpdate: false,
237
+ };
238
+
239
+ mockImpl.search.mockResolvedValue(mockResults);
240
+
241
+ await fileSearchService.search('vacation photos', allOptions);
242
+
243
+ expect(mockImpl.search).toHaveBeenCalledWith({
244
+ keywords: 'vacation photos',
245
+ ...allOptions,
246
+ });
247
+ });
248
+ });
249
+
250
+ describe('checkSearchServiceStatus', () => {
251
+ it('should return true when search service is available', async () => {
252
+ mockImpl.checkSearchServiceStatus.mockResolvedValue(true);
253
+
254
+ const result = await fileSearchService.checkSearchServiceStatus();
255
+
256
+ expect(mockImpl.checkSearchServiceStatus).toHaveBeenCalled();
257
+ expect(result).toBe(true);
258
+ });
259
+
260
+ it('should return false when search service is unavailable', async () => {
261
+ mockImpl.checkSearchServiceStatus.mockResolvedValue(false);
262
+
263
+ const result = await fileSearchService.checkSearchServiceStatus();
264
+
265
+ expect(mockImpl.checkSearchServiceStatus).toHaveBeenCalled();
266
+ expect(result).toBe(false);
267
+ });
268
+
269
+ it('should handle status check errors', async () => {
270
+ mockImpl.checkSearchServiceStatus.mockRejectedValue(
271
+ new Error('Unable to check service status'),
272
+ );
273
+
274
+ await expect(fileSearchService.checkSearchServiceStatus()).rejects.toThrow(
275
+ 'Unable to check service status',
276
+ );
277
+ });
278
+ });
279
+
280
+ describe('updateSearchIndex', () => {
281
+ it('should update search index without path', async () => {
282
+ mockImpl.updateSearchIndex.mockResolvedValue(true);
283
+
284
+ const result = await fileSearchService.updateSearchIndex();
285
+
286
+ expect(mockImpl.updateSearchIndex).toHaveBeenCalledWith(undefined);
287
+ expect(result).toBe(true);
288
+ });
289
+
290
+ it('should update search index with specified path', async () => {
291
+ mockImpl.updateSearchIndex.mockResolvedValue(true);
292
+
293
+ const result = await fileSearchService.updateSearchIndex('/home/user/documents');
294
+
295
+ expect(mockImpl.updateSearchIndex).toHaveBeenCalledWith('/home/user/documents');
296
+ expect(result).toBe(true);
297
+ });
298
+
299
+ it('should return false when index update fails', async () => {
300
+ mockImpl.updateSearchIndex.mockResolvedValue(false);
301
+
302
+ const result = await fileSearchService.updateSearchIndex('/home/user/documents');
303
+
304
+ expect(result).toBe(false);
305
+ });
306
+
307
+ it('should handle index update errors', async () => {
308
+ mockImpl.updateSearchIndex.mockRejectedValue(new Error('Index update failed'));
309
+
310
+ await expect(fileSearchService.updateSearchIndex('/home/user/documents')).rejects.toThrow(
311
+ 'Index update failed',
312
+ );
313
+ });
314
+
315
+ it('should handle index update for multiple different paths', async () => {
316
+ mockImpl.updateSearchIndex.mockResolvedValue(true);
317
+
318
+ const paths = ['/home/user/documents', '/home/user/downloads', '/home/user/desktop'];
319
+
320
+ for (const path of paths) {
321
+ const result = await fileSearchService.updateSearchIndex(path);
322
+ expect(result).toBe(true);
323
+ }
324
+
325
+ expect(mockImpl.updateSearchIndex).toHaveBeenCalledTimes(paths.length);
326
+ });
327
+ });
328
+
329
+ describe('integration behavior', () => {
330
+ it('should maintain consistent state across multiple operations', async () => {
331
+ mockImpl.checkSearchServiceStatus.mockResolvedValue(true);
332
+ mockImpl.updateSearchIndex.mockResolvedValue(true);
333
+ mockImpl.search.mockResolvedValue([]);
334
+
335
+ const statusBefore = await fileSearchService.checkSearchServiceStatus();
336
+ expect(statusBefore).toBe(true);
337
+
338
+ await fileSearchService.updateSearchIndex('/home/user');
339
+
340
+ const statusAfter = await fileSearchService.checkSearchServiceStatus();
341
+ expect(statusAfter).toBe(true);
342
+
343
+ const results = await fileSearchService.search('test');
344
+ expect(results).toEqual([]);
345
+ });
346
+
347
+ it('should handle directory search results correctly', async () => {
348
+ const mockResults: FileResult[] = [
349
+ {
350
+ name: 'documents',
351
+ path: '/home/user/documents',
352
+ type: 'directory',
353
+ size: 0,
354
+ isDirectory: true,
355
+ createdTime: new Date('2024-01-01'),
356
+ modifiedTime: new Date('2024-01-02'),
357
+ lastAccessTime: new Date('2024-01-03'),
358
+ },
359
+ ];
360
+
361
+ mockImpl.search.mockResolvedValue(mockResults);
362
+
363
+ const result = await fileSearchService.search('documents');
364
+
365
+ expect(result[0].isDirectory).toBe(true);
366
+ expect(result[0].type).toBe('directory');
367
+ });
368
+
369
+ it('should handle mixed file and directory results', async () => {
370
+ const mockResults: FileResult[] = [
371
+ {
372
+ name: 'documents',
373
+ path: '/home/user/documents',
374
+ type: 'directory',
375
+ size: 0,
376
+ isDirectory: true,
377
+ createdTime: new Date('2024-01-01'),
378
+ modifiedTime: new Date('2024-01-02'),
379
+ lastAccessTime: new Date('2024-01-03'),
380
+ },
381
+ {
382
+ name: 'readme.txt',
383
+ path: '/home/user/documents/readme.txt',
384
+ type: 'text/plain',
385
+ size: 512,
386
+ isDirectory: false,
387
+ createdTime: new Date('2024-01-01'),
388
+ modifiedTime: new Date('2024-01-02'),
389
+ lastAccessTime: new Date('2024-01-03'),
390
+ },
391
+ ];
392
+
393
+ mockImpl.search.mockResolvedValue(mockResults);
394
+
395
+ const result = await fileSearchService.search('readme');
396
+
397
+ expect(result).toHaveLength(2);
398
+ expect(result[0].isDirectory).toBe(true);
399
+ expect(result[1].isDirectory).toBe(false);
400
+ });
401
+ });
402
+ });
@@ -0,0 +1,91 @@
1
+ import { mkdirSync, statSync } from 'node:fs';
2
+ import { afterEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { makeSureDirExist } from '../file-system';
5
+
6
+ vi.mock('node:fs', () => ({
7
+ mkdirSync: vi.fn(),
8
+ statSync: vi.fn(),
9
+ }));
10
+
11
+ describe('file-system', () => {
12
+ afterEach(() => {
13
+ vi.clearAllMocks();
14
+ });
15
+
16
+ describe('makeSureDirExist', () => {
17
+ it('should not create directory if it already exists', () => {
18
+ const dir = '/test/path';
19
+ vi.mocked(statSync).mockReturnValue({} as any);
20
+
21
+ makeSureDirExist(dir);
22
+
23
+ expect(statSync).toHaveBeenCalledWith(dir);
24
+ expect(mkdirSync).not.toHaveBeenCalled();
25
+ });
26
+
27
+ it('should create directory if it does not exist', () => {
28
+ const dir = '/test/new-path';
29
+ vi.mocked(statSync).mockImplementation(() => {
30
+ throw new Error('ENOENT: no such file or directory');
31
+ });
32
+
33
+ makeSureDirExist(dir);
34
+
35
+ expect(statSync).toHaveBeenCalledWith(dir);
36
+ expect(mkdirSync).toHaveBeenCalledWith(dir, { recursive: true });
37
+ });
38
+
39
+ it('should create directory recursively', () => {
40
+ const dir = '/test/deeply/nested/path';
41
+ vi.mocked(statSync).mockImplementation(() => {
42
+ throw new Error('ENOENT: no such file or directory');
43
+ });
44
+
45
+ makeSureDirExist(dir);
46
+
47
+ expect(mkdirSync).toHaveBeenCalledWith(dir, { recursive: true });
48
+ });
49
+
50
+ it('should throw error if mkdir fails due to permission issues', () => {
51
+ const dir = '/test/permission-denied';
52
+ vi.mocked(statSync).mockImplementation(() => {
53
+ throw new Error('ENOENT: no such file or directory');
54
+ });
55
+ vi.mocked(mkdirSync).mockImplementation(() => {
56
+ throw new Error('EACCES: permission denied');
57
+ });
58
+
59
+ expect(() => makeSureDirExist(dir)).toThrowError(
60
+ `Could not create target directory: ${dir}. Error: EACCES: permission denied`,
61
+ );
62
+ });
63
+
64
+ it('should throw error if mkdir fails with custom error message', () => {
65
+ const dir = '/test/custom-error';
66
+ const customError = new Error('Custom mkdir error');
67
+ vi.mocked(statSync).mockImplementation(() => {
68
+ throw new Error('ENOENT: no such file or directory');
69
+ });
70
+ vi.mocked(mkdirSync).mockImplementation(() => {
71
+ throw customError;
72
+ });
73
+
74
+ expect(() => makeSureDirExist(dir)).toThrowError(
75
+ `Could not create target directory: ${dir}. Error: Custom mkdir error`,
76
+ );
77
+ });
78
+
79
+ it('should handle empty directory path', () => {
80
+ const dir = '';
81
+ vi.mocked(statSync).mockImplementation(() => {
82
+ throw new Error('ENOENT: no such file or directory');
83
+ });
84
+ vi.mocked(mkdirSync).mockImplementation(() => {});
85
+
86
+ makeSureDirExist(dir);
87
+
88
+ expect(mkdirSync).toHaveBeenCalledWith('', { recursive: true });
89
+ });
90
+ });
91
+ });