@lobehub/lobehub 2.0.0-next.40 → 2.0.0-next.42

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 (45) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/changelog/v1.json +18 -0
  3. package/locales/ar/chat.json +1 -0
  4. package/locales/bg-BG/chat.json +1 -0
  5. package/locales/de-DE/chat.json +1 -0
  6. package/locales/en-US/chat.json +1 -0
  7. package/locales/es-ES/chat.json +1 -0
  8. package/locales/fa-IR/chat.json +1 -0
  9. package/locales/fr-FR/chat.json +1 -0
  10. package/locales/it-IT/chat.json +1 -0
  11. package/locales/ja-JP/chat.json +1 -0
  12. package/locales/ko-KR/chat.json +1 -0
  13. package/locales/nl-NL/chat.json +1 -0
  14. package/locales/pl-PL/chat.json +1 -0
  15. package/locales/pt-BR/chat.json +1 -0
  16. package/locales/ru-RU/chat.json +1 -0
  17. package/locales/tr-TR/chat.json +1 -0
  18. package/locales/vi-VN/chat.json +1 -0
  19. package/locales/zh-CN/chat.json +1 -0
  20. package/locales/zh-TW/chat.json +1 -0
  21. package/package.json +1 -1
  22. package/packages/database/src/models/__tests__/messages/message.create.test.ts +549 -0
  23. package/packages/database/src/models/__tests__/messages/message.delete.test.ts +481 -0
  24. package/packages/database/src/models/__tests__/messages/message.query.test.ts +1187 -0
  25. package/packages/database/src/models/__tests__/messages/message.stats.test.ts +633 -0
  26. package/packages/database/src/models/__tests__/messages/message.update.test.ts +757 -0
  27. package/packages/database/src/models/message.ts +5 -55
  28. package/packages/utils/src/clientIP.ts +6 -6
  29. package/packages/utils/src/compressImage.ts +3 -3
  30. package/packages/utils/src/fetch/fetchSSE.ts +15 -15
  31. package/packages/utils/src/format.ts +2 -2
  32. package/packages/utils/src/merge.ts +3 -3
  33. package/packages/utils/src/parseModels.ts +3 -3
  34. package/packages/utils/src/sanitizeUTF8.ts +4 -4
  35. package/packages/utils/src/toolManifest.ts +4 -4
  36. package/packages/utils/src/trace.test.ts +359 -0
  37. package/packages/utils/src/uriParser.ts +4 -4
  38. package/src/features/ChatItem/components/Title.tsx +20 -16
  39. package/src/features/Conversation/Messages/Assistant/index.tsx +3 -2
  40. package/src/features/Conversation/Messages/Group/index.tsx +10 -3
  41. package/src/server/services/message/index.ts +14 -4
  42. package/src/store/chat/slices/aiChat/actions/conversationLifecycle.ts +8 -2
  43. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +1 -4
  44. package/src/store/chat/slices/message/actions/optimisticUpdate.ts +1 -1
  45. package/packages/database/src/models/__tests__/message.test.ts +0 -2632
@@ -0,0 +1,757 @@
1
+ import { eq } from 'drizzle-orm';
2
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
3
+
4
+ import { uuid } from '@/utils/uuid';
5
+
6
+ import {
7
+ agents,
8
+ chatGroups,
9
+ chunks,
10
+ embeddings,
11
+ fileChunks,
12
+ files,
13
+ messagePlugins,
14
+ messageQueries,
15
+ messageQueryChunks,
16
+ messageTTS,
17
+ messageTranslates,
18
+ messages,
19
+ messagesFiles,
20
+ sessions,
21
+ topics,
22
+ users,
23
+ } from '../../../schemas';
24
+ import { LobeChatDatabase } from '../../../type';
25
+ import { MessageModel } from '../../message';
26
+ import { getTestDB } from '../_util';
27
+ import { codeEmbedding } from '../fixtures/embedding';
28
+
29
+ const serverDB: LobeChatDatabase = await getTestDB();
30
+
31
+ const userId = 'message-update-test';
32
+ const otherUserId = 'message-update-test-other';
33
+ const messageModel = new MessageModel(serverDB, userId);
34
+ const embeddingsId = uuid();
35
+
36
+ beforeEach(async () => {
37
+ // Clear tables before each test case
38
+ await serverDB.transaction(async (trx) => {
39
+ await trx.delete(users).where(eq(users.id, userId));
40
+ await trx.delete(users).where(eq(users.id, otherUserId));
41
+ await trx.insert(users).values([{ id: userId }, { id: otherUserId }]);
42
+
43
+ await trx.insert(sessions).values([
44
+ // { id: 'session1', userId },
45
+ // { id: 'session2', userId },
46
+ { id: '1', userId },
47
+ ]);
48
+ await trx.insert(files).values({
49
+ id: 'f1',
50
+ userId: userId,
51
+ url: 'abc',
52
+ name: 'file-1',
53
+ fileType: 'image/png',
54
+ size: 1000,
55
+ });
56
+
57
+ await trx.insert(embeddings).values({
58
+ id: embeddingsId,
59
+ embeddings: codeEmbedding,
60
+ userId,
61
+ });
62
+ });
63
+ });
64
+
65
+ afterEach(async () => {
66
+ // Clear tables after each test case
67
+ await serverDB.delete(users).where(eq(users.id, userId));
68
+ await serverDB.delete(users).where(eq(users.id, otherUserId));
69
+ });
70
+
71
+ describe('MessageModel Update Tests', () => {
72
+ describe('updateMessage', () => {
73
+ it('should update message content', async () => {
74
+ // Create test data
75
+ await serverDB
76
+ .insert(messages)
77
+ .values([{ id: '1', userId, role: 'user', content: 'message 1' }]);
78
+
79
+ // Call updateMessage method
80
+ await messageModel.update('1', { content: 'updated message' });
81
+
82
+ // Assert result
83
+ const result = await serverDB.select().from(messages).where(eq(messages.id, '1'));
84
+ expect(result[0].content).toBe('updated message');
85
+ });
86
+
87
+ it('should only update messages belonging to the user', async () => {
88
+ // Create test data
89
+ await serverDB
90
+ .insert(messages)
91
+ .values([{ id: '1', userId: otherUserId, role: 'user', content: 'message 1' }]);
92
+
93
+ // Call updateMessage method
94
+ await messageModel.update('1', { content: 'updated message' });
95
+
96
+ // Assert result
97
+ const result = await serverDB.select().from(messages).where(eq(messages.id, '1'));
98
+ expect(result[0].content).toBe('message 1');
99
+ });
100
+
101
+ it('should update message tools', async () => {
102
+ // Create test data
103
+ await serverDB.insert(messages).values([
104
+ {
105
+ id: '1',
106
+ userId,
107
+ role: 'user',
108
+ content: 'message 1',
109
+ tools: [
110
+ {
111
+ id: 'call_Z8UU8LedZcoJHFGkfqYecjmT',
112
+ type: 'builtin',
113
+ apiName: 'searchWithSearXNG',
114
+ arguments:
115
+ '{"query":"杭州洪水 2023","searchEngines":["google","bing","baidu","duckduckgo","brave"]}',
116
+ identifier: 'lobe-web-browsing',
117
+ },
118
+ ],
119
+ },
120
+ ]);
121
+
122
+ // Call updateMessage method
123
+ await messageModel.update('1', {
124
+ tools: [
125
+ {
126
+ id: 'call_Z8UU8LedZcoJHFGkfqYecjmT',
127
+ type: 'builtin',
128
+ apiName: 'searchWithSearXNG',
129
+ arguments: '{"query":"2024 杭州暴雨","searchEngines":["duckduckgo","google","brave"]}',
130
+ identifier: 'lobe-web-browsing',
131
+ },
132
+ ],
133
+ });
134
+
135
+ // Assert result
136
+ const result = await serverDB.select().from(messages).where(eq(messages.id, '1'));
137
+ expect((result[0].tools as any)[0].arguments).toBe(
138
+ '{"query":"2024 杭州暴雨","searchEngines":["duckduckgo","google","brave"]}',
139
+ );
140
+ });
141
+
142
+ describe('update with imageList', () => {
143
+ it('should update a message and add image files', async () => {
144
+ // Create test data
145
+ await serverDB.insert(messages).values({
146
+ id: 'msg-to-update',
147
+ userId,
148
+ role: 'user',
149
+ content: 'original content',
150
+ });
151
+
152
+ await serverDB.insert(files).values([
153
+ {
154
+ id: 'img1',
155
+ name: 'image1.jpg',
156
+ fileType: 'image/jpeg',
157
+ size: 100,
158
+ url: 'url1',
159
+ userId,
160
+ },
161
+ { id: 'img2', name: 'image2.png', fileType: 'image/png', size: 200, url: 'url2', userId },
162
+ ]);
163
+
164
+ // Call update method
165
+ await messageModel.update('msg-to-update', {
166
+ content: 'updated content',
167
+ imageList: [
168
+ { id: 'img1', alt: 'image 1', url: 'url1' },
169
+ { id: 'img2', alt: 'image 2', url: 'url2' },
170
+ ],
171
+ });
172
+
173
+ // Verify message updated successfully
174
+ const updatedMessage = await serverDB
175
+ .select()
176
+ .from(messages)
177
+ .where(eq(messages.id, 'msg-to-update'));
178
+
179
+ expect(updatedMessage[0].content).toBe('updated content');
180
+
181
+ // Verify message file associations created successfully
182
+ const messageFiles = await serverDB
183
+ .select()
184
+ .from(messagesFiles)
185
+ .where(eq(messagesFiles.messageId, 'msg-to-update'));
186
+
187
+ expect(messageFiles).toHaveLength(2);
188
+ expect(messageFiles[0].fileId).toBe('img1');
189
+ expect(messageFiles[1].fileId).toBe('img2');
190
+ });
191
+
192
+ it('should handle empty imageList', async () => {
193
+ // Create test data
194
+ await serverDB.insert(messages).values({
195
+ id: 'msg-no-images',
196
+ userId,
197
+ role: 'user',
198
+ content: 'original content',
199
+ });
200
+
201
+ // Call update method without providing imageList
202
+ await messageModel.update('msg-no-images', {
203
+ content: 'updated content',
204
+ });
205
+
206
+ // Verify message updated successfully
207
+ const updatedMessage = await serverDB
208
+ .select()
209
+ .from(messages)
210
+ .where(eq(messages.id, 'msg-no-images'));
211
+
212
+ expect(updatedMessage[0].content).toBe('updated content');
213
+
214
+ // Verify no message file associations created
215
+ const messageFiles = await serverDB
216
+ .select()
217
+ .from(messagesFiles)
218
+ .where(eq(messagesFiles.messageId, 'msg-no-images'));
219
+
220
+ expect(messageFiles).toHaveLength(0);
221
+ });
222
+
223
+ it('should update multiple fields at once', async () => {
224
+ // Create test data
225
+ await serverDB.insert(messages).values({
226
+ id: 'msg-multi-update',
227
+ userId,
228
+ role: 'user',
229
+ content: 'original content',
230
+ model: 'gpt-3.5',
231
+ });
232
+
233
+ // Call update method to update multiple fields
234
+ await messageModel.update('msg-multi-update', {
235
+ content: 'updated content',
236
+ role: 'assistant',
237
+ model: 'gpt-4',
238
+ metadata: { tps: 1 },
239
+ });
240
+
241
+ // Verify message updated successfully
242
+ const updatedMessage = await serverDB
243
+ .select()
244
+ .from(messages)
245
+ .where(eq(messages.id, 'msg-multi-update'));
246
+
247
+ expect(updatedMessage[0].content).toBe('updated content');
248
+ expect(updatedMessage[0].role).toBe('assistant');
249
+ expect(updatedMessage[0].model).toBe('gpt-4');
250
+ expect(updatedMessage[0].metadata).toEqual({ tps: 1 });
251
+ });
252
+ });
253
+ });
254
+
255
+ describe('deleteMessage', () => {
256
+ it('should delete a message', async () => {
257
+ // Create test data
258
+ await serverDB
259
+ .insert(messages)
260
+ .values([{ id: '1', userId, role: 'user', content: 'message 1' }]);
261
+
262
+ // 调用 deleteMessage 方法
263
+ await messageModel.deleteMessage('1');
264
+
265
+ // Assert result
266
+ const result = await serverDB.select().from(messages).where(eq(messages.id, '1'));
267
+ expect(result).toHaveLength(0);
268
+ });
269
+
270
+ it('should delete a message with tool calls', async () => {
271
+ // Create test data
272
+ await serverDB.transaction(async (trx) => {
273
+ await trx.insert(messages).values([
274
+ { id: '1', userId, role: 'user', content: 'message 1', tools: [{ id: 'tool1' }] },
275
+ { id: '2', userId, role: 'tool', content: 'message 1' },
276
+ ]);
277
+ await trx
278
+ .insert(messagePlugins)
279
+ .values([{ id: '2', toolCallId: 'tool1', identifier: 'plugin-1', userId }]);
280
+ });
281
+
282
+ // 调用 deleteMessage 方法
283
+ await messageModel.deleteMessage('1');
284
+
285
+ // Assert result
286
+ const result = await serverDB.select().from(messages).where(eq(messages.id, '1'));
287
+ expect(result).toHaveLength(0);
288
+
289
+ const result2 = await serverDB
290
+ .select()
291
+ .from(messagePlugins)
292
+ .where(eq(messagePlugins.id, '2'));
293
+
294
+ expect(result2).toHaveLength(0);
295
+ });
296
+
297
+ it('should only delete messages belonging to the user', async () => {
298
+ // Create test data
299
+ await serverDB
300
+ .insert(messages)
301
+ .values([{ id: '1', userId: otherUserId, role: 'user', content: 'message 1' }]);
302
+
303
+ // 调用 deleteMessage 方法
304
+ await messageModel.deleteMessage('1');
305
+
306
+ // Assert result
307
+ const result = await serverDB.select().from(messages).where(eq(messages.id, '1'));
308
+ expect(result).toHaveLength(1);
309
+ });
310
+ });
311
+
312
+ describe('deleteMessages', () => {
313
+ it('should delete 2 messages', async () => {
314
+ // Create test data
315
+ await serverDB.insert(messages).values([
316
+ { id: '1', userId, role: 'user', content: 'message 1' },
317
+ { id: '2', userId, role: 'user', content: 'message 2' },
318
+ ]);
319
+
320
+ // 调用 deleteMessage 方法
321
+ await messageModel.deleteMessages(['1', '2']);
322
+
323
+ // Assert result
324
+ const result = await serverDB.select().from(messages).where(eq(messages.id, '1'));
325
+ expect(result).toHaveLength(0);
326
+ const result2 = await serverDB.select().from(messages).where(eq(messages.id, '2'));
327
+ expect(result2).toHaveLength(0);
328
+ });
329
+
330
+ it('should only delete messages belonging to the user', async () => {
331
+ // Create test data
332
+ await serverDB.insert(messages).values([
333
+ { id: '1', userId: otherUserId, role: 'user', content: 'message 1' },
334
+ { id: '2', userId: otherUserId, role: 'user', content: 'message 1' },
335
+ ]);
336
+
337
+ // 调用 deleteMessage 方法
338
+ await messageModel.deleteMessages(['1', '2']);
339
+
340
+ // Assert result
341
+ const result = await serverDB.select().from(messages).where(eq(messages.id, '1'));
342
+ expect(result).toHaveLength(1);
343
+ });
344
+ });
345
+
346
+ describe('deleteAllMessages', () => {
347
+ it('should delete all messages belonging to the user', async () => {
348
+ // Create test data
349
+ await serverDB.insert(messages).values([
350
+ { id: '1', userId, role: 'user', content: 'message 1' },
351
+ { id: '2', userId, role: 'user', content: 'message 2' },
352
+ { id: '3', userId: otherUserId, role: 'user', content: 'message 3' },
353
+ ]);
354
+
355
+ // 调用 deleteAllMessages 方法
356
+ await messageModel.deleteAllMessages();
357
+
358
+ // Assert result
359
+ const result = await serverDB.select().from(messages).where(eq(messages.userId, userId));
360
+
361
+ expect(result).toHaveLength(0);
362
+
363
+ const otherResult = await serverDB
364
+ .select()
365
+ .from(messages)
366
+ .where(eq(messages.userId, otherUserId));
367
+
368
+ expect(otherResult).toHaveLength(1);
369
+ });
370
+
371
+ it('should handle database errors gracefully', async () => {
372
+ // Create test message
373
+ await serverDB.insert(messages).values({
374
+ id: '1',
375
+ content: 'test message',
376
+ role: 'user',
377
+ userId,
378
+ });
379
+
380
+ // Mock database to throw error by trying to update with invalid sessionId reference
381
+ // This should trigger the catch block in the update method
382
+ const result = await messageModel.update('1', {
383
+ // @ts-expect-error - intentionally passing invalid sessionId to trigger error
384
+ sessionId: 'non-existent-session-that-violates-fk',
385
+ });
386
+
387
+ expect(result.success).toBe(false);
388
+ });
389
+ });
390
+
391
+ describe('updatePluginState', () => {
392
+ it('should update the state field in messagePlugins table', async () => {
393
+ // Create test data
394
+ await serverDB.insert(messages).values({ id: '1', content: 'abc', role: 'user', userId });
395
+ await serverDB.insert(messagePlugins).values([
396
+ {
397
+ id: '1',
398
+ toolCallId: 'tool1',
399
+ identifier: 'plugin1',
400
+ state: { key1: 'value1' },
401
+ userId,
402
+ },
403
+ ]);
404
+
405
+ // 调用 updatePluginState 方法
406
+ await messageModel.updatePluginState('1', { key2: 'value2' });
407
+
408
+ // Assert result
409
+ const result = await serverDB.select().from(messagePlugins).where(eq(messagePlugins.id, '1'));
410
+
411
+ expect(result[0].state).toEqual({ key1: 'value1', key2: 'value2' });
412
+ });
413
+
414
+ it('should handle null state in plugin', async () => {
415
+ // Create test data with null state
416
+ await serverDB.insert(messages).values({ id: '1', content: 'abc', role: 'user', userId });
417
+ await serverDB.insert(messagePlugins).values([
418
+ {
419
+ id: '1',
420
+ toolCallId: 'tool1',
421
+ identifier: 'plugin1',
422
+ state: null,
423
+ userId,
424
+ },
425
+ ]);
426
+
427
+ // Call updatePluginState method
428
+ await messageModel.updatePluginState('1', { key1: 'value1' });
429
+
430
+ // Assert result - should merge with empty object when state is null
431
+ const result = await serverDB.select().from(messagePlugins).where(eq(messagePlugins.id, '1'));
432
+
433
+ expect(result[0].state).toEqual({ key1: 'value1' });
434
+ });
435
+
436
+ it('should throw an error if plugin does not exist', async () => {
437
+ // 调用 updatePluginState 方法
438
+ await expect(messageModel.updatePluginState('1', { key: 'value' })).rejects.toThrowError(
439
+ 'Plugin not found',
440
+ );
441
+ });
442
+ });
443
+ describe('updateMessagePlugin', () => {
444
+ it('should update the state field in messagePlugins table', async () => {
445
+ // Create test data
446
+ await serverDB.insert(messages).values({ id: '1', content: 'abc', role: 'user', userId });
447
+ await serverDB.insert(messagePlugins).values([
448
+ {
449
+ id: '1',
450
+ toolCallId: 'tool1',
451
+ identifier: 'plugin1',
452
+ state: { key1: 'value1' },
453
+ userId,
454
+ },
455
+ ]);
456
+
457
+ // 调用 updatePluginState 方法
458
+ await messageModel.updateMessagePlugin('1', { identifier: 'plugin2' });
459
+
460
+ // Assert result
461
+ const result = await serverDB.select().from(messagePlugins).where(eq(messagePlugins.id, '1'));
462
+
463
+ expect(result[0].identifier).toEqual('plugin2');
464
+ });
465
+
466
+ it('should throw an error if plugin does not exist', async () => {
467
+ // 调用 updateMessagePlugin 方法(修复:之前错误地调用了 updatePluginState)
468
+ await expect(
469
+ messageModel.updateMessagePlugin('non-existent-id', { identifier: 'test' }),
470
+ ).rejects.toThrowError('Plugin not found');
471
+ });
472
+ });
473
+
474
+ describe('updateMetadata', () => {
475
+ it('should update metadata for an existing message', async () => {
476
+ // Create test data
477
+ await serverDB.insert(messages).values({
478
+ id: 'msg-with-metadata',
479
+ userId,
480
+ role: 'user',
481
+ content: 'test message',
482
+ metadata: { existingKey: 'existingValue' },
483
+ });
484
+
485
+ // 调用 updateMetadata 方法
486
+ await messageModel.updateMetadata('msg-with-metadata', { newKey: 'newValue' });
487
+
488
+ // Assert result
489
+ const result = await serverDB
490
+ .select()
491
+ .from(messages)
492
+ .where(eq(messages.id, 'msg-with-metadata'));
493
+
494
+ expect(result[0].metadata).toEqual({
495
+ existingKey: 'existingValue',
496
+ newKey: 'newValue',
497
+ });
498
+ });
499
+
500
+ it('should merge new metadata with existing metadata using lodash merge behavior', async () => {
501
+ // Create test data
502
+ await serverDB.insert(messages).values({
503
+ id: 'msg-merge-metadata',
504
+ userId,
505
+ role: 'assistant',
506
+ content: 'test message',
507
+ metadata: {
508
+ level1: {
509
+ level2a: 'original',
510
+ level2b: { level3: 'deep' },
511
+ },
512
+ array: [1, 2, 3],
513
+ },
514
+ });
515
+
516
+ // 调用 updateMetadata 方法
517
+ await messageModel.updateMetadata('msg-merge-metadata', {
518
+ level1: {
519
+ level2a: 'updated',
520
+ level2c: 'new',
521
+ },
522
+ newTopLevel: 'value',
523
+ });
524
+
525
+ // Assert result - 应该使用 lodash merge 行为
526
+ const result = await serverDB
527
+ .select()
528
+ .from(messages)
529
+ .where(eq(messages.id, 'msg-merge-metadata'));
530
+
531
+ expect(result[0].metadata).toEqual({
532
+ level1: {
533
+ level2a: 'updated',
534
+ level2b: { level3: 'deep' },
535
+ level2c: 'new',
536
+ },
537
+ array: [1, 2, 3],
538
+ newTopLevel: 'value',
539
+ });
540
+ });
541
+
542
+ it('should handle non-existent message IDs', async () => {
543
+ // 调用 updateMetadata 方法,尝试更新不存在的消息
544
+ const result = await messageModel.updateMetadata('non-existent-id', { key: 'value' });
545
+
546
+ // Assert result - 应该返回 undefined
547
+ expect(result).toBeUndefined();
548
+ });
549
+
550
+ it('should handle empty metadata updates', async () => {
551
+ // Create test data
552
+ await serverDB.insert(messages).values({
553
+ id: 'msg-empty-metadata',
554
+ userId,
555
+ role: 'user',
556
+ content: 'test message',
557
+ metadata: { originalKey: 'originalValue' },
558
+ });
559
+
560
+ // 调用 updateMetadata 方法,传递空对象
561
+ await messageModel.updateMetadata('msg-empty-metadata', {});
562
+
563
+ // Assert result - 原始 metadata 应该保持不变
564
+ const result = await serverDB
565
+ .select()
566
+ .from(messages)
567
+ .where(eq(messages.id, 'msg-empty-metadata'));
568
+
569
+ expect(result[0].metadata).toEqual({ originalKey: 'originalValue' });
570
+ });
571
+
572
+ it('should handle message with null metadata', async () => {
573
+ // Create test data
574
+ await serverDB.insert(messages).values({
575
+ id: 'msg-null-metadata',
576
+ userId,
577
+ role: 'user',
578
+ content: 'test message',
579
+ metadata: null,
580
+ });
581
+
582
+ // 调用 updateMetadata 方法
583
+ await messageModel.updateMetadata('msg-null-metadata', { key: 'value' });
584
+
585
+ // Assert result - 应该创建新的 metadata
586
+ const result = await serverDB
587
+ .select()
588
+ .from(messages)
589
+ .where(eq(messages.id, 'msg-null-metadata'));
590
+
591
+ expect(result[0].metadata).toEqual({ key: 'value' });
592
+ });
593
+
594
+ it('should only update messages belonging to the current user', async () => {
595
+ // Create test data - 其他用户的消息
596
+ await serverDB.insert(messages).values({
597
+ id: 'msg-other-user',
598
+ userId: otherUserId,
599
+ role: 'user',
600
+ content: 'test message',
601
+ metadata: { originalKey: 'originalValue' },
602
+ });
603
+
604
+ // 调用 updateMetadata 方法
605
+ const result = await messageModel.updateMetadata('msg-other-user', {
606
+ hackedKey: 'hackedValue',
607
+ });
608
+
609
+ // Assert result - 应该返回 undefined
610
+ expect(result).toBeUndefined();
611
+
612
+ // 验证原始 metadata 未被修改
613
+ const dbResult = await serverDB
614
+ .select()
615
+ .from(messages)
616
+ .where(eq(messages.id, 'msg-other-user'));
617
+
618
+ expect(dbResult[0].metadata).toEqual({ originalKey: 'originalValue' });
619
+ });
620
+
621
+ it('should handle complex nested metadata updates', async () => {
622
+ // Create test data
623
+ await serverDB.insert(messages).values({
624
+ id: 'msg-complex-metadata',
625
+ userId,
626
+ role: 'assistant',
627
+ content: 'test message',
628
+ metadata: {
629
+ config: {
630
+ settings: {
631
+ enabled: true,
632
+ options: ['a', 'b'],
633
+ },
634
+ version: 1,
635
+ },
636
+ },
637
+ });
638
+
639
+ // 调用 updateMetadata 方法
640
+ await messageModel.updateMetadata('msg-complex-metadata', {
641
+ config: {
642
+ settings: {
643
+ enabled: false,
644
+ timeout: 5000,
645
+ },
646
+ newField: 'value',
647
+ },
648
+ stats: { count: 10 },
649
+ });
650
+
651
+ // Assert result
652
+ const result = await serverDB
653
+ .select()
654
+ .from(messages)
655
+ .where(eq(messages.id, 'msg-complex-metadata'));
656
+
657
+ expect(result[0].metadata).toEqual({
658
+ config: {
659
+ settings: {
660
+ enabled: false,
661
+ options: ['a', 'b'],
662
+ timeout: 5000,
663
+ },
664
+ version: 1,
665
+ newField: 'value',
666
+ },
667
+ stats: { count: 10 },
668
+ });
669
+ });
670
+ });
671
+
672
+ describe('updateTranslate', () => {
673
+ it('should insert a new record if message does not exist in messageTranslates table', async () => {
674
+ // Create test data
675
+ await serverDB
676
+ .insert(messages)
677
+ .values([{ id: '1', userId, role: 'user', content: 'message 1' }]);
678
+
679
+ // 调用 updateTranslate 方法
680
+ await messageModel.updateTranslate('1', {
681
+ content: 'translated message 1',
682
+ from: 'en',
683
+ to: 'zh',
684
+ });
685
+
686
+ // Assert result
687
+ const result = await serverDB
688
+ .select()
689
+ .from(messageTranslates)
690
+ .where(eq(messageTranslates.id, '1'));
691
+
692
+ expect(result).toHaveLength(1);
693
+ expect(result[0].content).toBe('translated message 1');
694
+ });
695
+
696
+ it('should update the corresponding fields if message exists in messageTranslates table', async () => {
697
+ // Create test data
698
+ await serverDB.transaction(async (trx) => {
699
+ await trx
700
+ .insert(messages)
701
+ .values([{ id: '1', userId, role: 'user', content: 'message 1' }]);
702
+ await trx
703
+ .insert(messageTranslates)
704
+ .values([{ id: '1', content: 'translated message 1', from: 'en', to: 'zh', userId }]);
705
+ });
706
+
707
+ // 调用 updateTranslate 方法
708
+ await messageModel.updateTranslate('1', { content: 'updated translated message 1' });
709
+
710
+ // Assert result
711
+ const result = await serverDB
712
+ .select()
713
+ .from(messageTranslates)
714
+ .where(eq(messageTranslates.id, '1'));
715
+
716
+ expect(result[0].content).toBe('updated translated message 1');
717
+ });
718
+ });
719
+
720
+ describe('updateTTS', () => {
721
+ it('should insert a new record if message does not exist in messageTTS table', async () => {
722
+ // Create test data
723
+ await serverDB
724
+ .insert(messages)
725
+ .values([{ id: '1', userId, role: 'user', content: 'message 1' }]);
726
+
727
+ // 调用 updateTTS 方法
728
+ await messageModel.updateTTS('1', { contentMd5: 'md5', file: 'f1', voice: 'voice1' });
729
+
730
+ // Assert result
731
+ const result = await serverDB.select().from(messageTTS).where(eq(messageTTS.id, '1'));
732
+
733
+ expect(result).toHaveLength(1);
734
+ expect(result[0].voice).toBe('voice1');
735
+ });
736
+
737
+ it('should update the corresponding fields if message exists in messageTTS table', async () => {
738
+ // Create test data
739
+ await serverDB.transaction(async (trx) => {
740
+ await trx
741
+ .insert(messages)
742
+ .values([{ id: '1', userId, role: 'user', content: 'message 1' }]);
743
+ await trx
744
+ .insert(messageTTS)
745
+ .values([{ id: '1', contentMd5: 'md5', fileId: 'f1', voice: 'voice1', userId }]);
746
+ });
747
+
748
+ // 调用 updateTTS 方法
749
+ await messageModel.updateTTS('1', { voice: 'updated voice1' });
750
+
751
+ // Assert result
752
+ const result = await serverDB.select().from(messageTTS).where(eq(messageTTS.id, '1'));
753
+
754
+ expect(result[0].voice).toBe('updated voice1');
755
+ });
756
+ });
757
+ });