@lobehub/chat 1.124.1 → 1.124.2

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 (100) hide show
  1. package/.github/scripts/pr-comment.js +11 -2
  2. package/.github/workflows/desktop-pr-build.yml +86 -12
  3. package/.github/workflows/release-desktop-beta.yml +91 -20
  4. package/CHANGELOG.md +25 -0
  5. package/apps/desktop/electron-builder.js +8 -4
  6. package/changelog/v1.json +9 -0
  7. package/package.json +1 -1
  8. package/packages/const/src/hotkeys.ts +1 -1
  9. package/packages/const/src/index.ts +1 -0
  10. package/packages/const/src/settings/hotkey.ts +3 -2
  11. package/packages/const/src/trace.ts +1 -1
  12. package/packages/const/src/user.ts +1 -2
  13. package/packages/database/src/client/db.test.ts +19 -13
  14. package/packages/electron-server-ipc/src/ipcClient.test.ts +783 -1
  15. package/packages/file-loaders/src/loadFile.test.ts +61 -0
  16. package/packages/file-loaders/src/utils/isTextReadableFile.test.ts +43 -0
  17. package/packages/file-loaders/src/utils/parser-utils.test.ts +155 -0
  18. package/packages/model-runtime/package.json +2 -1
  19. package/packages/model-runtime/src/ai21/index.test.ts +2 -2
  20. package/packages/model-runtime/src/ai360/index.test.ts +2 -2
  21. package/packages/model-runtime/src/akashchat/index.test.ts +19 -0
  22. package/packages/model-runtime/src/anthropic/index.test.ts +1 -2
  23. package/packages/model-runtime/src/baichuan/index.test.ts +1 -2
  24. package/packages/model-runtime/src/bedrock/index.test.ts +1 -2
  25. package/packages/model-runtime/src/bfl/createImage.test.ts +1 -2
  26. package/packages/model-runtime/src/bfl/index.test.ts +1 -2
  27. package/packages/model-runtime/src/cloudflare/index.test.ts +1 -2
  28. package/packages/model-runtime/src/cohere/index.test.ts +19 -0
  29. package/packages/model-runtime/src/deepseek/index.test.ts +2 -2
  30. package/packages/model-runtime/src/fireworksai/index.test.ts +2 -2
  31. package/packages/model-runtime/src/giteeai/index.test.ts +2 -2
  32. package/packages/model-runtime/src/github/index.test.ts +2 -2
  33. package/packages/model-runtime/src/google/createImage.test.ts +1 -2
  34. package/packages/model-runtime/src/google/index.test.ts +1 -1
  35. package/packages/model-runtime/src/groq/index.test.ts +2 -3
  36. package/packages/model-runtime/src/huggingface/index.test.ts +40 -0
  37. package/packages/model-runtime/src/hunyuan/index.test.ts +2 -3
  38. package/packages/model-runtime/src/internlm/index.test.ts +2 -2
  39. package/packages/model-runtime/src/jina/index.test.ts +19 -0
  40. package/packages/model-runtime/src/lmstudio/index.test.ts +2 -2
  41. package/packages/model-runtime/src/minimax/index.test.ts +19 -0
  42. package/packages/model-runtime/src/mistral/index.test.ts +2 -3
  43. package/packages/model-runtime/src/modelscope/index.test.ts +19 -0
  44. package/packages/model-runtime/src/moonshot/index.test.ts +1 -2
  45. package/packages/model-runtime/src/nebius/index.test.ts +19 -0
  46. package/packages/model-runtime/src/novita/index.test.ts +3 -4
  47. package/packages/model-runtime/src/nvidia/index.test.ts +19 -0
  48. package/packages/model-runtime/src/openrouter/index.test.ts +2 -3
  49. package/packages/model-runtime/src/perplexity/index.test.ts +2 -3
  50. package/packages/model-runtime/src/ppio/index.test.ts +3 -4
  51. package/packages/model-runtime/src/qwen/index.test.ts +2 -2
  52. package/packages/model-runtime/src/sambanova/index.test.ts +19 -0
  53. package/packages/model-runtime/src/search1api/index.test.ts +19 -0
  54. package/packages/model-runtime/src/sensenova/index.test.ts +2 -2
  55. package/packages/model-runtime/src/spark/index.test.ts +2 -2
  56. package/packages/model-runtime/src/stepfun/index.test.ts +2 -2
  57. package/packages/model-runtime/src/taichu/index.test.ts +4 -5
  58. package/packages/model-runtime/src/tencentcloud/index.test.ts +1 -1
  59. package/packages/model-runtime/src/togetherai/index.test.ts +1 -2
  60. package/packages/model-runtime/src/upstage/index.test.ts +1 -2
  61. package/packages/model-runtime/src/utils/openaiCompatibleFactory/index.test.ts +9 -7
  62. package/packages/model-runtime/src/utils/streams/anthropic.ts +2 -2
  63. package/packages/model-runtime/src/utils/streams/openai/openai.ts +20 -13
  64. package/packages/model-runtime/src/utils/streams/openai/responsesStream.test.ts +1 -2
  65. package/packages/model-runtime/src/utils/streams/openai/responsesStream.ts +2 -2
  66. package/packages/model-runtime/src/utils/streams/protocol.ts +2 -2
  67. package/packages/model-runtime/src/wenxin/index.test.ts +2 -3
  68. package/packages/model-runtime/src/xai/index.test.ts +2 -2
  69. package/packages/model-runtime/src/zeroone/index.test.ts +1 -2
  70. package/packages/model-runtime/src/zhipu/index.test.ts +2 -3
  71. package/packages/model-runtime/vitest.config.mts +0 -7
  72. package/packages/types/src/index.ts +2 -0
  73. package/packages/types/src/message/base.ts +1 -1
  74. package/packages/types/src/openai/chat.ts +2 -3
  75. package/packages/utils/package.json +2 -1
  76. package/packages/utils/src/_deprecated/parseModels.test.ts +1 -1
  77. package/packages/utils/src/_deprecated/parseModels.ts +1 -1
  78. package/packages/utils/src/client/topic.test.ts +1 -2
  79. package/packages/utils/src/client/topic.ts +1 -2
  80. package/packages/utils/src/electron/desktopRemoteRPCFetch.ts +1 -1
  81. package/packages/utils/src/fetch/fetchSSE.ts +7 -8
  82. package/packages/utils/src/fetch/parseError.ts +1 -3
  83. package/packages/utils/src/format.test.ts +1 -2
  84. package/packages/utils/src/index.ts +1 -0
  85. package/packages/utils/src/toolManifest.ts +1 -2
  86. package/packages/utils/src/trace.ts +1 -1
  87. package/packages/utils/vitest.config.mts +1 -1
  88. package/packages/web-crawler/src/__tests__/urlRules.test.ts +275 -0
  89. package/packages/web-crawler/src/crawImpl/__tests__/exa.test.ts +269 -0
  90. package/packages/web-crawler/src/crawImpl/__tests__/firecrawl.test.ts +284 -0
  91. package/packages/web-crawler/src/crawImpl/__tests__/naive.test.ts +234 -0
  92. package/packages/web-crawler/src/crawImpl/__tests__/tavily.test.ts +359 -0
  93. package/packages/web-crawler/src/utils/__tests__/errorType.test.ts +217 -0
  94. package/packages/web-crawler/vitest.config.mts +3 -0
  95. package/scripts/electronWorkflow/mergeMacReleaseFiles.ts +207 -0
  96. package/src/components/Thinking/index.tsx +2 -3
  97. package/src/features/ChatInput/StoreUpdater.tsx +2 -0
  98. package/src/libs/traces/index.ts +1 -1
  99. package/src/server/modules/ModelRuntime/trace.ts +1 -2
  100. package/packages/model-runtime/src/openrouter/__snapshots__/index.test.ts.snap +0 -113
@@ -2,7 +2,7 @@ import fs from 'node:fs';
2
2
  import net from 'node:net';
3
3
  import os from 'node:os';
4
4
  import path from 'node:path';
5
- import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+ import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
6
6
 
7
7
  import { ElectronIpcClient } from './ipcClient';
8
8
 
@@ -14,6 +14,16 @@ vi.mock('node:path');
14
14
 
15
15
  const appId = 'lobehub';
16
16
  describe('ElectronIpcClient', () => {
17
+ // Swallow unhandledRejection during timeout tests to avoid Vitest global capture
18
+ const onUnhandled = (/* reason, promise */) => {};
19
+
20
+ beforeAll(() => {
21
+ process.on('unhandledRejection', onUnhandled);
22
+ });
23
+
24
+ afterAll(() => {
25
+ process.off('unhandledRejection', onUnhandled);
26
+ });
17
27
  // Mock data
18
28
  const mockTempDir = '/mock/temp/dir';
19
29
  const mockSocketInfoPath = '/mock/temp/dir/lobehub-electron-ipc-info.json';
@@ -41,6 +51,10 @@ describe('ElectronIpcClient', () => {
41
51
  // Mock console methods
42
52
  vi.spyOn(console, 'error').mockImplementation(() => {});
43
53
  vi.spyOn(console, 'log').mockImplementation(() => {});
54
+
55
+ // Mock timers
56
+ vi.spyOn(global, 'setTimeout');
57
+ vi.spyOn(global, 'clearTimeout');
44
58
  });
45
59
 
46
60
  afterEach(() => {
@@ -155,6 +169,10 @@ describe('ElectronIpcClient', () => {
155
169
 
156
170
  // Start request
157
171
  const requestPromise = client.sendRequest('getDatabasePath');
172
+ // Prevent unhandled rejection warnings before assertion
173
+ void requestPromise.catch(() => {});
174
+ // Prevent unhandled rejection warnings before assertion
175
+ void requestPromise.catch(() => {});
158
176
 
159
177
  // Simulate connection established
160
178
  if (connectionCallback) connectionCallback();
@@ -162,6 +180,729 @@ describe('ElectronIpcClient', () => {
162
180
  // Now await the promise
163
181
  await expect(requestPromise).rejects.toThrow('Write error');
164
182
  });
183
+
184
+ it('should handle successful request-response cycle', async () => {
185
+ // Setup connection callback
186
+ let connectionCallback: Function | undefined;
187
+ let dataCallback: Function | undefined;
188
+
189
+ vi.mocked(net.createConnection).mockImplementation((path, callback) => {
190
+ connectionCallback = callback as Function;
191
+ return mockSocket as unknown as net.Socket;
192
+ });
193
+
194
+ mockSocket.on.mockImplementation((event, callback) => {
195
+ if (event === 'data') {
196
+ dataCallback = callback as Function;
197
+ }
198
+ return mockSocket;
199
+ });
200
+
201
+ // Mock write to immediately call success callback
202
+ mockSocket.write.mockImplementation((data, callback) => {
203
+ if (callback) {
204
+ // Call success callback synchronously
205
+ setTimeout(() => callback(), 0);
206
+ }
207
+ return true;
208
+ });
209
+
210
+ // Start request
211
+ const requestPromise = client.sendRequest('getDatabasePath', { test: 'param' });
212
+
213
+ // Immediately resolve connection
214
+ if (connectionCallback) connectionCallback();
215
+
216
+ // Process all pending promises
217
+ await new Promise((resolve) => process.nextTick(resolve));
218
+
219
+ // Verify request was written
220
+ expect(mockSocket.write).toHaveBeenCalled();
221
+ const writeCall = mockSocket.write.mock.calls[0];
222
+ const request = JSON.parse(writeCall[0] as string);
223
+ expect(request).toMatchObject({
224
+ method: 'getDatabasePath',
225
+ params: { test: 'param' },
226
+ });
227
+
228
+ // Simulate server response immediately
229
+ const response = {
230
+ id: request.id,
231
+ result: '/path/to/database',
232
+ };
233
+
234
+ if (dataCallback) {
235
+ dataCallback(Buffer.from(JSON.stringify(response) + '\n'));
236
+ }
237
+
238
+ // Verify promise resolves with result
239
+ const result = await requestPromise;
240
+ expect(result).toBe('/path/to/database');
241
+ });
242
+
243
+ it('should handle server error responses', async () => {
244
+ // Setup connection and data callbacks
245
+ let connectionCallback: Function | undefined;
246
+ let dataCallback: Function | undefined;
247
+
248
+ vi.mocked(net.createConnection).mockImplementation((path, callback) => {
249
+ connectionCallback = callback as Function;
250
+ return mockSocket as unknown as net.Socket;
251
+ });
252
+
253
+ mockSocket.on.mockImplementation((event, callback) => {
254
+ if (event === 'data') {
255
+ dataCallback = callback as Function;
256
+ }
257
+ return mockSocket;
258
+ });
259
+
260
+ // Mock write to immediately call success callback
261
+ mockSocket.write.mockImplementation((data, callback) => {
262
+ if (callback) {
263
+ setTimeout(() => callback(), 0);
264
+ }
265
+ return true;
266
+ });
267
+
268
+ // Start request
269
+ const requestPromise = client.sendRequest('getDatabasePath');
270
+ // Prevent unhandled rejection warnings before assertion
271
+ void requestPromise.catch(() => {});
272
+ // Prevent unhandled rejection warnings before assertion
273
+ void requestPromise.catch(() => {});
274
+
275
+ // Immediately resolve connection
276
+ if (connectionCallback) connectionCallback();
277
+
278
+ // Process all pending promises
279
+ await new Promise((resolve) => process.nextTick(resolve));
280
+
281
+ const writeCall = mockSocket.write.mock.calls[0];
282
+ const request = JSON.parse(writeCall[0] as string);
283
+
284
+ // Simulate server error response
285
+ const errorResponse = {
286
+ id: request.id,
287
+ error: 'Database not found',
288
+ };
289
+
290
+ if (dataCallback) {
291
+ dataCallback(Buffer.from(JSON.stringify(errorResponse) + '\n'));
292
+ }
293
+
294
+ // Verify promise rejects with error
295
+ await expect(requestPromise).rejects.toThrow('Database not found');
296
+ });
297
+
298
+ it('should handle multiple messages in single data chunk', async () => {
299
+ let connectionCallback: Function | undefined;
300
+ let dataCallback: Function | undefined;
301
+
302
+ vi.mocked(net.createConnection).mockImplementation((path, callback) => {
303
+ connectionCallback = callback as Function;
304
+ return mockSocket as unknown as net.Socket;
305
+ });
306
+
307
+ mockSocket.on.mockImplementation((event, callback) => {
308
+ if (event === 'data') {
309
+ dataCallback = callback as Function;
310
+ }
311
+ return mockSocket;
312
+ });
313
+
314
+ // Mock write to immediately call success callback
315
+ mockSocket.write.mockImplementation((data, callback) => {
316
+ if (callback) {
317
+ setTimeout(() => callback(), 0);
318
+ }
319
+ return true;
320
+ });
321
+
322
+ // Start multiple requests
323
+ const request1Promise = client.sendRequest('getDatabasePath');
324
+
325
+ // Immediately resolve connection
326
+ if (connectionCallback) connectionCallback();
327
+
328
+ // Process all pending promises
329
+ await new Promise((resolve) => process.nextTick(resolve));
330
+
331
+ const request2Promise = client.sendRequest('getUserDataPath');
332
+
333
+ // Process second request
334
+ await new Promise((resolve) => process.nextTick(resolve));
335
+
336
+ // Get request IDs
337
+ const request1 = JSON.parse(mockSocket.write.mock.calls[0][0] as string);
338
+ const request2 = JSON.parse(mockSocket.write.mock.calls[1][0] as string);
339
+
340
+ // Simulate multiple responses in single data chunk
341
+ const response1 = { id: request1.id, result: '/db/path' };
342
+ const response2 = { id: request2.id, result: '/user/path' };
343
+
344
+ const combinedData = JSON.stringify(response1) + '\n' + JSON.stringify(response2) + '\n';
345
+
346
+ if (dataCallback) {
347
+ dataCallback(Buffer.from(combinedData));
348
+ }
349
+
350
+ // Both promises should resolve
351
+ const result1 = await request1Promise;
352
+ const result2 = await request2Promise;
353
+ expect(result1).toBe('/db/path');
354
+ expect(result2).toBe('/user/path');
355
+ });
356
+
357
+ it('should handle fragmented messages', async () => {
358
+ let connectionCallback: Function | undefined;
359
+ let dataCallback: Function | undefined;
360
+
361
+ vi.mocked(net.createConnection).mockImplementation((path, callback) => {
362
+ connectionCallback = callback as Function;
363
+ return mockSocket as unknown as net.Socket;
364
+ });
365
+
366
+ mockSocket.on.mockImplementation((event, callback) => {
367
+ if (event === 'data') {
368
+ dataCallback = callback as Function;
369
+ }
370
+ return mockSocket;
371
+ });
372
+
373
+ // Mock write to immediately call success callback
374
+ mockSocket.write.mockImplementation((data, callback) => {
375
+ if (callback) {
376
+ setTimeout(() => callback(), 0);
377
+ }
378
+ return true;
379
+ });
380
+
381
+ // Start request
382
+ const requestPromise = client.sendRequest('getDatabasePath');
383
+ // Prevent unhandled rejection warnings before assertion
384
+ void requestPromise.catch(() => {});
385
+ // Prevent unhandled rejection warnings before assertion
386
+ void requestPromise.catch(() => {});
387
+
388
+ // Immediately resolve connection
389
+ if (connectionCallback) connectionCallback();
390
+
391
+ // Process all pending promises
392
+ await new Promise((resolve) => process.nextTick(resolve));
393
+
394
+ const request = JSON.parse(mockSocket.write.mock.calls[0][0] as string);
395
+ const response = JSON.stringify({ id: request.id, result: '/database/path' }) + '\n';
396
+
397
+ // Send response in fragments
398
+ const fragment1 = response.slice(0, 20);
399
+ const fragment2 = response.slice(20);
400
+
401
+ if (dataCallback) {
402
+ // First fragment - should not resolve yet
403
+ dataCallback(Buffer.from(fragment1));
404
+
405
+ // Second fragment - should complete and resolve
406
+ dataCallback(Buffer.from(fragment2));
407
+ }
408
+
409
+ const result = await requestPromise;
410
+ expect(result).toBe('/database/path');
411
+ });
412
+
413
+ it('should handle request timeout', async () => {
414
+ let connectionCallback: Function | undefined;
415
+
416
+ vi.mocked(net.createConnection).mockImplementation((path, callback) => {
417
+ connectionCallback = callback as Function;
418
+ return mockSocket as unknown as net.Socket;
419
+ });
420
+
421
+ // Start request
422
+ const requestPromise = client.sendRequest('getDatabasePath');
423
+
424
+ // Allow promise to start
425
+ await vi.runAllTimersAsync();
426
+
427
+ if (connectionCallback) connectionCallback();
428
+
429
+ // Allow connection to be processed
430
+ await vi.runAllTimersAsync();
431
+
432
+ // Prepare assertion before advancing timers to avoid late-attached handlers
433
+ const expectReject = expect(requestPromise).rejects.toThrow(
434
+ 'Request timed out, method: getDatabasePath',
435
+ );
436
+ // Fast-forward time by 5000ms to trigger timeout
437
+ vi.advanceTimersByTime(5000);
438
+ // Run timer callbacks
439
+ await vi.runAllTimersAsync();
440
+ // Request should timeout
441
+ await expectReject;
442
+ }, 10000);
443
+
444
+ it('should handle socket close event', async () => {
445
+ let connectionCallback: Function | undefined;
446
+ let closeCallback: Function | undefined;
447
+
448
+ vi.mocked(net.createConnection).mockImplementation((path, callback) => {
449
+ connectionCallback = callback as Function;
450
+ return mockSocket as unknown as net.Socket;
451
+ });
452
+
453
+ mockSocket.on.mockImplementation((event, callback) => {
454
+ if (event === 'close') {
455
+ closeCallback = callback as Function;
456
+ }
457
+ return mockSocket;
458
+ });
459
+
460
+ // Mock write to immediately call success callback
461
+ mockSocket.write.mockImplementation((data, callback) => {
462
+ if (callback) {
463
+ setTimeout(() => callback(), 0);
464
+ }
465
+ return true;
466
+ });
467
+
468
+ // Start request
469
+ const requestPromise = client.sendRequest('getDatabasePath');
470
+
471
+ // Immediately resolve connection
472
+ if (connectionCallback) connectionCallback();
473
+
474
+ // Process all pending promises
475
+ await new Promise((resolve) => process.nextTick(resolve));
476
+
477
+ // Simulate socket close
478
+ if (closeCallback) closeCallback();
479
+
480
+ // Request should be rejected due to connection loss
481
+ await expect(requestPromise).rejects.toThrow('Connection to Electron IPC server lost');
482
+ });
483
+
484
+ it('should handle malformed JSON responses', async () => {
485
+ let connectionCallback: Function | undefined;
486
+ let dataCallback: Function | undefined;
487
+
488
+ vi.mocked(net.createConnection).mockImplementation((path, callback) => {
489
+ connectionCallback = callback as Function;
490
+ return mockSocket as unknown as net.Socket;
491
+ });
492
+
493
+ mockSocket.on.mockImplementation((event, callback) => {
494
+ if (event === 'data') {
495
+ dataCallback = callback as Function;
496
+ }
497
+ return mockSocket;
498
+ });
499
+
500
+ // Start request
501
+ const requestPromise = client.sendRequest('getDatabasePath');
502
+
503
+ // Allow request to start
504
+ await vi.runAllTimersAsync();
505
+
506
+ if (connectionCallback) connectionCallback();
507
+
508
+ // Allow connection to be processed
509
+ await vi.runAllTimersAsync();
510
+
511
+ // Send malformed JSON
512
+ if (dataCallback) {
513
+ dataCallback(Buffer.from('invalid json\n'));
514
+ }
515
+
516
+ // Allow malformed data to be processed
517
+ await vi.runAllTimersAsync();
518
+
519
+ // Should log error but not crash
520
+ expect(console.error).toHaveBeenCalledWith(
521
+ expect.stringContaining('Failed to parse response'),
522
+ expect.anything(),
523
+ 'invalid json',
524
+ );
525
+
526
+ // Original request should still timeout normally
527
+ const expectReject = expect(requestPromise).rejects.toThrow('Request timed out');
528
+ vi.advanceTimersByTime(5000);
529
+ await vi.runAllTimersAsync();
530
+ await expectReject;
531
+ }, 10000);
532
+
533
+ it('should handle response for unknown request ID', async () => {
534
+ let connectionCallback: Function | undefined;
535
+ let dataCallback: Function | undefined;
536
+
537
+ vi.mocked(net.createConnection).mockImplementation((path, callback) => {
538
+ connectionCallback = callback as Function;
539
+ return mockSocket as unknown as net.Socket;
540
+ });
541
+
542
+ mockSocket.on.mockImplementation((event, callback) => {
543
+ if (event === 'data') {
544
+ dataCallback = callback as Function;
545
+ }
546
+ return mockSocket;
547
+ });
548
+
549
+ // Start request
550
+ const requestPromise = client.sendRequest('getDatabasePath');
551
+
552
+ // Allow request to start
553
+ await vi.runAllTimersAsync();
554
+
555
+ if (connectionCallback) connectionCallback();
556
+
557
+ // Allow connection to be processed
558
+ await vi.runAllTimersAsync();
559
+
560
+ // Send response with unknown ID
561
+ const unknownResponse = {
562
+ id: 'unknown-id',
563
+ result: 'some result',
564
+ };
565
+
566
+ if (dataCallback) {
567
+ dataCallback(Buffer.from(JSON.stringify(unknownResponse) + '\n'));
568
+ }
569
+
570
+ // Allow unknown response to be processed
571
+ await vi.runAllTimersAsync();
572
+
573
+ // Should handle gracefully without crashing
574
+ vi.advanceTimersByTime(100); // Small delay
575
+ await vi.runAllTimersAsync();
576
+
577
+ // Original request should still timeout
578
+ const expectReject = expect(requestPromise).rejects.toThrow('Request timed out');
579
+ vi.advanceTimersByTime(5000);
580
+ await vi.runAllTimersAsync();
581
+ await expectReject;
582
+ }, 10000);
583
+
584
+ it('should skip empty messages', async () => {
585
+ let connectionCallback: Function | undefined;
586
+ let dataCallback: Function | undefined;
587
+
588
+ vi.mocked(net.createConnection).mockImplementation((path, callback) => {
589
+ connectionCallback = callback as Function;
590
+ return mockSocket as unknown as net.Socket;
591
+ });
592
+
593
+ mockSocket.on.mockImplementation((event, callback) => {
594
+ if (event === 'data') {
595
+ dataCallback = callback as Function;
596
+ }
597
+ return mockSocket;
598
+ });
599
+
600
+ // Mock write to immediately call success callback
601
+ mockSocket.write.mockImplementation((data, callback) => {
602
+ if (callback) {
603
+ setTimeout(() => callback(), 0);
604
+ }
605
+ return true;
606
+ });
607
+
608
+ // Start request
609
+ const requestPromise = client.sendRequest('getDatabasePath');
610
+
611
+ // Immediately resolve connection
612
+ if (connectionCallback) connectionCallback();
613
+
614
+ // Process all pending promises
615
+ await new Promise((resolve) => process.nextTick(resolve));
616
+
617
+ const request = JSON.parse(mockSocket.write.mock.calls[0][0] as string);
618
+
619
+ // Send data with empty lines and valid response
620
+ const dataWithEmptyLines =
621
+ '\n\n\n' + JSON.stringify({ id: request.id, result: '/path' }) + '\n';
622
+
623
+ if (dataCallback) {
624
+ dataCallback(Buffer.from(dataWithEmptyLines));
625
+ }
626
+
627
+ const result = await requestPromise;
628
+ expect(result).toBe('/path');
629
+ });
630
+
631
+ it('should handle connection attempt on already connected client', async () => {
632
+ let connectionCallback: Function | undefined;
633
+
634
+ vi.mocked(net.createConnection).mockImplementation((path, callback) => {
635
+ connectionCallback = callback as Function;
636
+ return mockSocket as unknown as net.Socket;
637
+ });
638
+
639
+ // First request - establishes connection
640
+ const request1Promise = client.sendRequest('getDatabasePath');
641
+ // Prevent unhandled rejection warnings before assertion
642
+ void request1Promise.catch(() => {});
643
+
644
+ // Allow first request to start
645
+ await vi.runAllTimersAsync();
646
+
647
+ if (connectionCallback) connectionCallback();
648
+
649
+ // Allow connection to be processed
650
+ await vi.runAllTimersAsync();
651
+
652
+ // Second request - should reuse existing connection
653
+ const request2Promise = client.sendRequest('getUserDataPath');
654
+ // Prevent unhandled rejection warnings before assertion
655
+ void request2Promise.catch(() => {});
656
+
657
+ // Allow second request to be processed
658
+ await vi.runAllTimersAsync();
659
+
660
+ // net.createConnection should only be called once
661
+ expect(net.createConnection).toHaveBeenCalledTimes(1);
662
+
663
+ // Clean up promises
664
+ vi.advanceTimersByTime(5000);
665
+ await vi.runAllTimersAsync();
666
+ await expect(request1Promise).rejects.toThrow('Request timed out');
667
+ await expect(request2Promise).rejects.toThrow('Request timed out');
668
+ }, 10000);
669
+ });
670
+
671
+ describe('reconnection logic', () => {
672
+ let client: ElectronIpcClient;
673
+
674
+ beforeEach(() => {
675
+ // Setup a client with a known socket path
676
+ vi.mocked(fs.existsSync).mockReturnValue(true);
677
+ vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockSocketInfo));
678
+ client = new ElectronIpcClient(appId);
679
+ });
680
+
681
+ it('should attempt reconnection after connection loss', async () => {
682
+ let connectionCallback: Function | undefined;
683
+ let errorCallback: Function | undefined;
684
+
685
+ vi.mocked(net.createConnection).mockImplementation((path, callback) => {
686
+ connectionCallback = callback as Function;
687
+ return mockSocket as unknown as net.Socket;
688
+ });
689
+
690
+ mockSocket.on.mockImplementation((event, callback) => {
691
+ if (event === 'error') {
692
+ errorCallback = callback as Function;
693
+ }
694
+ return mockSocket;
695
+ });
696
+
697
+ // Mock write to immediately call success callback
698
+ mockSocket.write.mockImplementation((data, callback) => {
699
+ if (callback) {
700
+ setTimeout(() => callback(), 0);
701
+ }
702
+ return true;
703
+ });
704
+
705
+ // Start request
706
+ const requestPromise = client.sendRequest('getDatabasePath');
707
+
708
+ // Immediately resolve connection
709
+ if (connectionCallback) connectionCallback();
710
+
711
+ // Process all pending promises
712
+ await new Promise((resolve) => process.nextTick(resolve));
713
+
714
+ // Simulate connection error
715
+ if (errorCallback) {
716
+ errorCallback(new Error('Connection lost'));
717
+ }
718
+
719
+ // Should schedule reconnection
720
+ expect(vi.mocked(setTimeout)).toHaveBeenCalled();
721
+
722
+ await expect(requestPromise).rejects.toThrow('Connection to Electron IPC server lost');
723
+ });
724
+
725
+ it('should give up after max reconnection attempts', async () => {
726
+ let connectionCallback: Function | undefined;
727
+ let errorCallback: Function | undefined;
728
+
729
+ // Mock multiple connection attempts that fail
730
+ vi.mocked(net.createConnection).mockImplementation((path, callback) => {
731
+ connectionCallback = callback as Function;
732
+ return mockSocket as unknown as net.Socket;
733
+ });
734
+
735
+ mockSocket.on.mockImplementation((event, callback) => {
736
+ if (event === 'error') {
737
+ errorCallback = callback as Function;
738
+ }
739
+ return mockSocket;
740
+ });
741
+
742
+ // Mock write to immediately call success callback
743
+ mockSocket.write.mockImplementation((data, callback) => {
744
+ if (callback) {
745
+ setTimeout(() => callback(), 0);
746
+ }
747
+ return true;
748
+ });
749
+
750
+ // Start request
751
+ const requestPromise = client.sendRequest('getDatabasePath');
752
+
753
+ // Immediately resolve connection
754
+ if (connectionCallback) connectionCallback();
755
+
756
+ // Process all pending promises
757
+ await new Promise((resolve) => process.nextTick(resolve));
758
+
759
+ if (errorCallback) {
760
+ errorCallback(new Error('Connection failed 1'));
761
+ }
762
+
763
+ await expect(requestPromise).rejects.toThrow('Connection to Electron IPC server lost');
764
+ expect(vi.mocked(setTimeout)).toHaveBeenCalled();
765
+ });
766
+
767
+ it('should clear existing reconnect timeout when handling new disconnect', async () => {
768
+ // This test verifies that clearTimeout is called during client close
769
+ let connectionCallback: Function | undefined;
770
+ let errorCallback: Function | undefined;
771
+
772
+ vi.mocked(net.createConnection).mockImplementation((path, callback) => {
773
+ connectionCallback = callback as Function;
774
+ return mockSocket as unknown as net.Socket;
775
+ });
776
+
777
+ mockSocket.on.mockImplementation((event, callback) => {
778
+ if (event === 'error') {
779
+ errorCallback = callback as Function;
780
+ }
781
+ return mockSocket;
782
+ });
783
+
784
+ // Mock write to immediately call success callback
785
+ mockSocket.write.mockImplementation((data, callback) => {
786
+ if (callback) {
787
+ setTimeout(() => callback(), 0);
788
+ }
789
+ return true;
790
+ });
791
+
792
+ // Start request
793
+ const requestPromise = client.sendRequest('getDatabasePath').catch(() => {});
794
+
795
+ // Immediately resolve connection
796
+ if (connectionCallback) connectionCallback();
797
+
798
+ // Process setup
799
+ await new Promise((resolve) => process.nextTick(resolve));
800
+
801
+ // Simulate error to trigger reconnection setup
802
+ if (errorCallback) {
803
+ errorCallback(new Error('First error'));
804
+ }
805
+
806
+ // Close client to trigger clearTimeout
807
+ client.close();
808
+
809
+ // clearTimeout should be called
810
+ expect(vi.mocked(clearTimeout)).toHaveBeenCalled();
811
+ });
812
+ });
813
+
814
+ describe('error scenarios', () => {
815
+ let client: ElectronIpcClient;
816
+
817
+ beforeEach(() => {
818
+ vi.mocked(fs.existsSync).mockReturnValue(true);
819
+ vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify(mockSocketInfo));
820
+ client = new ElectronIpcClient(appId);
821
+ });
822
+
823
+ // Note: socketPath null test skipped due to complexity with async error handling
824
+
825
+ it('should handle connection creation failure', async () => {
826
+ vi.mocked(net.createConnection).mockImplementation(() => {
827
+ throw new Error('Failed to create connection');
828
+ });
829
+
830
+ const requestPromise = client.sendRequest('getDatabasePath');
831
+
832
+ await expect(requestPromise).rejects.toThrow('Failed to create connection');
833
+ expect(console.error).toHaveBeenCalledWith(
834
+ 'Failed to connect to IPC server: %o',
835
+ expect.any(Error),
836
+ );
837
+ });
838
+
839
+ it('should handle JSON stringify error in sendRequest', async () => {
840
+ let connectionCallback: Function | undefined;
841
+
842
+ vi.mocked(net.createConnection).mockImplementation((path, callback) => {
843
+ connectionCallback = callback as Function;
844
+ return mockSocket as unknown as net.Socket;
845
+ });
846
+
847
+ // Create a circular reference object that will cause JSON.stringify to fail
848
+ const circularParams: any = { prop: 'value' };
849
+ circularParams.circular = circularParams;
850
+
851
+ // Start request with circular reference
852
+ const requestPromise = client.sendRequest('getDatabasePath', circularParams);
853
+ if (connectionCallback) connectionCallback();
854
+
855
+ await expect(requestPromise).rejects.toThrow();
856
+ expect(console.error).toHaveBeenCalledWith(
857
+ 'Error sending request (during setup/write phase): %o',
858
+ expect.any(Error),
859
+ );
860
+ });
861
+
862
+ it('should handle write failure without pending request', async () => {
863
+ let connectionCallback: Function | undefined;
864
+
865
+ vi.mocked(net.createConnection).mockImplementation((path, callback) => {
866
+ connectionCallback = callback as Function;
867
+ return mockSocket as unknown as net.Socket;
868
+ });
869
+
870
+ // Mock write to fail after clearing the request from queue
871
+ let writeCallback: Function | undefined;
872
+ mockSocket.write.mockImplementation((data, callback) => {
873
+ writeCallback = callback as Function;
874
+ return true;
875
+ });
876
+
877
+ // Start request
878
+ const requestPromise = client.sendRequest('getDatabasePath').catch(() => {});
879
+
880
+ // Allow request to start
881
+ await vi.runAllTimersAsync();
882
+
883
+ if (connectionCallback) connectionCallback();
884
+
885
+ // Allow connection to be processed
886
+ await vi.runAllTimersAsync();
887
+
888
+ // Manually clear the request queue to simulate timing issue
889
+ (client as any).requestQueue.clear();
890
+
891
+ // Now trigger write callback with error
892
+ if (writeCallback) {
893
+ writeCallback(new Error('Write failed'));
894
+ }
895
+
896
+ // Allow error to be processed
897
+ await vi.runAllTimersAsync();
898
+
899
+ // Should handle gracefully - note the error is logged but no exception is thrown
900
+ // because the request is no longer in the queue
901
+ expect(console.error).toHaveBeenCalledWith(
902
+ 'Failed to write request to socket: %o',
903
+ expect.any(Error),
904
+ );
905
+ }, 10000);
165
906
  });
166
907
 
167
908
  describe('close method', () => {
@@ -207,5 +948,46 @@ describe('ElectronIpcClient', () => {
207
948
  // Verify no errors
208
949
  expect(mockSocket.end).not.toHaveBeenCalled();
209
950
  });
951
+
952
+ it('should clear reconnect timeout when closing', async () => {
953
+ let connectionCallback: Function | undefined;
954
+ let errorCallback: Function | undefined;
955
+
956
+ vi.mocked(net.createConnection).mockImplementation((path, callback) => {
957
+ connectionCallback = callback as Function;
958
+ return mockSocket as unknown as net.Socket;
959
+ });
960
+
961
+ mockSocket.on.mockImplementation((event, callback) => {
962
+ if (event === 'error') {
963
+ errorCallback = callback as Function;
964
+ }
965
+ return mockSocket;
966
+ });
967
+
968
+ // Start request and trigger error to set up reconnection
969
+ const requestPromise = client.sendRequest('getDatabasePath').catch(() => {});
970
+
971
+ // Allow promise to start
972
+ await vi.runAllTimersAsync();
973
+
974
+ if (connectionCallback) connectionCallback();
975
+
976
+ // Allow connection to be processed
977
+ await vi.runAllTimersAsync();
978
+
979
+ if (errorCallback) {
980
+ errorCallback(new Error('Test error'));
981
+ }
982
+
983
+ // Allow error to be processed
984
+ await vi.runAllTimersAsync();
985
+
986
+ // Close the client
987
+ client.close();
988
+
989
+ // Should clear timeout
990
+ expect(vi.mocked(clearTimeout)).toHaveBeenCalled();
991
+ }, 10000);
210
992
  });
211
993
  });