@lobehub/chat 1.84.24 → 1.84.25

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.
@@ -0,0 +1,417 @@
1
+ import fs from 'node:fs';
2
+ import net from 'node:net';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
+
7
+ import { ElectronIPCServer } from './ipcServer';
8
+
9
+ // Mock node modules
10
+ vi.mock('node:fs');
11
+ vi.mock('node:net');
12
+ vi.mock('node:os');
13
+ vi.mock('node:path');
14
+
15
+ const appId = 'lobehub';
16
+
17
+ describe('ElectronIPCServer', () => {
18
+ // Mock data
19
+ const mockTempDir = '/mock/temp/dir';
20
+ const mockSocketPath = '/mock/temp/dir/lobehub-electron-ipc.sock';
21
+ const mockSocketInfoPath = '/mock/temp/dir/lobehub-electron-ipc-info.json';
22
+
23
+ // Mock server and socket
24
+ const mockServer = {
25
+ on: vi.fn(),
26
+ listen: vi.fn(),
27
+ close: vi.fn(),
28
+ };
29
+
30
+ const mockSocket = {
31
+ on: vi.fn(),
32
+ write: vi.fn(),
33
+ };
34
+
35
+ // Mock event handler
36
+ const mockEventHandler = {
37
+ testMethod: vi.fn(),
38
+ getStaticFilePath: vi.fn(),
39
+ };
40
+
41
+ beforeEach(() => {
42
+ // Reset all mocks
43
+ vi.resetAllMocks();
44
+
45
+ // 使用模拟定时器
46
+ vi.useFakeTimers();
47
+
48
+ // Setup common mocks
49
+ vi.mocked(os.tmpdir).mockReturnValue(mockTempDir);
50
+ vi.mocked(path.join).mockImplementation((...args) => args.join('/'));
51
+ vi.mocked(net.createServer).mockReturnValue(mockServer as unknown as net.Server);
52
+
53
+ // Mock socket path for different platforms
54
+ const originalPlatform = process.platform;
55
+ Object.defineProperty(process, 'platform', { value: 'darwin' });
56
+
57
+ // Mock fs functions
58
+ vi.mocked(fs.existsSync).mockReturnValue(false);
59
+ vi.mocked(fs.unlinkSync).mockReturnValue(undefined);
60
+ vi.mocked(fs.writeFileSync).mockReturnValue(undefined);
61
+
62
+ // Mock console methods
63
+ vi.spyOn(console, 'error').mockImplementation(() => {});
64
+ vi.spyOn(console, 'log').mockImplementation(() => {});
65
+ });
66
+
67
+ afterEach(() => {
68
+ vi.restoreAllMocks();
69
+ vi.useRealTimers();
70
+ });
71
+
72
+ describe('server initialization and start', () => {
73
+ it('should create server and start listening', async () => {
74
+ // Setup
75
+ mockServer.listen.mockImplementation((path, callback) => {
76
+ callback?.();
77
+ return mockServer;
78
+ });
79
+
80
+ // Execute
81
+ const server = new ElectronIPCServer(appId, mockEventHandler as any);
82
+ await server.start();
83
+
84
+ // Verify
85
+ expect(net.createServer).toHaveBeenCalled();
86
+ expect(mockServer.listen).toHaveBeenCalledWith(mockSocketPath, expect.any(Function));
87
+ expect(fs.writeFileSync).toHaveBeenCalledWith(
88
+ mockSocketInfoPath,
89
+ JSON.stringify({ socketPath: mockSocketPath }),
90
+ 'utf8',
91
+ );
92
+ });
93
+
94
+ it('should remove existing socket file if it exists', async () => {
95
+ // Setup
96
+ vi.mocked(fs.existsSync).mockReturnValue(true);
97
+ mockServer.listen.mockImplementation((path, callback) => {
98
+ callback?.();
99
+ return mockServer;
100
+ });
101
+
102
+ // Execute
103
+ const server = new ElectronIPCServer(appId, mockEventHandler as any);
104
+
105
+ // Verify
106
+ expect(fs.existsSync).toHaveBeenCalledWith(mockSocketPath);
107
+ expect(fs.unlinkSync).toHaveBeenCalledWith(mockSocketPath);
108
+ });
109
+
110
+ it('should handle server start error', async () => {
111
+ // Setup
112
+ const mockError = new Error('Server start error');
113
+ mockServer.on.mockImplementation((event, callback) => {
114
+ if (event === 'error') {
115
+ callback(mockError);
116
+ }
117
+ return mockServer;
118
+ });
119
+
120
+ // Execute and verify
121
+ const server = new ElectronIPCServer(appId, mockEventHandler as any);
122
+ await expect(server.start()).rejects.toThrow('Server start error');
123
+ });
124
+ });
125
+
126
+ describe('connection and message handling', () => {
127
+ let server: ElectronIPCServer;
128
+ let connectionHandler: Function;
129
+
130
+ beforeEach(() => {
131
+ // Setup connection handler capture
132
+ mockServer.on.mockReset();
133
+ mockSocket.on.mockReset();
134
+ mockSocket.write.mockReset();
135
+
136
+ vi.mocked(net.createServer).mockImplementation((handler) => {
137
+ connectionHandler = handler as any;
138
+ return mockServer as unknown as net.Server;
139
+ });
140
+
141
+ mockServer.listen.mockImplementation((path, callback) => {
142
+ callback?.();
143
+ return mockServer;
144
+ });
145
+
146
+ // Create server
147
+ server = new ElectronIPCServer(appId, mockEventHandler as any);
148
+ });
149
+
150
+ it('should handle client connection and setup data listeners', async () => {
151
+ // Start server
152
+ await server.start();
153
+
154
+ // Simulate connection
155
+ connectionHandler(mockSocket);
156
+
157
+ // Verify socket listeners setup
158
+ expect(mockSocket.on).toHaveBeenCalledWith('data', expect.any(Function));
159
+ expect(mockSocket.on).toHaveBeenCalledWith('error', expect.any(Function));
160
+ expect(mockSocket.on).toHaveBeenCalledWith('close', expect.any(Function));
161
+ });
162
+
163
+ it('should parse messages with \n separator and execute handler', async () => {
164
+ // Setup mock handler
165
+ mockEventHandler.testMethod.mockResolvedValue('success');
166
+
167
+ // Start server
168
+ await server.start();
169
+
170
+ // Simulate connection
171
+ connectionHandler(mockSocket);
172
+
173
+ // Get data handler
174
+ const dataHandlerCall = mockSocket.on.mock.calls.find((call) => call[0] === 'data');
175
+ expect(dataHandlerCall).toBeDefined();
176
+ const dataHandler = dataHandlerCall![1];
177
+
178
+ // Create test message
179
+ const message =
180
+ JSON.stringify({
181
+ id: 'test-id',
182
+ method: 'testMethod',
183
+ params: { key: 'value' },
184
+ }) + '\n';
185
+
186
+ // Send message
187
+ await dataHandler(Buffer.from(message));
188
+
189
+ // 确保异步处理完成
190
+ await vi.runAllTimersAsync();
191
+
192
+ // Verify handler execution
193
+ expect(mockEventHandler.testMethod).toHaveBeenCalledWith(
194
+ { key: 'value' },
195
+ expect.objectContaining({
196
+ id: 'test-id',
197
+ method: 'testMethod',
198
+ socket: mockSocket,
199
+ }),
200
+ );
201
+
202
+ // 触发服务器端处理程序执行
203
+ const pendingHandlerPromise = mockEventHandler.testMethod.mock.results[0].value;
204
+ await pendingHandlerPromise;
205
+
206
+ // Verify response format with \n\n separator
207
+ expect(mockSocket.write).toHaveBeenCalledWith(
208
+ JSON.stringify({ id: 'test-id', result: 'success' }) + '\n\n',
209
+ );
210
+ });
211
+
212
+ it('should handle multiple messages in single data chunk', async () => {
213
+ // Setup mock handlers with resolved values
214
+ mockEventHandler.testMethod.mockResolvedValue('success1');
215
+ mockEventHandler.getStaticFilePath.mockResolvedValue('path/to/file');
216
+
217
+ // Start server
218
+ await server.start();
219
+
220
+ // Simulate connection
221
+ connectionHandler(mockSocket);
222
+
223
+ // Get data handler
224
+ const dataHandlerCall = mockSocket.on.mock.calls.find((call) => call[0] === 'data');
225
+ expect(dataHandlerCall).toBeDefined();
226
+ const dataHandler = dataHandlerCall![1];
227
+
228
+ // Create multiple messages in one chunk
229
+ const message1 =
230
+ JSON.stringify({
231
+ id: 'id1',
232
+ method: 'testMethod',
233
+ params: { key1: 'value1' },
234
+ }) + '\n\n';
235
+
236
+ const message2 =
237
+ JSON.stringify({
238
+ id: 'id2',
239
+ method: 'getStaticFilePath',
240
+ params: 'path/param',
241
+ }) + '\n\n';
242
+
243
+ // Send combined message
244
+ await dataHandler(Buffer.from(message1 + message2));
245
+
246
+ // 确保异步处理完成
247
+ await vi.runAllTimersAsync();
248
+
249
+ // Verify both handlers were executed
250
+ expect(mockEventHandler.testMethod).toHaveBeenCalledWith(
251
+ { key1: 'value1' },
252
+ expect.objectContaining({ id: 'id1', method: 'testMethod' }),
253
+ );
254
+
255
+ expect(mockEventHandler.getStaticFilePath).toHaveBeenCalledWith(
256
+ 'path/param',
257
+ expect.objectContaining({ id: 'id2', method: 'getStaticFilePath' }),
258
+ );
259
+
260
+ // 等待处理程序完成
261
+ const promise1 = mockEventHandler.testMethod.mock.results[0].value;
262
+ const promise2 = mockEventHandler.getStaticFilePath.mock.results[0].value;
263
+ await Promise.all([promise1, promise2]);
264
+
265
+ // Verify responses
266
+ expect(mockSocket.write).toHaveBeenCalledTimes(2);
267
+ expect(mockSocket.write).toHaveBeenCalledWith(
268
+ JSON.stringify({ id: 'id1', result: 'success1' }) + '\n\n',
269
+ );
270
+ expect(mockSocket.write).toHaveBeenCalledWith(
271
+ JSON.stringify({ id: 'id2', result: 'path/to/file' }) + '\n\n',
272
+ );
273
+ });
274
+
275
+ it('should handle partial messages and buffer them', async () => {
276
+ // Setup mock handler
277
+ mockEventHandler.testMethod.mockResolvedValue('success');
278
+
279
+ // Start server
280
+ await server.start();
281
+
282
+ // Simulate connection
283
+ connectionHandler(mockSocket);
284
+
285
+ // Get data handler
286
+ const dataHandlerCall = mockSocket.on.mock.calls.find((call) => call[0] === 'data');
287
+ expect(dataHandlerCall).toBeDefined();
288
+ const dataHandler = dataHandlerCall![1];
289
+
290
+ // Create partial message (first half)
291
+ const fullMessage =
292
+ JSON.stringify({
293
+ id: 'test-id',
294
+ method: 'testMethod',
295
+ params: { data: 'test' },
296
+ }) + '\n\n';
297
+
298
+ const firstHalf = fullMessage.substring(0, 20);
299
+ await dataHandler(Buffer.from(firstHalf));
300
+
301
+ // 确保异步处理完成
302
+ await vi.runAllTimersAsync();
303
+
304
+ // Verify no handler calls yet
305
+ expect(mockEventHandler.testMethod).not.toHaveBeenCalled();
306
+
307
+ // Send second half
308
+ const secondHalf = fullMessage.substring(20);
309
+ await dataHandler(Buffer.from(secondHalf));
310
+
311
+ // 确保异步处理完成
312
+ await vi.runAllTimersAsync();
313
+
314
+ // Now handler should be called
315
+ expect(mockEventHandler.testMethod).toHaveBeenCalledWith(
316
+ { data: 'test' },
317
+ expect.objectContaining({ id: 'test-id' }),
318
+ );
319
+
320
+ // 等待处理程序完成
321
+ const pendingHandlerPromise = mockEventHandler.testMethod.mock.results[0].value;
322
+ await pendingHandlerPromise;
323
+
324
+ // 验证响应发送
325
+ expect(mockSocket.write).toHaveBeenCalledWith(
326
+ JSON.stringify({ id: 'test-id', result: 'success' }) + '\n\n',
327
+ );
328
+ });
329
+
330
+ it('should handle errors from method handlers', async () => {
331
+ // Setup mock handler to throw error
332
+ const mockError = new Error('Handler error');
333
+ mockEventHandler.testMethod.mockRejectedValue(mockError);
334
+
335
+ // Start server
336
+ await server.start();
337
+
338
+ // Simulate connection
339
+ connectionHandler(mockSocket);
340
+
341
+ // Get data handler
342
+ const dataHandlerCall = mockSocket.on.mock.calls.find((call) => call[0] === 'data');
343
+ expect(dataHandlerCall).toBeDefined();
344
+ const dataHandler = dataHandlerCall![1];
345
+
346
+ // Create test message
347
+ const message =
348
+ JSON.stringify({
349
+ id: 'test-id',
350
+ method: 'testMethod',
351
+ params: {},
352
+ }) + '\n\n';
353
+
354
+ // Send message
355
+ await dataHandler(Buffer.from(message));
356
+
357
+ // 确保异步处理完成
358
+ await vi.runAllTimersAsync();
359
+
360
+ // 等待Promise被拒绝
361
+ try {
362
+ const pendingHandlerPromise = mockEventHandler.testMethod.mock.results[0].value;
363
+ await pendingHandlerPromise;
364
+ } catch (error) {
365
+ // 错误预期会被捕获
366
+ }
367
+
368
+ // Verify error response
369
+ expect(mockSocket.write).toHaveBeenCalledWith(
370
+ expect.stringContaining(
371
+ '{"error":"Failed to handle method(testMethod): Handler error","id":"test-id"}\n\n',
372
+ ),
373
+ );
374
+ });
375
+ });
376
+
377
+ describe('server close', () => {
378
+ it('should close server and clean up socket file', async () => {
379
+ // Setup
380
+ mockServer.listen.mockImplementation((path, callback) => {
381
+ callback?.();
382
+ return mockServer;
383
+ });
384
+
385
+ // 明确模拟关闭回调
386
+ mockServer.close.mockImplementation((callback) => {
387
+ if (callback) {
388
+ setTimeout(() => callback(), 0);
389
+ }
390
+ return mockServer;
391
+ });
392
+
393
+ // 为非Windows环境设置平台
394
+ Object.defineProperty(process, 'platform', { value: 'darwin' });
395
+
396
+ // 模拟文件存在
397
+ vi.mocked(fs.existsSync).mockReturnValue(true);
398
+
399
+ // Execute
400
+ const server = new ElectronIPCServer(appId, mockEventHandler as any);
401
+ await server.start();
402
+
403
+ // 调用关闭方法
404
+ const closePromise = server.close();
405
+
406
+ // 运行所有计时器使关闭回调触发
407
+ await vi.runAllTimersAsync();
408
+
409
+ // 等待关闭完成
410
+ await closePromise;
411
+
412
+ // Verify
413
+ expect(mockServer.close).toHaveBeenCalled();
414
+ expect(fs.unlinkSync).toHaveBeenCalledWith(mockSocketPath);
415
+ });
416
+ });
417
+ });
@@ -70,20 +70,25 @@ export class ElectronIPCServer {
70
70
  log('Received data chunk, size: %d bytes', chunk.length);
71
71
  dataBuffer += chunk;
72
72
 
73
- try {
74
- // 尝试解析 JSON 消息
75
- const message = JSON.parse(dataBuffer);
76
- log('Successfully parsed JSON message: %o', {
77
- id: message.id,
78
- method: message.method,
79
- });
80
- dataBuffer = ''; // 重置缓冲区
81
-
82
- // 处理请求
83
- this.handleRequest(socket, message);
84
- } catch {
85
- // 如果不是有效的 JSON,可能是消息不完整,继续等待
86
- log('Incomplete or invalid JSON, buffering for more data');
73
+ // 按 \n\n 分割消息
74
+ const messages = dataBuffer.split('\n');
75
+ // 保留最后一个可能不完整的消息
76
+ dataBuffer = messages.pop() || '';
77
+
78
+ // 处理每个完整的消息
79
+ for (const message of messages) {
80
+ if (!message.trim()) continue;
81
+
82
+ try {
83
+ const parsedMessage = JSON.parse(message);
84
+ log('Successfully parsed JSON message: %o', {
85
+ id: parsedMessage.id,
86
+ method: parsedMessage.method,
87
+ });
88
+ this.handleRequest(socket, parsedMessage);
89
+ } catch (err) {
90
+ console.error('Failed to parse message: %s', err);
91
+ }
87
92
  }
88
93
  });
89
94
 
@@ -123,14 +128,14 @@ export class ElectronIPCServer {
123
128
 
124
129
  // 发送结果
125
130
  private sendResult(socket: net.Socket, id: string, result: any): void {
126
- const response = JSON.stringify({ id, result }) + '\n';
131
+ const response = JSON.stringify({ id, result }) + '\n\n';
127
132
  log('Sending success response for ID: %s, size: %d bytes', id, response.length);
128
133
  socket.write(response);
129
134
  }
130
135
 
131
136
  // 发送错误
132
137
  private sendError(socket: net.Socket, id: string, error: string): void {
133
- const response = JSON.stringify({ error, id }) + '\n';
138
+ const response = JSON.stringify({ error, id }) + '\n\n';
134
139
  log('Sending error response for ID: %s: %s', id, error);
135
140
  socket.write(response);
136
141
  }
package/vitest.config.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { resolve } from 'node:path';
1
+ import { join, resolve } from 'node:path';
2
2
  import { coverageConfigDefaults, defineConfig } from 'vitest/config';
3
3
 
4
4
  export default defineConfig({
@@ -32,6 +32,7 @@ export default defineConfig({
32
32
  '**/node_modules/**',
33
33
  '**/dist/**',
34
34
  '**/build/**',
35
+ '**/apps/desktop/**',
35
36
  'src/database/server/**/**',
36
37
  'src/database/repositories/dataImporter/deprecated/**/**',
37
38
  ],
@@ -41,6 +42,6 @@ export default defineConfig({
41
42
  inline: ['vitest-canvas-mock'],
42
43
  },
43
44
  },
44
- setupFiles: './tests/setup.ts',
45
+ setupFiles: join(__dirname, './tests/setup.ts'),
45
46
  },
46
47
  });