@lobehub/chat 1.124.1 → 1.124.3
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.
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/package.json +1 -1
- package/packages/const/package.json +3 -1
- package/packages/const/src/analytics.ts +1 -1
- package/packages/const/src/desktop.ts +3 -2
- package/packages/const/src/discover.ts +3 -2
- package/packages/const/src/guide.ts +2 -2
- package/packages/const/src/hotkeys.ts +1 -1
- package/packages/const/src/index.ts +2 -0
- package/packages/const/src/settings/common.ts +1 -1
- package/packages/const/src/settings/genUserLLMConfig.test.ts +1 -2
- package/packages/const/src/settings/genUserLLMConfig.ts +1 -2
- package/packages/const/src/settings/hotkey.ts +3 -2
- package/packages/const/src/settings/index.ts +1 -3
- package/packages/const/src/settings/knowledge.ts +1 -1
- package/packages/const/src/settings/llm.ts +2 -4
- package/packages/const/src/settings/systemAgent.ts +1 -5
- package/packages/const/src/settings/tts.ts +1 -1
- package/packages/const/src/trace.ts +1 -1
- package/packages/const/src/url.ts +2 -2
- package/packages/const/src/user.ts +1 -2
- package/packages/database/src/client/db.test.ts +19 -13
- package/packages/electron-server-ipc/src/ipcClient.test.ts +783 -1
- package/packages/file-loaders/src/loadFile.test.ts +61 -0
- package/packages/file-loaders/src/utils/isTextReadableFile.test.ts +43 -0
- package/packages/file-loaders/src/utils/parser-utils.test.ts +155 -0
- package/packages/model-runtime/package.json +2 -1
- package/packages/model-runtime/src/ai21/index.test.ts +2 -2
- package/packages/model-runtime/src/ai360/index.test.ts +2 -2
- package/packages/model-runtime/src/akashchat/index.test.ts +19 -0
- package/packages/model-runtime/src/anthropic/index.test.ts +1 -2
- package/packages/model-runtime/src/baichuan/index.test.ts +1 -2
- package/packages/model-runtime/src/bedrock/index.test.ts +1 -2
- package/packages/model-runtime/src/bfl/createImage.test.ts +1 -2
- package/packages/model-runtime/src/bfl/index.test.ts +1 -2
- package/packages/model-runtime/src/cloudflare/index.test.ts +1 -2
- package/packages/model-runtime/src/cohere/index.test.ts +19 -0
- package/packages/model-runtime/src/deepseek/index.test.ts +2 -2
- package/packages/model-runtime/src/fireworksai/index.test.ts +2 -2
- package/packages/model-runtime/src/giteeai/index.test.ts +2 -2
- package/packages/model-runtime/src/github/index.test.ts +2 -2
- package/packages/model-runtime/src/google/createImage.test.ts +1 -2
- package/packages/model-runtime/src/google/index.test.ts +1 -1
- package/packages/model-runtime/src/groq/index.test.ts +2 -3
- package/packages/model-runtime/src/higress/index.ts +2 -3
- package/packages/model-runtime/src/huggingface/index.test.ts +40 -0
- package/packages/model-runtime/src/hunyuan/index.test.ts +2 -3
- package/packages/model-runtime/src/internlm/index.test.ts +2 -2
- package/packages/model-runtime/src/jina/index.test.ts +19 -0
- package/packages/model-runtime/src/lmstudio/index.test.ts +2 -2
- package/packages/model-runtime/src/minimax/index.test.ts +19 -0
- package/packages/model-runtime/src/mistral/index.test.ts +2 -3
- package/packages/model-runtime/src/modelscope/index.test.ts +19 -0
- package/packages/model-runtime/src/moonshot/index.test.ts +1 -2
- package/packages/model-runtime/src/nebius/index.test.ts +19 -0
- package/packages/model-runtime/src/novita/index.test.ts +3 -4
- package/packages/model-runtime/src/nvidia/index.test.ts +19 -0
- package/packages/model-runtime/src/openrouter/index.test.ts +2 -3
- package/packages/model-runtime/src/perplexity/index.test.ts +2 -3
- package/packages/model-runtime/src/ppio/index.test.ts +3 -4
- package/packages/model-runtime/src/qwen/index.test.ts +2 -2
- package/packages/model-runtime/src/sambanova/index.test.ts +19 -0
- package/packages/model-runtime/src/search1api/index.test.ts +19 -0
- package/packages/model-runtime/src/sensenova/index.test.ts +2 -2
- package/packages/model-runtime/src/spark/index.test.ts +2 -2
- package/packages/model-runtime/src/stepfun/index.test.ts +2 -2
- package/packages/model-runtime/src/taichu/index.test.ts +4 -5
- package/packages/model-runtime/src/tencentcloud/index.test.ts +1 -1
- package/packages/model-runtime/src/togetherai/index.test.ts +1 -2
- package/packages/model-runtime/src/upstage/index.test.ts +1 -2
- package/packages/model-runtime/src/utils/openaiCompatibleFactory/index.test.ts +9 -7
- package/packages/model-runtime/src/utils/streams/anthropic.ts +2 -2
- package/packages/model-runtime/src/utils/streams/openai/openai.ts +20 -13
- package/packages/model-runtime/src/utils/streams/openai/responsesStream.test.ts +1 -2
- package/packages/model-runtime/src/utils/streams/openai/responsesStream.ts +2 -2
- package/packages/model-runtime/src/utils/streams/protocol.ts +2 -2
- package/packages/model-runtime/src/wenxin/index.test.ts +2 -3
- package/packages/model-runtime/src/xai/index.test.ts +2 -2
- package/packages/model-runtime/src/zeroone/index.test.ts +1 -2
- package/packages/model-runtime/src/zhipu/index.test.ts +2 -3
- package/packages/model-runtime/vitest.config.mts +0 -7
- package/packages/types/src/discover/index.ts +0 -8
- package/packages/types/src/index.ts +4 -0
- package/packages/types/src/message/base.ts +1 -1
- package/packages/types/src/openai/chat.ts +2 -3
- package/packages/types/src/tool/index.ts +1 -0
- package/packages/types/src/tool/tool.ts +1 -1
- package/packages/types/src/user/settings/index.ts +1 -2
- package/packages/utils/package.json +2 -1
- package/packages/utils/src/_deprecated/parseModels.test.ts +1 -1
- package/packages/utils/src/_deprecated/parseModels.ts +1 -1
- package/packages/utils/src/client/topic.test.ts +1 -2
- package/packages/utils/src/client/topic.ts +1 -2
- package/packages/utils/src/electron/desktopRemoteRPCFetch.ts +1 -1
- package/packages/utils/src/fetch/fetchSSE.ts +7 -8
- package/packages/utils/src/fetch/parseError.ts +1 -3
- package/packages/utils/src/format.test.ts +1 -2
- package/packages/utils/src/index.ts +1 -0
- package/packages/utils/src/toolManifest.ts +1 -2
- package/packages/utils/src/trace.ts +1 -1
- package/packages/utils/vitest.config.mts +1 -2
- package/packages/web-crawler/src/__tests__/urlRules.test.ts +275 -0
- package/packages/web-crawler/src/crawImpl/__tests__/exa.test.ts +269 -0
- package/packages/web-crawler/src/crawImpl/__tests__/firecrawl.test.ts +284 -0
- package/packages/web-crawler/src/crawImpl/__tests__/naive.test.ts +234 -0
- package/packages/web-crawler/src/crawImpl/__tests__/tavily.test.ts +359 -0
- package/packages/web-crawler/src/utils/__tests__/errorType.test.ts +217 -0
- package/packages/web-crawler/vitest.config.mts +3 -0
- package/src/app/(backend)/webapi/models/[provider]/pull/route.ts +0 -2
- package/src/app/(backend)/webapi/models/[provider]/route.ts +0 -2
- package/src/app/(backend)/webapi/text-to-image/[provider]/route.ts +0 -2
- package/src/app/(backend)/webapi/trace/route.ts +0 -2
- package/src/components/Thinking/index.tsx +2 -3
- package/src/features/ChatInput/StoreUpdater.tsx +2 -0
- package/src/libs/traces/index.ts +1 -1
- package/src/server/modules/ModelRuntime/trace.ts +1 -2
- package/packages/const/src/settings/sync.ts +0 -5
- 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
|
});
|