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

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 (58) hide show
  1. package/CHANGELOG.md +50 -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 +18 -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/migrations/0054_better_auth_two_factor.sql +2 -0
  40. package/packages/database/src/core/migrations.json +1 -1
  41. package/packages/database/src/models/user.ts +27 -5
  42. package/packages/types/src/discover/mcp.ts +2 -1
  43. package/packages/types/src/tool/plugin.ts +2 -1
  44. package/scripts/migrateServerDB/errorHint.js +26 -0
  45. package/scripts/migrateServerDB/index.ts +5 -1
  46. package/src/app/[variants]/(main)/chat/settings/features/SmartAgentActionButton/MarketPublishButton.tsx +0 -2
  47. package/src/app/[variants]/(main)/discover/(detail)/mcp/features/Sidebar/ActionButton/index.tsx +33 -7
  48. package/src/features/PluginStore/McpList/List/Action.tsx +20 -1
  49. package/src/layout/AuthProvider/MarketAuth/MarketAuthConfirmModal.tsx +158 -0
  50. package/src/layout/AuthProvider/MarketAuth/MarketAuthProvider.tsx +130 -14
  51. package/src/libs/mcp/types.ts +8 -0
  52. package/src/locales/default/marketAuth.ts +13 -0
  53. package/src/server/routers/lambda/market/index.ts +85 -2
  54. package/src/server/services/discover/index.ts +45 -4
  55. package/src/services/discover.ts +1 -1
  56. package/src/services/mcp.ts +18 -3
  57. package/src/store/tool/slices/mcpStore/action.test.ts +141 -0
  58. package/src/store/tool/slices/mcpStore/action.ts +153 -11
@@ -0,0 +1,374 @@
1
+ /**
2
+ * @vitest-environment happy-dom
3
+ */
4
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
5
+
6
+ import { invoke } from './invoke';
7
+
8
+ // Mock dependencies
9
+ vi.mock('./invoke', () => ({
10
+ invoke: vi.fn(),
11
+ }));
12
+
13
+ vi.mock('~common/routes', () => ({
14
+ findMatchingRoute: vi.fn(),
15
+ }));
16
+
17
+ const { findMatchingRoute } = await import('~common/routes');
18
+ const { setupRouteInterceptors } = await import('./routeInterceptor');
19
+
20
+ describe('setupRouteInterceptors', () => {
21
+ let consoleLogSpy: ReturnType<typeof vi.spyOn>;
22
+ let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
23
+
24
+ beforeEach(() => {
25
+ vi.clearAllMocks();
26
+
27
+ // Mock console methods
28
+ consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
29
+ consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
30
+
31
+ // Setup happy-dom window and document
32
+ vi.stubGlobal('location', {
33
+ href: 'http://localhost:3000/chat',
34
+ origin: 'http://localhost:3000',
35
+ pathname: '/chat',
36
+ });
37
+
38
+ // Clear existing event listeners by resetting document
39
+ document.body.innerHTML = '';
40
+ });
41
+
42
+ describe('window.open interception', () => {
43
+ it('should intercept external URL and invoke openExternalLink', () => {
44
+ setupRouteInterceptors();
45
+
46
+ const externalUrl = 'https://google.com';
47
+ const result = window.open(externalUrl, '_blank');
48
+
49
+ expect(invoke).toHaveBeenCalledWith('openExternalLink', externalUrl);
50
+ expect(result).toBeNull();
51
+ });
52
+
53
+ it('should intercept URL object for external link', () => {
54
+ setupRouteInterceptors();
55
+
56
+ const externalUrl = new URL('https://github.com');
57
+ const result = window.open(externalUrl, '_blank');
58
+
59
+ expect(invoke).toHaveBeenCalledWith('openExternalLink', 'https://github.com/');
60
+ expect(result).toBeNull();
61
+ });
62
+
63
+ it('should allow internal link to proceed with original window.open', () => {
64
+ setupRouteInterceptors();
65
+
66
+ const originalWindowOpen = window.open;
67
+ const internalUrl = 'http://localhost:3000/settings';
68
+
69
+ // We can't fully test the original behavior in happy-dom, but we can verify invoke is not called
70
+ window.open(internalUrl);
71
+
72
+ expect(invoke).not.toHaveBeenCalledWith('openExternalLink', expect.anything());
73
+ });
74
+
75
+ it('should handle relative URL that resolves as internal link', () => {
76
+ setupRouteInterceptors();
77
+
78
+ // In happy-dom, 'invalid-url' is resolved relative to window.location.href
79
+ // So it becomes 'http://localhost:3000/invalid-url' which is internal
80
+ const relativeUrl = 'invalid-url';
81
+ window.open(relativeUrl);
82
+
83
+ // Since it's internal, it won't call invoke for external link
84
+ expect(invoke).not.toHaveBeenCalledWith('openExternalLink', expect.anything());
85
+ });
86
+ });
87
+
88
+ describe('link click interception', () => {
89
+ it('should intercept external link clicks', async () => {
90
+ setupRouteInterceptors();
91
+
92
+ const link = document.createElement('a');
93
+ link.href = 'https://example.com';
94
+ document.body.append(link);
95
+
96
+ const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true });
97
+ const preventDefaultSpy = vi.spyOn(clickEvent, 'preventDefault');
98
+ const stopPropagationSpy = vi.spyOn(clickEvent, 'stopPropagation');
99
+
100
+ link.dispatchEvent(clickEvent);
101
+
102
+ // Wait for async handling
103
+ await new Promise((resolve) => setTimeout(resolve, 0));
104
+
105
+ expect(invoke).toHaveBeenCalledWith('openExternalLink', 'https://example.com/');
106
+ expect(preventDefaultSpy).toHaveBeenCalled();
107
+ expect(stopPropagationSpy).toHaveBeenCalled();
108
+ });
109
+
110
+ it('should intercept internal link matching route pattern', async () => {
111
+ setupRouteInterceptors();
112
+
113
+ const matchedRoute = { pathPrefix: '/desktop/devtools', targetWindow: 'devtools' };
114
+ vi.mocked(findMatchingRoute).mockReturnValue(matchedRoute);
115
+
116
+ const link = document.createElement('a');
117
+ link.href = 'http://localhost:3000/desktop/devtools';
118
+ document.body.append(link);
119
+
120
+ const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true });
121
+ const preventDefaultSpy = vi.spyOn(clickEvent, 'preventDefault');
122
+
123
+ link.dispatchEvent(clickEvent);
124
+ await new Promise((resolve) => setTimeout(resolve, 0));
125
+
126
+ expect(findMatchingRoute).toHaveBeenCalledWith('/desktop/devtools');
127
+ expect(invoke).toHaveBeenCalledWith('interceptRoute', {
128
+ path: '/desktop/devtools',
129
+ source: 'link-click',
130
+ url: 'http://localhost:3000/desktop/devtools',
131
+ });
132
+ expect(preventDefaultSpy).toHaveBeenCalled();
133
+ });
134
+
135
+ it('should not intercept if already on target page', async () => {
136
+ setupRouteInterceptors();
137
+
138
+ // Set current location to be in the target page
139
+ vi.stubGlobal('location', {
140
+ href: 'http://localhost:3000/desktop/devtools/console',
141
+ origin: 'http://localhost:3000',
142
+ pathname: '/desktop/devtools/console',
143
+ });
144
+
145
+ const matchedRoute = { pathPrefix: '/desktop/devtools', targetWindow: 'devtools' };
146
+ vi.mocked(findMatchingRoute).mockReturnValue(matchedRoute);
147
+
148
+ const link = document.createElement('a');
149
+ link.href = 'http://localhost:3000/desktop/devtools/network';
150
+ document.body.append(link);
151
+
152
+ const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true });
153
+ const preventDefaultSpy = vi.spyOn(clickEvent, 'preventDefault');
154
+
155
+ link.dispatchEvent(clickEvent);
156
+ await new Promise((resolve) => setTimeout(resolve, 0));
157
+
158
+ expect(preventDefaultSpy).not.toHaveBeenCalled();
159
+ expect(invoke).not.toHaveBeenCalledWith('interceptRoute', expect.anything());
160
+ });
161
+
162
+ it('should handle non-HTTP link protocols as external links', async () => {
163
+ setupRouteInterceptors();
164
+
165
+ const link = document.createElement('a');
166
+ link.href = 'mailto:test@example.com';
167
+ document.body.append(link);
168
+
169
+ const clickEvent = new MouseEvent('click', { bubbles: true, cancelable: true });
170
+ const preventDefaultSpy = vi.spyOn(clickEvent, 'preventDefault');
171
+
172
+ link.dispatchEvent(clickEvent);
173
+
174
+ await new Promise((resolve) => setTimeout(resolve, 0));
175
+
176
+ // mailto: links are treated as external links by the URL constructor
177
+ expect(invoke).toHaveBeenCalledWith('openExternalLink', 'mailto:test@example.com');
178
+ expect(preventDefaultSpy).toHaveBeenCalled();
179
+ });
180
+ });
181
+
182
+ describe('history.pushState interception', () => {
183
+ it('should intercept pushState for matched routes', () => {
184
+ setupRouteInterceptors();
185
+
186
+ const matchedRoute = { pathPrefix: '/desktop/devtools', targetWindow: 'devtools' };
187
+ vi.mocked(findMatchingRoute).mockReturnValue(matchedRoute);
188
+
189
+ const originalLength = history.length;
190
+ history.pushState({}, '', '/desktop/devtools');
191
+
192
+ expect(findMatchingRoute).toHaveBeenCalledWith('/desktop/devtools');
193
+ expect(invoke).toHaveBeenCalledWith('interceptRoute', {
194
+ path: '/desktop/devtools',
195
+ source: 'push-state',
196
+ url: 'http://localhost:3000/desktop/devtools',
197
+ });
198
+ // Ensure navigation was prevented
199
+ expect(history.length).toBe(originalLength);
200
+ });
201
+
202
+ it('should not intercept if already on target page', () => {
203
+ setupRouteInterceptors();
204
+
205
+ vi.stubGlobal('location', {
206
+ href: 'http://localhost:3000/desktop/devtools/console',
207
+ origin: 'http://localhost:3000',
208
+ pathname: '/desktop/devtools/console',
209
+ });
210
+
211
+ const matchedRoute = { pathPrefix: '/desktop/devtools', targetWindow: 'devtools' };
212
+ vi.mocked(findMatchingRoute).mockReturnValue(matchedRoute);
213
+
214
+ history.pushState({}, '', '/desktop/devtools/network');
215
+
216
+ expect(consoleLogSpy).toHaveBeenCalledWith(
217
+ expect.stringContaining('Skip pushState interception'),
218
+ );
219
+ });
220
+
221
+ it('should allow pushState for non-matched routes', () => {
222
+ setupRouteInterceptors();
223
+
224
+ vi.mocked(findMatchingRoute).mockReturnValue(undefined);
225
+
226
+ history.pushState({}, '', '/chat/new');
227
+
228
+ expect(invoke).not.toHaveBeenCalledWith('interceptRoute', expect.anything());
229
+ });
230
+
231
+ it('should handle pushState errors gracefully', () => {
232
+ setupRouteInterceptors();
233
+
234
+ vi.mocked(findMatchingRoute).mockImplementation(() => {
235
+ throw new Error('Route matching error');
236
+ });
237
+
238
+ history.pushState({}, '', '/some/path');
239
+
240
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
241
+ expect.stringContaining('pushState interception error'),
242
+ expect.any(Error),
243
+ );
244
+ });
245
+ });
246
+
247
+ describe('history.replaceState interception', () => {
248
+ it('should intercept replaceState for matched routes', () => {
249
+ setupRouteInterceptors();
250
+
251
+ const matchedRoute = { pathPrefix: '/desktop/devtools', targetWindow: 'devtools' };
252
+ vi.mocked(findMatchingRoute).mockReturnValue(matchedRoute);
253
+
254
+ history.replaceState({}, '', '/desktop/devtools');
255
+
256
+ expect(findMatchingRoute).toHaveBeenCalledWith('/desktop/devtools');
257
+ expect(invoke).toHaveBeenCalledWith('interceptRoute', {
258
+ path: '/desktop/devtools',
259
+ source: 'replace-state',
260
+ url: 'http://localhost:3000/desktop/devtools',
261
+ });
262
+ });
263
+
264
+ it('should not intercept if already on target page', () => {
265
+ setupRouteInterceptors();
266
+
267
+ vi.stubGlobal('location', {
268
+ href: 'http://localhost:3000/desktop/devtools/console',
269
+ origin: 'http://localhost:3000',
270
+ pathname: '/desktop/devtools/console',
271
+ });
272
+
273
+ const matchedRoute = { pathPrefix: '/desktop/devtools', targetWindow: 'devtools' };
274
+ vi.mocked(findMatchingRoute).mockReturnValue(matchedRoute);
275
+
276
+ history.replaceState({}, '', '/desktop/devtools/network');
277
+
278
+ expect(consoleLogSpy).toHaveBeenCalledWith(
279
+ expect.stringContaining('Skip replaceState interception'),
280
+ );
281
+ });
282
+
283
+ it('should allow replaceState for non-matched routes', () => {
284
+ setupRouteInterceptors();
285
+
286
+ vi.mocked(findMatchingRoute).mockReturnValue(undefined);
287
+
288
+ history.replaceState({}, '', '/chat/session-123');
289
+
290
+ expect(invoke).not.toHaveBeenCalledWith('interceptRoute', expect.anything());
291
+ });
292
+ });
293
+
294
+ describe('error event interception', () => {
295
+ it('should prevent navigation errors for prevented paths', () => {
296
+ setupRouteInterceptors();
297
+
298
+ // First trigger a route interception to add path to preventedPaths
299
+ const matchedRoute = { pathPrefix: '/desktop/devtools', targetWindow: 'devtools' };
300
+ vi.mocked(findMatchingRoute).mockReturnValue(matchedRoute);
301
+ history.pushState({}, '', '/desktop/devtools');
302
+
303
+ // Now trigger an error event with navigation in the message
304
+ const errorEvent = new ErrorEvent('error', {
305
+ bubbles: true,
306
+ cancelable: true,
307
+ message: 'navigation error occurred',
308
+ });
309
+ const preventDefaultSpy = vi.spyOn(errorEvent, 'preventDefault');
310
+
311
+ window.dispatchEvent(errorEvent);
312
+
313
+ expect(preventDefaultSpy).toHaveBeenCalled();
314
+ expect(consoleLogSpy).toHaveBeenCalledWith(
315
+ expect.stringContaining('Captured possible routing error'),
316
+ );
317
+ });
318
+
319
+ it('should not prevent non-navigation errors', () => {
320
+ setupRouteInterceptors();
321
+
322
+ const errorEvent = new ErrorEvent('error', {
323
+ bubbles: true,
324
+ cancelable: true,
325
+ message: 'some other error',
326
+ });
327
+ const preventDefaultSpy = vi.spyOn(errorEvent, 'preventDefault');
328
+
329
+ window.dispatchEvent(errorEvent);
330
+
331
+ expect(preventDefaultSpy).not.toHaveBeenCalled();
332
+ });
333
+ });
334
+
335
+ describe('interceptRoute helper', () => {
336
+ it('should handle successful route interception', async () => {
337
+ vi.mocked(invoke).mockResolvedValue(undefined);
338
+
339
+ setupRouteInterceptors();
340
+
341
+ const matchedRoute = { pathPrefix: '/desktop/devtools', targetWindow: 'devtools' };
342
+ vi.mocked(findMatchingRoute).mockReturnValue(matchedRoute);
343
+
344
+ history.pushState({}, '', '/desktop/devtools');
345
+
346
+ await new Promise((resolve) => setTimeout(resolve, 0));
347
+
348
+ expect(invoke).toHaveBeenCalledWith('interceptRoute', {
349
+ path: '/desktop/devtools',
350
+ source: 'push-state',
351
+ url: 'http://localhost:3000/desktop/devtools',
352
+ });
353
+ });
354
+
355
+ it('should handle route interception errors gracefully', async () => {
356
+ const error = new Error('IPC communication failed');
357
+ vi.mocked(invoke).mockRejectedValue(error);
358
+
359
+ setupRouteInterceptors();
360
+
361
+ const matchedRoute = { pathPrefix: '/desktop/devtools', targetWindow: 'devtools' };
362
+ vi.mocked(findMatchingRoute).mockReturnValue(matchedRoute);
363
+
364
+ history.pushState({}, '', '/desktop/devtools');
365
+
366
+ await new Promise((resolve) => setTimeout(resolve, 0));
367
+
368
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
369
+ expect.stringContaining('Route interception (push-state) call failed'),
370
+ error,
371
+ );
372
+ });
373
+ });
374
+ });