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

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 (24) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/Dockerfile +2 -0
  3. package/apps/desktop/src/main/controllers/__tests__/McpInstallCtr.test.ts +286 -0
  4. package/apps/desktop/src/main/controllers/__tests__/NotificationCtr.test.ts +347 -0
  5. package/apps/desktop/src/main/controllers/__tests__/RemoteServerConfigCtr.test.ts +645 -0
  6. package/apps/desktop/src/main/controllers/__tests__/RemoteServerSyncCtr.test.ts +372 -0
  7. package/apps/desktop/src/main/controllers/__tests__/SystemCtr.test.ts +276 -0
  8. package/apps/desktop/src/main/controllers/__tests__/UploadFileCtr.test.ts +171 -0
  9. package/apps/desktop/src/main/core/browser/__tests__/Browser.test.ts +573 -0
  10. package/apps/desktop/src/main/core/browser/__tests__/BrowserManager.test.ts +415 -0
  11. package/apps/desktop/src/main/core/infrastructure/__tests__/I18nManager.test.ts +353 -0
  12. package/apps/desktop/src/main/core/infrastructure/__tests__/IoCContainer.test.ts +156 -0
  13. package/apps/desktop/src/main/core/infrastructure/__tests__/ProtocolManager.test.ts +348 -0
  14. package/apps/desktop/src/main/core/infrastructure/__tests__/StaticFileServerManager.test.ts +481 -0
  15. package/apps/desktop/src/main/core/infrastructure/__tests__/StoreManager.test.ts +164 -0
  16. package/apps/desktop/src/main/core/infrastructure/__tests__/UpdaterManager.test.ts +513 -0
  17. package/changelog/v1.json +9 -0
  18. package/docs/self-hosting/environment-variables/model-provider.mdx +31 -0
  19. package/docs/self-hosting/environment-variables/model-provider.zh-CN.mdx +30 -0
  20. package/package.json +1 -1
  21. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +6 -3
  22. package/src/config/modelProviders/vertexai.ts +1 -1
  23. package/src/envs/llm.ts +4 -0
  24. package/src/server/modules/ModelRuntime/index.ts +4 -4
@@ -0,0 +1,353 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import type { App as AppCore } from '../../App';
4
+ import { I18nManager } from '../I18nManager';
5
+
6
+ // Use vi.hoisted to define mocks before hoisting
7
+ const { mockApp, mockI18nextInstance, mockLoadResources, mockCreateInstance } = vi.hoisted(() => {
8
+ const mockI18nextInstance = {
9
+ addResourceBundle: vi.fn(),
10
+ changeLanguage: vi.fn().mockResolvedValue(undefined),
11
+ init: vi.fn().mockResolvedValue(undefined),
12
+ language: 'en-US',
13
+ on: vi.fn(),
14
+ t: vi.fn().mockImplementation((key: string) => key),
15
+ };
16
+
17
+ const mockCreateInstance = vi.fn().mockReturnValue(mockI18nextInstance);
18
+
19
+ return {
20
+ mockApp: {
21
+ getLocale: vi.fn().mockReturnValue('en-US'),
22
+ },
23
+ mockCreateInstance,
24
+ mockI18nextInstance,
25
+ mockLoadResources: vi.fn().mockResolvedValue({ key: 'value' }),
26
+ };
27
+ });
28
+
29
+ // Mock electron app
30
+ vi.mock('electron', () => ({
31
+ app: mockApp,
32
+ }));
33
+
34
+ // Mock i18next
35
+ vi.mock('i18next', () => ({
36
+ default: {
37
+ createInstance: mockCreateInstance,
38
+ },
39
+ }));
40
+
41
+ // Mock logger
42
+ vi.mock('@/utils/logger', () => ({
43
+ createLogger: () => ({
44
+ debug: vi.fn(),
45
+ error: vi.fn(),
46
+ info: vi.fn(),
47
+ warn: vi.fn(),
48
+ }),
49
+ }));
50
+
51
+ // Mock loadResources
52
+ vi.mock('@/locales/resources', () => ({
53
+ loadResources: mockLoadResources,
54
+ }));
55
+
56
+ describe('I18nManager', () => {
57
+ let manager: I18nManager;
58
+ let mockAppCore: AppCore;
59
+ let mockStoreManagerGet: ReturnType<typeof vi.fn>;
60
+ let mockRefreshMenus: ReturnType<typeof vi.fn>;
61
+
62
+ beforeEach(() => {
63
+ vi.clearAllMocks();
64
+
65
+ // Reset i18next mock state
66
+ mockI18nextInstance.language = 'en-US';
67
+ mockI18nextInstance.t.mockImplementation((key: string) => key);
68
+ mockI18nextInstance.init.mockResolvedValue(undefined);
69
+ mockI18nextInstance.changeLanguage.mockResolvedValue(undefined);
70
+
71
+ // Reset loadResources mock
72
+ mockLoadResources.mockResolvedValue({ key: 'value' });
73
+
74
+ // Reset electron app mock
75
+ mockApp.getLocale.mockReturnValue('en-US');
76
+
77
+ // Create mock App core
78
+ mockStoreManagerGet = vi.fn().mockReturnValue('auto');
79
+ mockRefreshMenus = vi.fn();
80
+
81
+ mockAppCore = {
82
+ menuManager: {
83
+ refreshMenus: mockRefreshMenus,
84
+ },
85
+ storeManager: {
86
+ get: mockStoreManagerGet,
87
+ },
88
+ } as unknown as AppCore;
89
+
90
+ manager = new I18nManager(mockAppCore);
91
+ });
92
+
93
+ describe('constructor', () => {
94
+ it('should create i18next instance', () => {
95
+ expect(mockCreateInstance).toHaveBeenCalled();
96
+ });
97
+ });
98
+
99
+ describe('init', () => {
100
+ it('should initialize i18next with default settings', async () => {
101
+ await manager.init();
102
+
103
+ expect(mockI18nextInstance.init).toHaveBeenCalledWith({
104
+ defaultNS: 'menu',
105
+ fallbackLng: 'en-US',
106
+ initAsync: true,
107
+ interpolation: {
108
+ escapeValue: false,
109
+ },
110
+ lng: 'en-US',
111
+ ns: ['menu', 'dialog', 'common'],
112
+ partialBundledLanguages: true,
113
+ });
114
+ });
115
+
116
+ it('should use provided language parameter', async () => {
117
+ await manager.init('zh-CN');
118
+
119
+ expect(mockI18nextInstance.init).toHaveBeenCalledWith(
120
+ expect.objectContaining({
121
+ lng: 'zh-CN',
122
+ }),
123
+ );
124
+ });
125
+
126
+ it('should use stored locale when not auto', async () => {
127
+ mockStoreManagerGet.mockReturnValue('ja-JP');
128
+
129
+ await manager.init();
130
+
131
+ expect(mockI18nextInstance.init).toHaveBeenCalledWith(
132
+ expect.objectContaining({
133
+ lng: 'ja-JP',
134
+ }),
135
+ );
136
+ });
137
+
138
+ it('should use system locale when stored locale is auto', async () => {
139
+ mockStoreManagerGet.mockReturnValue('auto');
140
+ mockApp.getLocale.mockReturnValue('fr-FR');
141
+
142
+ await manager.init();
143
+
144
+ expect(mockI18nextInstance.init).toHaveBeenCalledWith(
145
+ expect.objectContaining({
146
+ lng: 'fr-FR',
147
+ }),
148
+ );
149
+ });
150
+
151
+ it('should skip initialization if already initialized', async () => {
152
+ await manager.init();
153
+ vi.clearAllMocks();
154
+
155
+ await manager.init();
156
+
157
+ expect(mockI18nextInstance.init).not.toHaveBeenCalled();
158
+ });
159
+
160
+ it('should load locale resources after init', async () => {
161
+ await manager.init();
162
+
163
+ // Should load menu, dialog, common namespaces
164
+ expect(mockLoadResources).toHaveBeenCalledWith('en-US', 'menu');
165
+ expect(mockLoadResources).toHaveBeenCalledWith('en-US', 'dialog');
166
+ expect(mockLoadResources).toHaveBeenCalledWith('en-US', 'common');
167
+ });
168
+
169
+ it('should refresh main UI after init', async () => {
170
+ await manager.init();
171
+
172
+ expect(mockRefreshMenus).toHaveBeenCalled();
173
+ });
174
+
175
+ it('should register languageChanged listener', async () => {
176
+ await manager.init();
177
+
178
+ expect(mockI18nextInstance.on).toHaveBeenCalledWith('languageChanged', expect.any(Function));
179
+ });
180
+ });
181
+
182
+ describe('t', () => {
183
+ beforeEach(async () => {
184
+ await manager.init();
185
+ });
186
+
187
+ it('should call i18next t function', () => {
188
+ mockI18nextInstance.t.mockReturnValue('translated');
189
+
190
+ const result = manager.t('test.key');
191
+
192
+ expect(mockI18nextInstance.t).toHaveBeenCalledWith('test.key', undefined);
193
+ expect(result).toBe('translated');
194
+ });
195
+
196
+ it('should pass options to i18next', () => {
197
+ mockI18nextInstance.t.mockReturnValue('translated with options');
198
+
199
+ const result = manager.t('test.key', { count: 5 });
200
+
201
+ expect(mockI18nextInstance.t).toHaveBeenCalledWith('test.key', { count: 5 });
202
+ expect(result).toBe('translated with options');
203
+ });
204
+
205
+ it('should warn when translation key is not found', () => {
206
+ // When translation is not found, i18next returns the key itself
207
+ mockI18nextInstance.t.mockImplementation((key: string) => key);
208
+
209
+ manager.t('missing.key');
210
+
211
+ // The warn should be logged (we can't verify the log content with our mock setup)
212
+ expect(mockI18nextInstance.t).toHaveBeenCalledWith('missing.key', undefined);
213
+ });
214
+ });
215
+
216
+ describe('createNamespacedT', () => {
217
+ beforeEach(async () => {
218
+ await manager.init();
219
+ });
220
+
221
+ it('should return a function that adds namespace to options', () => {
222
+ mockI18nextInstance.t.mockReturnValue('namespaced translation');
223
+
224
+ const menuT = manager.createNamespacedT('menu');
225
+ const result = menuT('test.key');
226
+
227
+ expect(mockI18nextInstance.t).toHaveBeenCalledWith('test.key', { ns: 'menu' });
228
+ expect(result).toBe('namespaced translation');
229
+ });
230
+
231
+ it('should merge provided options with namespace', () => {
232
+ mockI18nextInstance.t.mockReturnValue('merged translation');
233
+
234
+ const menuT = manager.createNamespacedT('dialog');
235
+ const result = menuT('test.key', { count: 3 });
236
+
237
+ expect(mockI18nextInstance.t).toHaveBeenCalledWith('test.key', { count: 3, ns: 'dialog' });
238
+ expect(result).toBe('merged translation');
239
+ });
240
+ });
241
+
242
+ describe('ns', () => {
243
+ beforeEach(async () => {
244
+ await manager.init();
245
+ });
246
+
247
+ it('should be an alias for createNamespacedT', () => {
248
+ mockI18nextInstance.t.mockReturnValue('ns translation');
249
+
250
+ const dialogT = manager.ns('dialog');
251
+ const result = dialogT('test.key');
252
+
253
+ expect(mockI18nextInstance.t).toHaveBeenCalledWith('test.key', { ns: 'dialog' });
254
+ expect(result).toBe('ns translation');
255
+ });
256
+ });
257
+
258
+ describe('getCurrentLanguage', () => {
259
+ beforeEach(async () => {
260
+ await manager.init();
261
+ });
262
+
263
+ it('should return current i18next language', () => {
264
+ mockI18nextInstance.language = 'de-DE';
265
+
266
+ expect(manager.getCurrentLanguage()).toBe('de-DE');
267
+ });
268
+ });
269
+
270
+ describe('changeLanguage', () => {
271
+ beforeEach(async () => {
272
+ await manager.init();
273
+ });
274
+
275
+ it('should call i18next changeLanguage', async () => {
276
+ await manager.changeLanguage('zh-CN');
277
+
278
+ expect(mockI18nextInstance.changeLanguage).toHaveBeenCalledWith('zh-CN');
279
+ });
280
+
281
+ it('should initialize if not already initialized', async () => {
282
+ // Create a new manager that is not initialized
283
+ const uninitializedManager = new I18nManager(mockAppCore);
284
+
285
+ await uninitializedManager.changeLanguage('zh-CN');
286
+
287
+ expect(mockI18nextInstance.init).toHaveBeenCalled();
288
+ expect(mockI18nextInstance.changeLanguage).toHaveBeenCalledWith('zh-CN');
289
+ });
290
+ });
291
+
292
+ describe('handleLanguageChanged', () => {
293
+ beforeEach(async () => {
294
+ await manager.init();
295
+ });
296
+
297
+ it('should load locale and refresh UI on language change', async () => {
298
+ // Get the languageChanged handler
299
+ const languageChangedHandler = mockI18nextInstance.on.mock.calls.find(
300
+ (call) => call[0] === 'languageChanged',
301
+ )?.[1];
302
+
303
+ expect(languageChangedHandler).toBeDefined();
304
+
305
+ // Clear mocks to check only the handler's behavior
306
+ mockLoadResources.mockClear();
307
+ mockRefreshMenus.mockClear();
308
+
309
+ // Trigger language change
310
+ await languageChangedHandler('ja-JP');
311
+
312
+ // Should load resources for new language
313
+ expect(mockLoadResources).toHaveBeenCalledWith('ja-JP', 'menu');
314
+ expect(mockLoadResources).toHaveBeenCalledWith('ja-JP', 'dialog');
315
+ expect(mockLoadResources).toHaveBeenCalledWith('ja-JP', 'common');
316
+
317
+ // Should refresh menus
318
+ expect(mockRefreshMenus).toHaveBeenCalled();
319
+ });
320
+ });
321
+
322
+ describe('loadNamespace', () => {
323
+ beforeEach(async () => {
324
+ await manager.init();
325
+ vi.clearAllMocks();
326
+ });
327
+
328
+ it('should load resources and add to i18next', async () => {
329
+ mockLoadResources.mockResolvedValue({ hello: 'world' });
330
+
331
+ // Access private method
332
+ const result = await manager['loadNamespace']('en-US', 'menu');
333
+
334
+ expect(mockLoadResources).toHaveBeenCalledWith('en-US', 'menu');
335
+ expect(mockI18nextInstance.addResourceBundle).toHaveBeenCalledWith(
336
+ 'en-US',
337
+ 'menu',
338
+ { hello: 'world' },
339
+ true,
340
+ true,
341
+ );
342
+ expect(result).toBe(true);
343
+ });
344
+
345
+ it('should return false on error', async () => {
346
+ mockLoadResources.mockRejectedValue(new Error('Load failed'));
347
+
348
+ const result = await manager['loadNamespace']('en-US', 'menu');
349
+
350
+ expect(result).toBe(false);
351
+ });
352
+ });
353
+ });
@@ -0,0 +1,156 @@
1
+ import { beforeEach, describe, expect, it } from 'vitest';
2
+
3
+ import { IoCContainer } from '../IoCContainer';
4
+
5
+ describe('IoCContainer', () => {
6
+ // Sample class targets for testing WeakMap storage
7
+ class TestController {}
8
+ class AnotherController {}
9
+
10
+ beforeEach(() => {
11
+ // Reset static WeakMaps by creating new instances
12
+ // WeakMaps can't be cleared, but we can verify they work correctly
13
+ // For each test, use fresh class instances
14
+ });
15
+
16
+ describe('controllers WeakMap', () => {
17
+ it('should store controller metadata', () => {
18
+ const metadata = [{ methodName: 'handleMessage', mode: 'client' as const, name: 'message' }];
19
+
20
+ IoCContainer.controllers.set(TestController, metadata);
21
+
22
+ expect(IoCContainer.controllers.get(TestController)).toEqual(metadata);
23
+ });
24
+
25
+ it('should allow multiple controllers', () => {
26
+ const metadata1 = [{ methodName: 'method1', mode: 'client' as const, name: 'action1' }];
27
+ const metadata2 = [{ methodName: 'method2', mode: 'server' as const, name: 'action2' }];
28
+
29
+ IoCContainer.controllers.set(TestController, metadata1);
30
+ IoCContainer.controllers.set(AnotherController, metadata2);
31
+
32
+ expect(IoCContainer.controllers.get(TestController)).toEqual(metadata1);
33
+ expect(IoCContainer.controllers.get(AnotherController)).toEqual(metadata2);
34
+ });
35
+
36
+ it('should allow overwriting controller metadata', () => {
37
+ const oldMetadata = [{ methodName: 'oldMethod', mode: 'client' as const, name: 'old' }];
38
+ const newMetadata = [{ methodName: 'newMethod', mode: 'server' as const, name: 'new' }];
39
+
40
+ IoCContainer.controllers.set(TestController, oldMetadata);
41
+ IoCContainer.controllers.set(TestController, newMetadata);
42
+
43
+ expect(IoCContainer.controllers.get(TestController)).toEqual(newMetadata);
44
+ });
45
+
46
+ it('should support multiple methods per controller', () => {
47
+ const metadata = [
48
+ { methodName: 'method1', mode: 'client' as const, name: 'action1' },
49
+ { methodName: 'method2', mode: 'server' as const, name: 'action2' },
50
+ { methodName: 'method3', mode: 'client' as const, name: 'action3' },
51
+ ];
52
+
53
+ IoCContainer.controllers.set(TestController, metadata);
54
+
55
+ const stored = IoCContainer.controllers.get(TestController);
56
+ expect(stored).toHaveLength(3);
57
+ expect(stored?.[0].mode).toBe('client');
58
+ expect(stored?.[1].mode).toBe('server');
59
+ });
60
+ });
61
+
62
+ describe('shortcuts WeakMap', () => {
63
+ it('should store shortcut metadata', () => {
64
+ const metadata = [{ methodName: 'toggleDarkMode', name: 'CmdOrCtrl+Shift+D' }];
65
+
66
+ IoCContainer.shortcuts.set(TestController, metadata);
67
+
68
+ expect(IoCContainer.shortcuts.get(TestController)).toEqual(metadata);
69
+ });
70
+
71
+ it('should allow multiple shortcuts per class', () => {
72
+ const metadata = [
73
+ { methodName: 'toggleDarkMode', name: 'CmdOrCtrl+Shift+D' },
74
+ { methodName: 'openSettings', name: 'CmdOrCtrl+,' },
75
+ { methodName: 'newChat', name: 'CmdOrCtrl+N' },
76
+ ];
77
+
78
+ IoCContainer.shortcuts.set(TestController, metadata);
79
+
80
+ const stored = IoCContainer.shortcuts.get(TestController);
81
+ expect(stored).toHaveLength(3);
82
+ });
83
+
84
+ it('should return undefined for unregistered class', () => {
85
+ class UnregisteredClass {}
86
+
87
+ expect(IoCContainer.shortcuts.get(UnregisteredClass)).toBeUndefined();
88
+ });
89
+ });
90
+
91
+ describe('protocolHandlers WeakMap', () => {
92
+ it('should store protocol handler metadata', () => {
93
+ const metadata = [{ action: 'install', methodName: 'handleInstall', urlType: 'plugin' }];
94
+
95
+ IoCContainer.protocolHandlers.set(TestController, metadata);
96
+
97
+ expect(IoCContainer.protocolHandlers.get(TestController)).toEqual(metadata);
98
+ });
99
+
100
+ it('should support multiple protocol handlers', () => {
101
+ const metadata = [
102
+ { action: 'install', methodName: 'handleInstall', urlType: 'plugin' },
103
+ { action: 'uninstall', methodName: 'handleUninstall', urlType: 'plugin' },
104
+ { action: 'open', methodName: 'handleOpen', urlType: 'chat' },
105
+ ];
106
+
107
+ IoCContainer.protocolHandlers.set(TestController, metadata);
108
+
109
+ const stored = IoCContainer.protocolHandlers.get(TestController);
110
+ expect(stored).toHaveLength(3);
111
+ expect(stored?.map((h) => h.urlType)).toContain('plugin');
112
+ expect(stored?.map((h) => h.urlType)).toContain('chat');
113
+ });
114
+
115
+ it('should allow different classes to have different handlers', () => {
116
+ const metadata1 = [{ action: 'install', methodName: 'handleInstall', urlType: 'plugin' }];
117
+ const metadata2 = [{ action: 'open', methodName: 'handleOpen', urlType: 'chat' }];
118
+
119
+ IoCContainer.protocolHandlers.set(TestController, metadata1);
120
+ IoCContainer.protocolHandlers.set(AnotherController, metadata2);
121
+
122
+ expect(IoCContainer.protocolHandlers.get(TestController)?.[0].urlType).toBe('plugin');
123
+ expect(IoCContainer.protocolHandlers.get(AnotherController)?.[0].urlType).toBe('chat');
124
+ });
125
+ });
126
+
127
+ describe('init', () => {
128
+ it('should be callable without error', () => {
129
+ const container = new IoCContainer();
130
+
131
+ expect(() => container.init()).not.toThrow();
132
+ });
133
+
134
+ it('should return undefined', () => {
135
+ const container = new IoCContainer();
136
+
137
+ const result = container.init();
138
+
139
+ expect(result).toBeUndefined();
140
+ });
141
+ });
142
+
143
+ describe('static properties', () => {
144
+ it('should have controllers as a WeakMap', () => {
145
+ expect(IoCContainer.controllers).toBeInstanceOf(WeakMap);
146
+ });
147
+
148
+ it('should have shortcuts as a WeakMap', () => {
149
+ expect(IoCContainer.shortcuts).toBeInstanceOf(WeakMap);
150
+ });
151
+
152
+ it('should have protocolHandlers as a WeakMap', () => {
153
+ expect(IoCContainer.protocolHandlers).toBeInstanceOf(WeakMap);
154
+ });
155
+ });
156
+ });