@lobehub/lobehub 2.0.0-next.140 → 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 (30) hide show
  1. package/CHANGELOG.md +50 -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 +18 -0
  18. package/docs/development/database-schema.dbml +1 -2
  19. package/docs/self-hosting/environment-variables/model-provider.mdx +31 -0
  20. package/docs/self-hosting/environment-variables/model-provider.zh-CN.mdx +30 -0
  21. package/package.json +1 -1
  22. package/packages/database/migrations/0055_rename_phone_number_to_phone.sql +4 -0
  23. package/packages/database/migrations/meta/0055_snapshot.json +8396 -0
  24. package/packages/database/migrations/meta/_journal.json +7 -0
  25. package/packages/database/src/core/migrations.json +11 -0
  26. package/packages/database/src/schemas/user.ts +1 -2
  27. package/packages/model-runtime/src/core/openaiCompatibleFactory/index.ts +6 -3
  28. package/src/config/modelProviders/vertexai.ts +1 -1
  29. package/src/envs/llm.ts +4 -0
  30. package/src/server/modules/ModelRuntime/index.ts +4 -4
@@ -0,0 +1,481 @@
1
+ import { getPort } from 'get-port-please';
2
+ import { createServer } from 'node:http';
3
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
4
+
5
+ import type { App } from '../../App';
6
+ import { StaticFileServerManager } from '../StaticFileServerManager';
7
+
8
+ // Mock get-port-please
9
+ vi.mock('get-port-please', () => ({
10
+ getPort: vi.fn().mockResolvedValue(33250),
11
+ }));
12
+
13
+ // Create mock server and handler storage
14
+ const mockServerHandler = { current: null as any };
15
+ const mockServer = {
16
+ close: vi.fn((cb?: () => void) => cb?.()),
17
+ listen: vi.fn((_port: number, _host: string, cb: () => void) => cb()),
18
+ on: vi.fn(),
19
+ };
20
+
21
+ // Mock node:http
22
+ vi.mock('node:http', () => ({
23
+ createServer: vi.fn((handler: any) => {
24
+ mockServerHandler.current = handler;
25
+ return mockServer;
26
+ }),
27
+ }));
28
+
29
+ // Mock logger
30
+ vi.mock('@/utils/logger', () => ({
31
+ createLogger: () => ({
32
+ debug: vi.fn(),
33
+ error: vi.fn(),
34
+ info: vi.fn(),
35
+ warn: vi.fn(),
36
+ }),
37
+ }));
38
+
39
+ // Mock LOCAL_STORAGE_URL_PREFIX
40
+ vi.mock('@/const/dir', () => ({
41
+ LOCAL_STORAGE_URL_PREFIX: '/lobe-desktop-file',
42
+ }));
43
+
44
+ describe('StaticFileServerManager', () => {
45
+ let manager: StaticFileServerManager;
46
+ let mockApp: App;
47
+ let mockFileService: { getFile: ReturnType<typeof vi.fn> };
48
+
49
+ beforeEach(() => {
50
+ vi.clearAllMocks();
51
+
52
+ // Reset server handler
53
+ mockServerHandler.current = null;
54
+
55
+ // Reset getPort mock to default behavior
56
+ vi.mocked(getPort).mockResolvedValue(33250);
57
+
58
+ // Reset server mock behaviors
59
+ mockServer.listen.mockImplementation((_port: number, _host: string, cb: () => void) => cb());
60
+ mockServer.close.mockImplementation((cb?: () => void) => cb?.());
61
+ mockServer.on.mockReset();
62
+
63
+ // Create mock FileService
64
+ mockFileService = {
65
+ getFile: vi.fn().mockResolvedValue({
66
+ content: new ArrayBuffer(10),
67
+ mimeType: 'image/png',
68
+ }),
69
+ };
70
+
71
+ // Create mock App
72
+ mockApp = {
73
+ getService: vi.fn().mockReturnValue(mockFileService),
74
+ } as unknown as App;
75
+
76
+ manager = new StaticFileServerManager(mockApp);
77
+ });
78
+
79
+ afterEach(() => {
80
+ // Ensure cleanup
81
+ if ((manager as any).isInitialized) {
82
+ manager.destroy();
83
+ }
84
+ });
85
+
86
+ describe('constructor', () => {
87
+ it('should initialize with app reference and get FileService', () => {
88
+ expect(mockApp.getService).toHaveBeenCalled();
89
+ });
90
+ });
91
+
92
+ describe('initialize', () => {
93
+ it('should get available port and start HTTP server', async () => {
94
+ await manager.initialize();
95
+
96
+ expect(getPort).toHaveBeenCalledWith({
97
+ host: '127.0.0.1',
98
+ port: 33_250,
99
+ ports: [33_251, 33_252, 33_253, 33_254, 33_255],
100
+ });
101
+ expect(createServer).toHaveBeenCalled();
102
+ expect(mockServer.listen).toHaveBeenCalledWith(33250, '127.0.0.1', expect.any(Function));
103
+ });
104
+
105
+ it('should skip initialization if already initialized', async () => {
106
+ await manager.initialize();
107
+
108
+ // Clear mocks after first initialization
109
+ vi.mocked(getPort).mockClear();
110
+ vi.mocked(createServer).mockClear();
111
+
112
+ await manager.initialize();
113
+
114
+ expect(getPort).not.toHaveBeenCalled();
115
+ expect(createServer).not.toHaveBeenCalled();
116
+ });
117
+
118
+ it('should throw error when port acquisition fails', async () => {
119
+ const error = new Error('No available port');
120
+ vi.mocked(getPort).mockRejectedValue(error);
121
+
122
+ await expect(manager.initialize()).rejects.toThrow('No available port');
123
+ });
124
+
125
+ it('should handle server startup error', async () => {
126
+ const serverError = new Error('Address in use');
127
+
128
+ // Mock server.on to capture error handler
129
+ let errorHandler: ((err: Error) => void) | undefined;
130
+ mockServer.on.mockImplementation((event: string, handler: any) => {
131
+ if (event === 'error') {
132
+ errorHandler = handler;
133
+ }
134
+ return mockServer;
135
+ });
136
+
137
+ // Mock listen to not call callback but trigger error
138
+ mockServer.listen.mockImplementation(() => {
139
+ // Trigger error after a tick
140
+ setTimeout(() => {
141
+ if (errorHandler) {
142
+ errorHandler(serverError);
143
+ }
144
+ }, 0);
145
+ return mockServer;
146
+ });
147
+
148
+ await expect(manager.initialize()).rejects.toThrow('Address in use');
149
+ });
150
+ });
151
+
152
+ describe('HTTP request handling', () => {
153
+ beforeEach(async () => {
154
+ // Reset mock server behavior
155
+ mockServer.listen.mockImplementation((_port, _host, cb) => cb());
156
+ await manager.initialize();
157
+ });
158
+
159
+ it('should handle OPTIONS request with CORS headers', async () => {
160
+ const req = {
161
+ headers: { origin: 'http://localhost:3000' },
162
+ method: 'OPTIONS',
163
+ on: vi.fn(),
164
+ setTimeout: vi.fn(),
165
+ url: '/lobe-desktop-file/test.png',
166
+ };
167
+ const res = {
168
+ destroyed: false,
169
+ end: vi.fn(),
170
+ headersSent: false,
171
+ writeHead: vi.fn(),
172
+ };
173
+
174
+ await mockServerHandler.current(req, res);
175
+
176
+ expect(res.writeHead).toHaveBeenCalledWith(204, {
177
+ 'Access-Control-Allow-Headers': 'Content-Type',
178
+ 'Access-Control-Allow-Methods': 'GET, OPTIONS',
179
+ 'Access-Control-Allow-Origin': 'http://localhost:3000',
180
+ 'Access-Control-Max-Age': '86400',
181
+ });
182
+ expect(res.end).toHaveBeenCalled();
183
+ });
184
+
185
+ it('should serve file with correct content type and CORS headers', async () => {
186
+ const fileContent = new ArrayBuffer(100);
187
+ mockFileService.getFile.mockResolvedValue({
188
+ content: fileContent,
189
+ mimeType: 'image/jpeg',
190
+ });
191
+
192
+ const req = {
193
+ headers: { origin: 'http://127.0.0.1:3000' },
194
+ method: 'GET',
195
+ on: vi.fn(),
196
+ setTimeout: vi.fn(),
197
+ url: '/lobe-desktop-file/images/test.jpg',
198
+ };
199
+ const res = {
200
+ destroyed: false,
201
+ end: vi.fn(),
202
+ headersSent: false,
203
+ writeHead: vi.fn(),
204
+ };
205
+
206
+ await mockServerHandler.current(req, res);
207
+
208
+ expect(mockFileService.getFile).toHaveBeenCalledWith('desktop://images/test.jpg');
209
+ expect(res.writeHead).toHaveBeenCalledWith(200, {
210
+ 'Access-Control-Allow-Origin': 'http://127.0.0.1:3000',
211
+ 'Cache-Control': 'public, max-age=31536000',
212
+ 'Content-Length': expect.any(Number),
213
+ 'Content-Type': 'image/jpeg',
214
+ });
215
+ expect(res.end).toHaveBeenCalled();
216
+ });
217
+
218
+ it('should return 400 for empty file path', async () => {
219
+ const req = {
220
+ headers: {},
221
+ method: 'GET',
222
+ on: vi.fn(),
223
+ setTimeout: vi.fn(),
224
+ url: '/lobe-desktop-file/',
225
+ };
226
+ const res = {
227
+ destroyed: false,
228
+ end: vi.fn(),
229
+ headersSent: false,
230
+ writeHead: vi.fn(),
231
+ };
232
+
233
+ await mockServerHandler.current(req, res);
234
+
235
+ expect(res.writeHead).toHaveBeenCalledWith(400, { 'Content-Type': 'text/plain' });
236
+ expect(res.end).toHaveBeenCalledWith('Bad Request: Empty file path');
237
+ });
238
+
239
+ it('should return 404 when file not found', async () => {
240
+ const notFoundError = new Error('File not found');
241
+ notFoundError.name = 'FileNotFoundError';
242
+ mockFileService.getFile.mockRejectedValue(notFoundError);
243
+
244
+ const req = {
245
+ headers: { origin: 'http://localhost:3000' },
246
+ method: 'GET',
247
+ on: vi.fn(),
248
+ setTimeout: vi.fn(),
249
+ url: '/lobe-desktop-file/nonexistent.png',
250
+ };
251
+ const res = {
252
+ destroyed: false,
253
+ end: vi.fn(),
254
+ headersSent: false,
255
+ writeHead: vi.fn(),
256
+ };
257
+
258
+ await mockServerHandler.current(req, res);
259
+
260
+ expect(res.writeHead).toHaveBeenCalledWith(404, {
261
+ 'Access-Control-Allow-Origin': 'http://localhost:3000',
262
+ 'Content-Type': 'text/plain',
263
+ });
264
+ expect(res.end).toHaveBeenCalledWith('File Not Found');
265
+ });
266
+
267
+ it('should return 500 for server errors', async () => {
268
+ mockFileService.getFile.mockRejectedValue(new Error('Database error'));
269
+
270
+ const req = {
271
+ headers: {},
272
+ method: 'GET',
273
+ on: vi.fn(),
274
+ setTimeout: vi.fn(),
275
+ url: '/lobe-desktop-file/test.png',
276
+ };
277
+ const res = {
278
+ destroyed: false,
279
+ end: vi.fn(),
280
+ headersSent: false,
281
+ writeHead: vi.fn(),
282
+ };
283
+
284
+ await mockServerHandler.current(req, res);
285
+
286
+ expect(res.writeHead).toHaveBeenCalledWith(500, {
287
+ 'Access-Control-Allow-Origin': '*',
288
+ 'Content-Type': 'text/plain',
289
+ });
290
+ expect(res.end).toHaveBeenCalledWith('Internal Server Error');
291
+ });
292
+
293
+ it('should skip processing if response is already destroyed', async () => {
294
+ const req = {
295
+ headers: {},
296
+ method: 'GET',
297
+ on: vi.fn(),
298
+ setTimeout: vi.fn(),
299
+ url: '/lobe-desktop-file/test.png',
300
+ };
301
+ const res = {
302
+ destroyed: true,
303
+ end: vi.fn(),
304
+ headersSent: false,
305
+ writeHead: vi.fn(),
306
+ };
307
+
308
+ await mockServerHandler.current(req, res);
309
+
310
+ expect(res.writeHead).not.toHaveBeenCalled();
311
+ expect(res.end).not.toHaveBeenCalled();
312
+ expect(mockFileService.getFile).not.toHaveBeenCalled();
313
+ });
314
+
315
+ it('should handle URL-encoded file paths', async () => {
316
+ const req = {
317
+ headers: {},
318
+ method: 'GET',
319
+ on: vi.fn(),
320
+ setTimeout: vi.fn(),
321
+ url: '/lobe-desktop-file/path%20with%20spaces/file%20name.png',
322
+ };
323
+ const res = {
324
+ destroyed: false,
325
+ end: vi.fn(),
326
+ headersSent: false,
327
+ writeHead: vi.fn(),
328
+ };
329
+
330
+ await mockServerHandler.current(req, res);
331
+
332
+ expect(mockFileService.getFile).toHaveBeenCalledWith(
333
+ 'desktop://path with spaces/file name.png',
334
+ );
335
+ });
336
+ });
337
+
338
+ describe('CORS handling', () => {
339
+ beforeEach(async () => {
340
+ mockServer.listen.mockImplementation((_port, _host, cb) => cb());
341
+ await manager.initialize();
342
+ });
343
+
344
+ it('should return specific origin for localhost', async () => {
345
+ const req = {
346
+ headers: { origin: 'http://localhost:3000' },
347
+ method: 'OPTIONS',
348
+ on: vi.fn(),
349
+ setTimeout: vi.fn(),
350
+ url: '/test',
351
+ };
352
+ const res = {
353
+ destroyed: false,
354
+ end: vi.fn(),
355
+ headersSent: false,
356
+ writeHead: vi.fn(),
357
+ };
358
+
359
+ await mockServerHandler.current(req, res);
360
+
361
+ expect(res.writeHead).toHaveBeenCalledWith(
362
+ 204,
363
+ expect.objectContaining({
364
+ 'Access-Control-Allow-Origin': 'http://localhost:3000',
365
+ }),
366
+ );
367
+ });
368
+
369
+ it('should return specific origin for 127.0.0.1', async () => {
370
+ const req = {
371
+ headers: { origin: 'http://127.0.0.1:8080' },
372
+ method: 'OPTIONS',
373
+ on: vi.fn(),
374
+ setTimeout: vi.fn(),
375
+ url: '/test',
376
+ };
377
+ const res = {
378
+ destroyed: false,
379
+ end: vi.fn(),
380
+ headersSent: false,
381
+ writeHead: vi.fn(),
382
+ };
383
+
384
+ await mockServerHandler.current(req, res);
385
+
386
+ expect(res.writeHead).toHaveBeenCalledWith(
387
+ 204,
388
+ expect.objectContaining({
389
+ 'Access-Control-Allow-Origin': 'http://127.0.0.1:8080',
390
+ }),
391
+ );
392
+ });
393
+
394
+ it('should return * for other origins', async () => {
395
+ const req = {
396
+ headers: { origin: 'https://example.com' },
397
+ method: 'OPTIONS',
398
+ on: vi.fn(),
399
+ setTimeout: vi.fn(),
400
+ url: '/test',
401
+ };
402
+ const res = {
403
+ destroyed: false,
404
+ end: vi.fn(),
405
+ headersSent: false,
406
+ writeHead: vi.fn(),
407
+ };
408
+
409
+ await mockServerHandler.current(req, res);
410
+
411
+ expect(res.writeHead).toHaveBeenCalledWith(
412
+ 204,
413
+ expect.objectContaining({
414
+ 'Access-Control-Allow-Origin': '*',
415
+ }),
416
+ );
417
+ });
418
+
419
+ it('should return * for no origin', async () => {
420
+ const req = {
421
+ headers: {},
422
+ method: 'OPTIONS',
423
+ on: vi.fn(),
424
+ setTimeout: vi.fn(),
425
+ url: '/test',
426
+ };
427
+ const res = {
428
+ destroyed: false,
429
+ end: vi.fn(),
430
+ headersSent: false,
431
+ writeHead: vi.fn(),
432
+ };
433
+
434
+ await mockServerHandler.current(req, res);
435
+
436
+ expect(res.writeHead).toHaveBeenCalledWith(
437
+ 204,
438
+ expect.objectContaining({
439
+ 'Access-Control-Allow-Origin': '*',
440
+ }),
441
+ );
442
+ });
443
+ });
444
+
445
+ describe('getFileServerDomain', () => {
446
+ it('should return correct domain when initialized', async () => {
447
+ mockServer.listen.mockImplementation((_port, _host, cb) => cb());
448
+ await manager.initialize();
449
+
450
+ const domain = manager.getFileServerDomain();
451
+
452
+ expect(domain).toBe('http://127.0.0.1:33250');
453
+ });
454
+
455
+ it('should throw error when not initialized', () => {
456
+ expect(() => manager.getFileServerDomain()).toThrow(
457
+ 'StaticFileServerManager not initialized or server not started',
458
+ );
459
+ });
460
+ });
461
+
462
+ describe('destroy', () => {
463
+ it('should close server and reset state', async () => {
464
+ mockServer.listen.mockImplementation((_port, _host, cb) => cb());
465
+ await manager.initialize();
466
+
467
+ manager.destroy();
468
+
469
+ expect(mockServer.close).toHaveBeenCalled();
470
+ expect((manager as any).httpServer).toBeNull();
471
+ expect((manager as any).serverPort).toBe(0);
472
+ expect((manager as any).isInitialized).toBe(false);
473
+ });
474
+
475
+ it('should do nothing if not initialized', () => {
476
+ manager.destroy();
477
+
478
+ expect(mockServer.close).not.toHaveBeenCalled();
479
+ });
480
+ });
481
+ });
@@ -0,0 +1,164 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ import type { App as AppCore } from '../../App';
4
+ import { StoreManager } from '../StoreManager';
5
+
6
+ // Use vi.hoisted to define mocks before hoisting
7
+ const { mockStoreInstance, mockMakeSureDirExist, MockStore } = vi.hoisted(() => {
8
+ const mockStoreInstance = {
9
+ clear: vi.fn(),
10
+ delete: vi.fn(),
11
+ get: vi.fn().mockImplementation((key: string, defaultValue?: any) => {
12
+ if (key === 'storagePath') return '/mock/storage/path';
13
+ return defaultValue;
14
+ }),
15
+ has: vi.fn().mockReturnValue(false),
16
+ openInEditor: vi.fn().mockResolvedValue(undefined),
17
+ set: vi.fn(),
18
+ };
19
+
20
+ const MockStore = vi.fn().mockImplementation(() => mockStoreInstance);
21
+
22
+ return {
23
+ MockStore,
24
+ mockMakeSureDirExist: vi.fn(),
25
+ mockStoreInstance,
26
+ };
27
+ });
28
+
29
+ // Mock electron-store
30
+ vi.mock('electron-store', () => ({
31
+ default: MockStore,
32
+ }));
33
+
34
+ // Mock logger
35
+ vi.mock('@/utils/logger', () => ({
36
+ createLogger: () => ({
37
+ debug: vi.fn(),
38
+ error: vi.fn(),
39
+ info: vi.fn(),
40
+ warn: vi.fn(),
41
+ }),
42
+ }));
43
+
44
+ // Mock file-system utils
45
+ vi.mock('@/utils/file-system', () => ({
46
+ makeSureDirExist: mockMakeSureDirExist,
47
+ }));
48
+
49
+ // Mock store constants
50
+ vi.mock('@/const/store', () => ({
51
+ STORE_DEFAULTS: {
52
+ locale: 'auto',
53
+ storagePath: '/default/storage/path',
54
+ },
55
+ STORE_NAME: 'test-config',
56
+ }));
57
+
58
+ describe('StoreManager', () => {
59
+ let manager: StoreManager;
60
+ let mockAppCore: AppCore;
61
+
62
+ beforeEach(() => {
63
+ vi.clearAllMocks();
64
+
65
+ // Reset store mock behaviors
66
+ mockStoreInstance.get.mockImplementation((key: string, defaultValue?: any) => {
67
+ if (key === 'storagePath') return '/mock/storage/path';
68
+ return defaultValue;
69
+ });
70
+ mockStoreInstance.has.mockReturnValue(false);
71
+
72
+ // Create mock App core
73
+ mockAppCore = {} as unknown as AppCore;
74
+
75
+ manager = new StoreManager(mockAppCore);
76
+ });
77
+
78
+ describe('constructor', () => {
79
+ it('should create electron-store with correct options', () => {
80
+ expect(MockStore).toHaveBeenCalledWith({
81
+ defaults: {
82
+ locale: 'auto',
83
+ storagePath: '/default/storage/path',
84
+ },
85
+ name: 'test-config',
86
+ });
87
+ });
88
+
89
+ it('should ensure storage directory exists', () => {
90
+ expect(mockMakeSureDirExist).toHaveBeenCalledWith('/mock/storage/path');
91
+ });
92
+ });
93
+
94
+ describe('get', () => {
95
+ it('should call store.get with key', () => {
96
+ mockStoreInstance.get.mockReturnValue('test-value');
97
+
98
+ const result = manager.get('locale' as any);
99
+
100
+ expect(mockStoreInstance.get).toHaveBeenCalledWith('locale', undefined);
101
+ expect(result).toBe('test-value');
102
+ });
103
+
104
+ it('should call store.get with key and default value', () => {
105
+ mockStoreInstance.get.mockImplementation((_key: string, defaultValue?: any) => defaultValue);
106
+
107
+ const result = manager.get('locale' as any, 'en-US' as any);
108
+
109
+ expect(mockStoreInstance.get).toHaveBeenCalledWith('locale', 'en-US');
110
+ expect(result).toBe('en-US');
111
+ });
112
+ });
113
+
114
+ describe('set', () => {
115
+ it('should call store.set with key and value', () => {
116
+ manager.set('locale' as any, 'zh-CN' as any);
117
+
118
+ expect(mockStoreInstance.set).toHaveBeenCalledWith('locale', 'zh-CN');
119
+ });
120
+ });
121
+
122
+ describe('delete', () => {
123
+ it('should call store.delete with key', () => {
124
+ manager.delete('locale' as any);
125
+
126
+ expect(mockStoreInstance.delete).toHaveBeenCalledWith('locale');
127
+ });
128
+ });
129
+
130
+ describe('clear', () => {
131
+ it('should call store.clear', () => {
132
+ manager.clear();
133
+
134
+ expect(mockStoreInstance.clear).toHaveBeenCalled();
135
+ });
136
+ });
137
+
138
+ describe('has', () => {
139
+ it('should return true when key exists', () => {
140
+ mockStoreInstance.has.mockReturnValue(true);
141
+
142
+ const result = manager.has('locale' as any);
143
+
144
+ expect(mockStoreInstance.has).toHaveBeenCalledWith('locale');
145
+ expect(result).toBe(true);
146
+ });
147
+
148
+ it('should return false when key does not exist', () => {
149
+ mockStoreInstance.has.mockReturnValue(false);
150
+
151
+ const result = manager.has('nonExistent' as any);
152
+
153
+ expect(result).toBe(false);
154
+ });
155
+ });
156
+
157
+ describe('openInEditor', () => {
158
+ it('should call store.openInEditor', async () => {
159
+ await manager.openInEditor();
160
+
161
+ expect(mockStoreInstance.openInEditor).toHaveBeenCalled();
162
+ });
163
+ });
164
+ });