@lobehub/lobehub 2.0.0-next.141 → 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 (75) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/Dockerfile +2 -0
  3. package/apps/desktop/package.json +1 -0
  4. package/apps/desktop/src/main/controllers/__tests__/McpInstallCtr.test.ts +286 -0
  5. package/apps/desktop/src/main/controllers/__tests__/NotificationCtr.test.ts +347 -0
  6. package/apps/desktop/src/main/controllers/__tests__/RemoteServerConfigCtr.test.ts +645 -0
  7. package/apps/desktop/src/main/controllers/__tests__/RemoteServerSyncCtr.test.ts +372 -0
  8. package/apps/desktop/src/main/controllers/__tests__/SystemCtr.test.ts +276 -0
  9. package/apps/desktop/src/main/controllers/__tests__/UploadFileCtr.test.ts +171 -0
  10. package/apps/desktop/src/main/core/browser/__tests__/Browser.test.ts +573 -0
  11. package/apps/desktop/src/main/core/browser/__tests__/BrowserManager.test.ts +415 -0
  12. package/apps/desktop/src/main/core/infrastructure/__tests__/I18nManager.test.ts +353 -0
  13. package/apps/desktop/src/main/core/infrastructure/__tests__/IoCContainer.test.ts +156 -0
  14. package/apps/desktop/src/main/core/infrastructure/__tests__/ProtocolManager.test.ts +348 -0
  15. package/apps/desktop/src/main/core/infrastructure/__tests__/StaticFileServerManager.test.ts +481 -0
  16. package/apps/desktop/src/main/core/infrastructure/__tests__/StoreManager.test.ts +164 -0
  17. package/apps/desktop/src/main/core/infrastructure/__tests__/UpdaterManager.test.ts +513 -0
  18. package/apps/desktop/src/main/core/ui/__tests__/MenuManager.test.ts +320 -0
  19. package/apps/desktop/src/main/core/ui/__tests__/Tray.test.ts +518 -0
  20. package/apps/desktop/src/main/core/ui/__tests__/TrayManager.test.ts +360 -0
  21. package/apps/desktop/src/main/menus/impls/BaseMenuPlatform.test.ts +49 -0
  22. package/apps/desktop/src/main/menus/impls/linux.test.ts +552 -0
  23. package/apps/desktop/src/main/menus/impls/macOS.test.ts +464 -0
  24. package/apps/desktop/src/main/menus/impls/windows.test.ts +429 -0
  25. package/apps/desktop/src/main/modules/fileSearch/__tests__/macOS.integration.test.ts +2 -2
  26. package/apps/desktop/src/main/services/__tests__/fileSearchSrv.test.ts +402 -0
  27. package/apps/desktop/src/main/utils/__tests__/file-system.test.ts +91 -0
  28. package/apps/desktop/src/main/utils/__tests__/logger.test.ts +229 -0
  29. package/apps/desktop/src/preload/electronApi.test.ts +142 -0
  30. package/apps/desktop/src/preload/invoke.test.ts +145 -0
  31. package/apps/desktop/src/preload/routeInterceptor.test.ts +374 -0
  32. package/apps/desktop/src/preload/streamer.test.ts +365 -0
  33. package/apps/desktop/vitest.config.mts +1 -0
  34. package/changelog/v1.json +18 -0
  35. package/docs/self-hosting/environment-variables/model-provider.mdx +31 -0
  36. package/docs/self-hosting/environment-variables/model-provider.zh-CN.mdx +30 -0
  37. package/locales/ar/marketAuth.json +13 -0
  38. package/locales/bg-BG/marketAuth.json +13 -0
  39. package/locales/de-DE/marketAuth.json +13 -0
  40. package/locales/en-US/marketAuth.json +13 -0
  41. package/locales/es-ES/marketAuth.json +13 -0
  42. package/locales/fa-IR/marketAuth.json +13 -0
  43. package/locales/fr-FR/marketAuth.json +13 -0
  44. package/locales/it-IT/marketAuth.json +13 -0
  45. package/locales/ja-JP/marketAuth.json +13 -0
  46. package/locales/ko-KR/marketAuth.json +13 -0
  47. package/locales/nl-NL/marketAuth.json +13 -0
  48. package/locales/pl-PL/marketAuth.json +13 -0
  49. package/locales/pt-BR/marketAuth.json +13 -0
  50. package/locales/ru-RU/marketAuth.json +13 -0
  51. package/locales/tr-TR/marketAuth.json +13 -0
  52. package/locales/vi-VN/marketAuth.json +13 -0
  53. package/locales/zh-CN/marketAuth.json +13 -0
  54. package/locales/zh-TW/marketAuth.json +13 -0
  55. package/package.json +1 -1
  56. package/packages/database/src/models/user.ts +2 -0
  57. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +6 -3
  58. package/packages/types/src/discover/mcp.ts +2 -1
  59. package/packages/types/src/tool/plugin.ts +2 -1
  60. package/src/app/[variants]/(main)/chat/settings/features/SmartAgentActionButton/MarketPublishButton.tsx +0 -2
  61. package/src/app/[variants]/(main)/discover/(detail)/mcp/features/Sidebar/ActionButton/index.tsx +33 -7
  62. package/src/config/modelProviders/vertexai.ts +1 -1
  63. package/src/envs/llm.ts +4 -0
  64. package/src/features/PluginStore/McpList/List/Action.tsx +20 -1
  65. package/src/layout/AuthProvider/MarketAuth/MarketAuthConfirmModal.tsx +158 -0
  66. package/src/layout/AuthProvider/MarketAuth/MarketAuthProvider.tsx +130 -14
  67. package/src/libs/mcp/types.ts +8 -0
  68. package/src/locales/default/marketAuth.ts +13 -0
  69. package/src/server/modules/ModelRuntime/index.ts +4 -4
  70. package/src/server/routers/lambda/market/index.ts +85 -2
  71. package/src/server/services/discover/index.ts +45 -4
  72. package/src/services/discover.ts +1 -1
  73. package/src/services/mcp.ts +18 -3
  74. package/src/store/tool/slices/mcpStore/action.test.ts +141 -0
  75. package/src/store/tool/slices/mcpStore/action.ts +153 -11
@@ -0,0 +1,513 @@
1
+ import { autoUpdater } from 'electron-updater';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import type { App as AppCore } from '../../App';
5
+ import { UpdaterManager } from '../UpdaterManager';
6
+
7
+ // Use vi.hoisted to ensure mocks work with require()
8
+ const { mockGetAllWindows, mockReleaseSingleInstanceLock } = vi.hoisted(() => ({
9
+ mockGetAllWindows: vi.fn().mockReturnValue([]),
10
+ mockReleaseSingleInstanceLock: vi.fn(),
11
+ }));
12
+
13
+ // Mock electron-log
14
+ vi.mock('electron-log', () => ({
15
+ default: {
16
+ transports: {
17
+ file: {
18
+ level: 'info',
19
+ getFile: vi.fn().mockReturnValue({ path: '/mock/log/path' }),
20
+ },
21
+ },
22
+ },
23
+ }));
24
+
25
+ // Mock electron-updater
26
+ vi.mock('electron-updater', () => ({
27
+ autoUpdater: {
28
+ allowDowngrade: false,
29
+ allowPrerelease: false,
30
+ autoDownload: false,
31
+ autoInstallOnAppQuit: false,
32
+ channel: 'stable',
33
+ checkForUpdates: vi.fn(),
34
+ downloadUpdate: vi.fn(),
35
+ forceDevUpdateConfig: false,
36
+ logger: null as any,
37
+ on: vi.fn(),
38
+ quitAndInstall: vi.fn(),
39
+ },
40
+ }));
41
+
42
+ // Mock electron - uses hoisted functions for require() compatibility
43
+ vi.mock('electron', () => ({
44
+ BrowserWindow: {
45
+ getAllWindows: mockGetAllWindows,
46
+ },
47
+ app: {
48
+ releaseSingleInstanceLock: mockReleaseSingleInstanceLock,
49
+ },
50
+ }));
51
+
52
+ // Mock logger
53
+ vi.mock('@/utils/logger', () => ({
54
+ createLogger: () => ({
55
+ debug: vi.fn(),
56
+ error: vi.fn(),
57
+ info: vi.fn(),
58
+ warn: vi.fn(),
59
+ }),
60
+ }));
61
+
62
+ // Mock updater configs
63
+ vi.mock('@/modules/updater/configs', () => ({
64
+ UPDATE_CHANNEL: 'stable',
65
+ updaterConfig: {
66
+ app: {
67
+ autoCheckUpdate: false,
68
+ autoDownloadUpdate: true,
69
+ checkUpdateInterval: 60 * 60 * 1000,
70
+ },
71
+ enableAppUpdate: true,
72
+ enableRenderHotUpdate: true,
73
+ },
74
+ }));
75
+
76
+ // Mock isDev
77
+ vi.mock('@/const/env', () => ({
78
+ isDev: false,
79
+ }));
80
+
81
+ describe('UpdaterManager', () => {
82
+ let updaterManager: UpdaterManager;
83
+ let mockApp: AppCore;
84
+ let mockBroadcast: ReturnType<typeof vi.fn>;
85
+ let registeredEvents: Map<string, (...args: any[]) => void>;
86
+
87
+ beforeEach(() => {
88
+ vi.clearAllMocks();
89
+ vi.useFakeTimers();
90
+
91
+ // Reset autoUpdater state
92
+ (autoUpdater as any).autoDownload = false;
93
+ (autoUpdater as any).autoInstallOnAppQuit = false;
94
+ (autoUpdater as any).channel = 'stable';
95
+ (autoUpdater as any).allowPrerelease = false;
96
+ (autoUpdater as any).allowDowngrade = false;
97
+ (autoUpdater as any).forceDevUpdateConfig = false;
98
+
99
+ // Capture registered events
100
+ registeredEvents = new Map();
101
+ vi.mocked(autoUpdater.on).mockImplementation((event: string, handler: any) => {
102
+ registeredEvents.set(event, handler);
103
+ return autoUpdater;
104
+ });
105
+
106
+ // Mock broadcast function
107
+ mockBroadcast = vi.fn();
108
+
109
+ // Create mock App
110
+ mockApp = {
111
+ browserManager: {
112
+ getMainWindow: vi.fn().mockReturnValue({
113
+ broadcast: mockBroadcast,
114
+ }),
115
+ },
116
+ isQuiting: false,
117
+ } as unknown as AppCore;
118
+
119
+ updaterManager = new UpdaterManager(mockApp);
120
+ });
121
+
122
+ afterEach(() => {
123
+ vi.useRealTimers();
124
+ });
125
+
126
+ describe('constructor', () => {
127
+ it('should set up electron-log for autoUpdater', () => {
128
+ expect(autoUpdater.logger).not.toBeNull();
129
+ });
130
+ });
131
+
132
+ describe('initialize', () => {
133
+ it('should configure autoUpdater properties', async () => {
134
+ await updaterManager.initialize();
135
+
136
+ expect(autoUpdater.autoDownload).toBe(false);
137
+ expect(autoUpdater.autoInstallOnAppQuit).toBe(false);
138
+ expect(autoUpdater.channel).toBe('stable');
139
+ expect(autoUpdater.allowPrerelease).toBe(false);
140
+ expect(autoUpdater.allowDowngrade).toBe(false);
141
+ });
142
+
143
+ it('should register all event listeners', async () => {
144
+ await updaterManager.initialize();
145
+
146
+ expect(autoUpdater.on).toHaveBeenCalledWith('checking-for-update', expect.any(Function));
147
+ expect(autoUpdater.on).toHaveBeenCalledWith('update-available', expect.any(Function));
148
+ expect(autoUpdater.on).toHaveBeenCalledWith('update-not-available', expect.any(Function));
149
+ expect(autoUpdater.on).toHaveBeenCalledWith('error', expect.any(Function));
150
+ expect(autoUpdater.on).toHaveBeenCalledWith('download-progress', expect.any(Function));
151
+ expect(autoUpdater.on).toHaveBeenCalledWith('update-downloaded', expect.any(Function));
152
+ });
153
+ });
154
+
155
+ describe('checkForUpdates', () => {
156
+ beforeEach(async () => {
157
+ await updaterManager.initialize();
158
+ vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
159
+ });
160
+
161
+ it('should call autoUpdater.checkForUpdates', async () => {
162
+ await updaterManager.checkForUpdates();
163
+
164
+ expect(autoUpdater.checkForUpdates).toHaveBeenCalled();
165
+ });
166
+
167
+ it('should broadcast manualUpdateCheckStart when manual check', async () => {
168
+ await updaterManager.checkForUpdates({ manual: true });
169
+
170
+ expect(mockBroadcast).toHaveBeenCalledWith('manualUpdateCheckStart');
171
+ });
172
+
173
+ it('should not broadcast when auto check', async () => {
174
+ await updaterManager.checkForUpdates({ manual: false });
175
+
176
+ expect(mockBroadcast).not.toHaveBeenCalledWith('manualUpdateCheckStart');
177
+ });
178
+
179
+ it('should ignore duplicate check requests while checking', async () => {
180
+ // Start first check but don't resolve
181
+ vi.mocked(autoUpdater.checkForUpdates).mockImplementation(
182
+ () => new Promise((resolve) => setTimeout(resolve, 1000)) as any,
183
+ );
184
+
185
+ const firstCheck = updaterManager.checkForUpdates();
186
+ const secondCheck = updaterManager.checkForUpdates();
187
+
188
+ await vi.advanceTimersByTimeAsync(1000);
189
+ await Promise.all([firstCheck, secondCheck]);
190
+
191
+ expect(autoUpdater.checkForUpdates).toHaveBeenCalledTimes(1);
192
+ });
193
+
194
+ it('should broadcast updateError when check fails during manual check', async () => {
195
+ const error = new Error('Network error');
196
+ vi.mocked(autoUpdater.checkForUpdates).mockRejectedValue(error);
197
+
198
+ await updaterManager.checkForUpdates({ manual: true });
199
+
200
+ expect(mockBroadcast).toHaveBeenCalledWith('updateError', 'Network error');
201
+ });
202
+ });
203
+
204
+ describe('downloadUpdate', () => {
205
+ beforeEach(async () => {
206
+ await updaterManager.initialize();
207
+ vi.mocked(autoUpdater.downloadUpdate).mockResolvedValue([] as any);
208
+
209
+ // Simulate update available
210
+ const updateAvailableHandler = registeredEvents.get('update-available');
211
+ updateAvailableHandler?.({ version: '2.0.0' });
212
+ });
213
+
214
+ it('should call autoUpdater.downloadUpdate', async () => {
215
+ await updaterManager.downloadUpdate();
216
+
217
+ expect(autoUpdater.downloadUpdate).toHaveBeenCalled();
218
+ });
219
+
220
+ it('should ignore download request when no update available', async () => {
221
+ // Create fresh manager without update available
222
+ const freshManager = new UpdaterManager(mockApp);
223
+ await freshManager.initialize();
224
+
225
+ await freshManager.downloadUpdate();
226
+
227
+ // Reset call count since downloadUpdate might have been called in beforeEach
228
+ vi.mocked(autoUpdater.downloadUpdate).mockClear();
229
+ await freshManager.downloadUpdate();
230
+
231
+ // downloadUpdate should not be called on autoUpdater for fresh manager
232
+ expect(autoUpdater.downloadUpdate).not.toHaveBeenCalled();
233
+ });
234
+
235
+ it('should ignore duplicate download requests while downloading', async () => {
236
+ vi.mocked(autoUpdater.downloadUpdate).mockImplementation(
237
+ () => new Promise((resolve) => setTimeout(resolve, 1000)) as any,
238
+ );
239
+
240
+ const firstDownload = updaterManager.downloadUpdate();
241
+ const secondDownload = updaterManager.downloadUpdate();
242
+
243
+ await vi.advanceTimersByTimeAsync(1000);
244
+ await Promise.all([firstDownload, secondDownload]);
245
+
246
+ expect(autoUpdater.downloadUpdate).toHaveBeenCalledTimes(1);
247
+ });
248
+
249
+ it('should broadcast updateDownloadStart when isManualCheck is true', async () => {
250
+ // Create a fresh manager to avoid state pollution from beforeEach
251
+ const freshManager = new UpdaterManager(mockApp);
252
+
253
+ // Setup fresh event capture
254
+ const freshEvents = new Map<string, (...args: any[]) => void>();
255
+ vi.mocked(autoUpdater.on).mockImplementation((event: string, handler: any) => {
256
+ freshEvents.set(event, handler);
257
+ return autoUpdater;
258
+ });
259
+ await freshManager.initialize();
260
+
261
+ // Trigger a manual check to set isManualCheck = true
262
+ vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
263
+ await freshManager.checkForUpdates({ manual: true });
264
+
265
+ // Manually set updateAvailable without triggering auto-download
266
+ // Access private property to set state
267
+ (freshManager as any).updateAvailable = true;
268
+
269
+ // Clear previous broadcast calls
270
+ mockBroadcast.mockClear();
271
+
272
+ // Now download should broadcast updateDownloadStart because isManualCheck is true
273
+ vi.mocked(autoUpdater.downloadUpdate).mockResolvedValue([] as any);
274
+ await freshManager.downloadUpdate();
275
+
276
+ expect(mockBroadcast).toHaveBeenCalledWith('updateDownloadStart');
277
+ });
278
+
279
+ it('should broadcast updateError when download fails with isManualCheck true', async () => {
280
+ // Create a fresh manager to avoid state pollution from beforeEach
281
+ const freshManager = new UpdaterManager(mockApp);
282
+
283
+ // Setup fresh event capture
284
+ const freshEvents = new Map<string, (...args: any[]) => void>();
285
+ vi.mocked(autoUpdater.on).mockImplementation((event: string, handler: any) => {
286
+ freshEvents.set(event, handler);
287
+ return autoUpdater;
288
+ });
289
+ await freshManager.initialize();
290
+
291
+ // Trigger a manual check to set isManualCheck = true
292
+ vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
293
+ await freshManager.checkForUpdates({ manual: true });
294
+
295
+ // Manually set updateAvailable without triggering auto-download
296
+ (freshManager as any).updateAvailable = true;
297
+
298
+ // Clear previous broadcast calls
299
+ mockBroadcast.mockClear();
300
+
301
+ // Setup error
302
+ const error = new Error('Download failed');
303
+ vi.mocked(autoUpdater.downloadUpdate).mockRejectedValue(error);
304
+
305
+ await freshManager.downloadUpdate();
306
+
307
+ expect(mockBroadcast).toHaveBeenCalledWith('updateError', 'Download failed');
308
+ });
309
+ });
310
+
311
+ describe('installNow', () => {
312
+ // Note: installNow uses require('electron') which is difficult to mock in vitest.
313
+ // These tests are skipped because vi.mock doesn't work with dynamic require().
314
+ // The functionality should be tested in integration tests or E2E tests.
315
+
316
+ it.skip('should set app.isQuiting to true', () => {
317
+ updaterManager.installNow();
318
+ expect(mockApp.isQuiting).toBe(true);
319
+ });
320
+
321
+ it.skip('should close all windows', () => {
322
+ const mockWindow1 = { close: vi.fn(), isDestroyed: vi.fn().mockReturnValue(false) };
323
+ const mockWindow2 = { close: vi.fn(), isDestroyed: vi.fn().mockReturnValue(false) };
324
+ mockGetAllWindows.mockReturnValue([mockWindow1, mockWindow2]);
325
+ updaterManager.installNow();
326
+ expect(mockWindow1.close).toHaveBeenCalled();
327
+ expect(mockWindow2.close).toHaveBeenCalled();
328
+ });
329
+
330
+ it.skip('should not close destroyed windows', () => {
331
+ const mockWindow = { close: vi.fn(), isDestroyed: vi.fn().mockReturnValue(true) };
332
+ mockGetAllWindows.mockReturnValue([mockWindow]);
333
+ updaterManager.installNow();
334
+ expect(mockWindow.close).not.toHaveBeenCalled();
335
+ });
336
+
337
+ it.skip('should release single instance lock', () => {
338
+ updaterManager.installNow();
339
+ expect(mockReleaseSingleInstanceLock).toHaveBeenCalled();
340
+ });
341
+
342
+ it.skip('should call quitAndInstall with correct parameters after delay', async () => {
343
+ updaterManager.installNow();
344
+ expect(autoUpdater.quitAndInstall).not.toHaveBeenCalled();
345
+ await vi.advanceTimersByTimeAsync(100);
346
+ expect(autoUpdater.quitAndInstall).toHaveBeenCalledWith(true, true);
347
+ });
348
+ });
349
+
350
+ describe('installLater', () => {
351
+ it('should set autoInstallOnAppQuit to true', () => {
352
+ updaterManager.installLater();
353
+
354
+ expect(autoUpdater.autoInstallOnAppQuit).toBe(true);
355
+ });
356
+
357
+ it('should broadcast updateWillInstallLater', () => {
358
+ updaterManager.installLater();
359
+
360
+ expect(mockBroadcast).toHaveBeenCalledWith('updateWillInstallLater');
361
+ });
362
+ });
363
+
364
+ describe('event handlers', () => {
365
+ beforeEach(async () => {
366
+ await updaterManager.initialize();
367
+ });
368
+
369
+ describe('update-available', () => {
370
+ it('should broadcast manualUpdateAvailable when manual check', async () => {
371
+ // Trigger manual check first
372
+ vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
373
+ await updaterManager.checkForUpdates({ manual: true });
374
+
375
+ const updateInfo = { version: '2.0.0' };
376
+ const handler = registeredEvents.get('update-available');
377
+ handler?.(updateInfo);
378
+
379
+ expect(mockBroadcast).toHaveBeenCalledWith('manualUpdateAvailable', updateInfo);
380
+ });
381
+
382
+ it('should auto download when auto check finds update', async () => {
383
+ // Trigger auto check first
384
+ vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
385
+ await updaterManager.checkForUpdates({ manual: false });
386
+
387
+ vi.mocked(autoUpdater.downloadUpdate).mockResolvedValue([] as any);
388
+
389
+ const handler = registeredEvents.get('update-available');
390
+ handler?.({ version: '2.0.0' });
391
+
392
+ expect(autoUpdater.downloadUpdate).toHaveBeenCalled();
393
+ });
394
+ });
395
+
396
+ describe('update-not-available', () => {
397
+ it('should broadcast manualUpdateNotAvailable when manual check', async () => {
398
+ vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
399
+ await updaterManager.checkForUpdates({ manual: true });
400
+
401
+ const info = { version: '1.0.0' };
402
+ const handler = registeredEvents.get('update-not-available');
403
+ handler?.(info);
404
+
405
+ expect(mockBroadcast).toHaveBeenCalledWith('manualUpdateNotAvailable', info);
406
+ });
407
+
408
+ it('should not broadcast when auto check', async () => {
409
+ vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
410
+ await updaterManager.checkForUpdates({ manual: false });
411
+
412
+ const handler = registeredEvents.get('update-not-available');
413
+ handler?.({ version: '1.0.0' });
414
+
415
+ expect(mockBroadcast).not.toHaveBeenCalledWith(
416
+ 'manualUpdateNotAvailable',
417
+ expect.anything(),
418
+ );
419
+ });
420
+ });
421
+
422
+ describe('download-progress', () => {
423
+ it('should broadcast progress when manual check', async () => {
424
+ vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
425
+ await updaterManager.checkForUpdates({ manual: true });
426
+
427
+ const progressObj = {
428
+ bytesPerSecond: 1024,
429
+ percent: 50,
430
+ total: 1024 * 1024,
431
+ transferred: 512 * 1024,
432
+ };
433
+ const handler = registeredEvents.get('download-progress');
434
+ handler?.(progressObj);
435
+
436
+ expect(mockBroadcast).toHaveBeenCalledWith('updateDownloadProgress', progressObj);
437
+ });
438
+ });
439
+
440
+ describe('update-downloaded', () => {
441
+ it('should broadcast updateDownloaded', async () => {
442
+ await updaterManager.initialize();
443
+
444
+ const info = { version: '2.0.0' };
445
+ const handler = registeredEvents.get('update-downloaded');
446
+ handler?.(info);
447
+
448
+ expect(mockBroadcast).toHaveBeenCalledWith('updateDownloaded', info);
449
+ });
450
+ });
451
+
452
+ describe('error', () => {
453
+ it('should broadcast updateError when manual check', async () => {
454
+ vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
455
+ await updaterManager.checkForUpdates({ manual: true });
456
+
457
+ const error = new Error('Update error');
458
+ const handler = registeredEvents.get('error');
459
+ handler?.(error);
460
+
461
+ expect(mockBroadcast).toHaveBeenCalledWith('updateError', 'Update error');
462
+ });
463
+
464
+ it('should not broadcast when auto check', async () => {
465
+ vi.mocked(autoUpdater.checkForUpdates).mockResolvedValue({} as any);
466
+ await updaterManager.checkForUpdates({ manual: false });
467
+
468
+ const error = new Error('Update error');
469
+ const handler = registeredEvents.get('error');
470
+ handler?.(error);
471
+
472
+ expect(mockBroadcast).not.toHaveBeenCalledWith('updateError', expect.anything());
473
+ });
474
+ });
475
+ });
476
+
477
+ describe('simulation methods (dev mode)', () => {
478
+ it('simulateUpdateAvailable should do nothing when not in dev mode', () => {
479
+ // Current mock has isDev = false
480
+ updaterManager.simulateUpdateAvailable();
481
+
482
+ // Should not broadcast anything since isDev is false
483
+ expect(mockBroadcast).not.toHaveBeenCalledWith(
484
+ 'manualUpdateAvailable',
485
+ expect.objectContaining({ version: '1.0.0' }),
486
+ );
487
+ });
488
+
489
+ it('simulateUpdateDownloaded should do nothing when not in dev mode', () => {
490
+ updaterManager.simulateUpdateDownloaded();
491
+
492
+ expect(mockBroadcast).not.toHaveBeenCalledWith(
493
+ 'updateDownloaded',
494
+ expect.objectContaining({ version: '1.0.0' }),
495
+ );
496
+ });
497
+
498
+ it('simulateDownloadProgress should do nothing when not in dev mode', () => {
499
+ updaterManager.simulateDownloadProgress();
500
+
501
+ expect(mockBroadcast).not.toHaveBeenCalledWith('updateDownloadStart');
502
+ });
503
+ });
504
+
505
+ describe('mainWindow getter', () => {
506
+ it('should return main window from browserManager', () => {
507
+ const mainWindow = updaterManager['mainWindow'];
508
+
509
+ expect(mockApp.browserManager.getMainWindow).toHaveBeenCalled();
510
+ expect(mainWindow.broadcast).toBe(mockBroadcast);
511
+ });
512
+ });
513
+ });