@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,1187 @@
1
+ import { INBOX_SESSION_ID } from '@lobechat/const';
2
+ import dayjs from 'dayjs';
3
+ import { eq } from 'drizzle-orm';
4
+ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
5
+
6
+ import { uuid } from '@/utils/uuid';
7
+
8
+ import {
9
+ agents,
10
+ chatGroups,
11
+ chunks,
12
+ documents,
13
+ embeddings,
14
+ fileChunks,
15
+ files,
16
+ messagePlugins,
17
+ messageQueries,
18
+ messageQueryChunks,
19
+ messageTTS,
20
+ messageTranslates,
21
+ messages,
22
+ messagesFiles,
23
+ sessions,
24
+ topics,
25
+ users,
26
+ } from '../../../schemas';
27
+ import { LobeChatDatabase } from '../../../type';
28
+ import { MessageModel } from '../../message';
29
+ import { getTestDB } from '../_util';
30
+ import { codeEmbedding } from '../fixtures/embedding';
31
+
32
+ const serverDB: LobeChatDatabase = await getTestDB();
33
+
34
+ const userId = 'message-query-test';
35
+ const otherUserId = 'message-query-test-other';
36
+ const messageModel = new MessageModel(serverDB, userId);
37
+ const embeddingsId = uuid();
38
+
39
+ beforeEach(async () => {
40
+ // Clear tables before each test case
41
+ await serverDB.transaction(async (trx) => {
42
+ await trx.delete(users).where(eq(users.id, userId));
43
+ await trx.delete(users).where(eq(users.id, otherUserId));
44
+ await trx.insert(users).values([{ id: userId }, { id: otherUserId }]);
45
+
46
+ await trx.insert(sessions).values([
47
+ // { id: 'session1', userId },
48
+ // { id: 'session2', userId },
49
+ { id: '1', userId },
50
+ ]);
51
+ await trx.insert(files).values({
52
+ id: 'f1',
53
+ userId: userId,
54
+ url: 'abc',
55
+ name: 'file-1',
56
+ fileType: 'image/png',
57
+ size: 1000,
58
+ });
59
+
60
+ await trx.insert(embeddings).values({
61
+ id: embeddingsId,
62
+ embeddings: codeEmbedding,
63
+ userId,
64
+ });
65
+ });
66
+ });
67
+
68
+ afterEach(async () => {
69
+ // Clear tables after each test case
70
+ await serverDB.delete(users).where(eq(users.id, userId));
71
+ await serverDB.delete(users).where(eq(users.id, otherUserId));
72
+ });
73
+
74
+ describe('MessageModel Query Tests', () => {
75
+ describe('query', () => {
76
+ it('should query messages by user ID', async () => {
77
+ // Create test data
78
+ await serverDB.insert(messages).values([
79
+ { id: '1', userId, role: 'user', content: 'message 1', createdAt: new Date('2023-01-01') },
80
+ { id: '2', userId, role: 'user', content: 'message 2', createdAt: new Date('2023-02-01') },
81
+ {
82
+ id: '3',
83
+ userId: otherUserId,
84
+ role: 'user',
85
+ content: 'message 3',
86
+ createdAt: new Date('2023-03-01'),
87
+ },
88
+ ]);
89
+
90
+ // Call query method
91
+ const result = await messageModel.query();
92
+
93
+ // Assert result
94
+ expect(result).toHaveLength(2);
95
+ expect(result[0].id).toBe('1');
96
+ expect(result[1].id).toBe('2');
97
+ });
98
+
99
+ it('should return empty messages if not match the user ID', async () => {
100
+ // Create test data
101
+ await serverDB.insert(messages).values([
102
+ {
103
+ id: '1',
104
+ userId: otherUserId,
105
+ role: 'user',
106
+ content: '1',
107
+ createdAt: new Date('2023-01-01'),
108
+ },
109
+ {
110
+ id: '2',
111
+ userId: otherUserId,
112
+ role: 'user',
113
+ content: '2',
114
+ createdAt: new Date('2023-02-01'),
115
+ },
116
+ {
117
+ id: '3',
118
+ userId: otherUserId,
119
+ role: 'user',
120
+ content: '3',
121
+ createdAt: new Date('2023-03-01'),
122
+ },
123
+ ]);
124
+
125
+ // Call query method
126
+ const result = await messageModel.query();
127
+
128
+ // Assert result
129
+ expect(result).toHaveLength(0);
130
+ });
131
+
132
+ it('should query messages with pagination', async () => {
133
+ // Create test data
134
+ await serverDB.insert(messages).values([
135
+ { id: '1', userId, role: 'user', content: 'message 1', createdAt: new Date('2023-01-01') },
136
+ { id: '2', userId, role: 'user', content: 'message 2', createdAt: new Date('2023-02-01') },
137
+ { id: '3', userId, role: 'user', content: 'message 3', createdAt: new Date('2023-03-01') },
138
+ ]);
139
+
140
+ // Test pagination
141
+ const result1 = await messageModel.query({ current: 0, pageSize: 2 });
142
+ expect(result1).toHaveLength(2);
143
+
144
+ const result2 = await messageModel.query({ current: 1, pageSize: 1 });
145
+ expect(result2).toHaveLength(1);
146
+ expect(result2[0].id).toBe('2');
147
+ });
148
+
149
+ it('should filter messages by sessionId', async () => {
150
+ // Create test data
151
+ await serverDB.insert(sessions).values([
152
+ { id: 'session1', userId },
153
+ { id: 'session2', userId },
154
+ ]);
155
+ await serverDB.insert(messages).values([
156
+ {
157
+ id: '1',
158
+ userId,
159
+ role: 'user',
160
+ sessionId: 'session1',
161
+ content: 'message 1',
162
+ createdAt: new Date('2022-02-01'),
163
+ },
164
+ {
165
+ id: '2',
166
+ userId,
167
+ role: 'user',
168
+ sessionId: 'session1',
169
+ content: 'message 2',
170
+ createdAt: new Date('2023-02-02'),
171
+ },
172
+ { id: '3', userId, role: 'user', sessionId: 'session2', content: 'message 3' },
173
+ ]);
174
+
175
+ // Test filtering by sessionId
176
+ const result = await messageModel.query({ sessionId: 'session1' });
177
+ expect(result).toHaveLength(2);
178
+ expect(result[0].id).toBe('1');
179
+ expect(result[1].id).toBe('2');
180
+ });
181
+
182
+ it('should filter messages by topicId', async () => {
183
+ // Create test data
184
+ const sessionId = 'session1';
185
+ await serverDB.insert(sessions).values([{ id: sessionId, userId }]);
186
+ const topicId = 'topic1';
187
+ await serverDB.insert(topics).values([
188
+ { id: topicId, sessionId, userId },
189
+ { id: 'topic2', sessionId, userId },
190
+ ]);
191
+
192
+ await serverDB.insert(messages).values([
193
+ { id: '1', userId, role: 'user', topicId, content: '1', createdAt: new Date('2022-04-01') },
194
+ { id: '2', userId, role: 'user', topicId, content: '2', createdAt: new Date('2023-02-01') },
195
+ { id: '3', userId, role: 'user', topicId: 'topic2', content: 'message 3' },
196
+ ]);
197
+
198
+ // Test filtering by topicId
199
+ const result = await messageModel.query({ topicId });
200
+ expect(result).toHaveLength(2);
201
+ expect(result[0].id).toBe('1');
202
+ expect(result[1].id).toBe('2');
203
+ });
204
+
205
+ it('should filter messages by groupId and expose group metadata', async () => {
206
+ await serverDB.transaction(async (trx) => {
207
+ await trx.insert(chatGroups).values([
208
+ { id: 'group-1', userId, title: 'Group 1' },
209
+ { id: 'group-2', userId, title: 'Group 2' },
210
+ ]);
211
+
212
+ await trx.insert(agents).values([
213
+ { id: 'agent-group', userId, title: 'Agent Group' },
214
+ { id: 'agent-other', userId, title: 'Agent Other' },
215
+ ]);
216
+
217
+ await trx.insert(messages).values([
218
+ {
219
+ id: 'group-message',
220
+ userId,
221
+ role: 'assistant',
222
+ content: 'group message',
223
+ groupId: 'group-1',
224
+ agentId: 'agent-group',
225
+ targetId: 'user',
226
+ createdAt: new Date('2024-01-01'),
227
+ },
228
+ {
229
+ id: 'other-message',
230
+ userId,
231
+ role: 'assistant',
232
+ content: 'other group message',
233
+ groupId: 'group-2',
234
+ agentId: 'agent-other',
235
+ targetId: 'user',
236
+ createdAt: new Date('2024-01-02'),
237
+ },
238
+ ]);
239
+ });
240
+
241
+ const result = await messageModel.query({ groupId: 'group-1' });
242
+
243
+ expect(result).toHaveLength(1);
244
+ expect(result[0].id).toBe('group-message');
245
+ expect(result[0].groupId).toBe('group-1');
246
+ expect(result[0].agentId).toBe('agent-group');
247
+ expect(result[0].targetId).toBe('user');
248
+ });
249
+
250
+ it('should query messages with join', async () => {
251
+ // Create test data
252
+ await serverDB.transaction(async (trx) => {
253
+ await trx.insert(messages).values([
254
+ {
255
+ id: '1',
256
+ userId,
257
+ role: 'user',
258
+ content: 'message 1',
259
+ createdAt: new Date('2023-01-01'),
260
+ },
261
+ {
262
+ id: '2',
263
+ userId,
264
+ role: 'user',
265
+ content: 'message 2',
266
+ createdAt: new Date('2023-02-01'),
267
+ },
268
+ {
269
+ id: '3',
270
+ userId: otherUserId,
271
+ role: 'user',
272
+ content: 'message 3',
273
+ createdAt: new Date('2023-03-01'),
274
+ },
275
+ ]);
276
+ await trx.insert(files).values([
277
+ { id: 'f-0', url: 'abc', name: 'file-1', userId, fileType: 'image/png', size: 1000 },
278
+ { id: 'f-1', url: 'abc', name: 'file-1', userId, fileType: 'image/png', size: 100 },
279
+ { id: 'f-3', url: 'abc', name: 'file-3', userId, fileType: 'image/png', size: 400 },
280
+ ]);
281
+ await trx.insert(messageTTS).values([
282
+ { id: '1', userId },
283
+ { id: '2', voice: 'a', fileId: 'f-1', contentMd5: 'abc', userId },
284
+ ]);
285
+
286
+ await trx.insert(messagesFiles).values([
287
+ { fileId: 'f-0', messageId: '1', userId },
288
+ { fileId: 'f-3', messageId: '1', userId },
289
+ ]);
290
+ });
291
+
292
+ const domain = 'http://abc.com';
293
+ // Call query method
294
+ const result = await messageModel.query(
295
+ {},
296
+ { postProcessUrl: async (path) => `${domain}/${path}` },
297
+ );
298
+
299
+ // Assert result
300
+ expect(result).toHaveLength(2);
301
+ expect(result[0].id).toBe('1');
302
+ expect(result[0].imageList).toEqual([
303
+ { alt: 'file-1', id: 'f-0', url: `${domain}/abc` },
304
+ { alt: 'file-3', id: 'f-3', url: `${domain}/abc` },
305
+ ]);
306
+
307
+ expect(result[1].id).toBe('2');
308
+ expect(result[1].imageList).toEqual([]);
309
+ });
310
+
311
+ it('should include translate, tts and other extra fields in query result', async () => {
312
+ // Create test data
313
+ await serverDB.transaction(async (trx) => {
314
+ await trx.insert(messages).values([
315
+ {
316
+ id: '1',
317
+ userId,
318
+ role: 'user',
319
+ content: 'message 1',
320
+ createdAt: new Date('2023-01-01'),
321
+ },
322
+ ]);
323
+ await trx
324
+ .insert(messageTranslates)
325
+ .values([{ id: '1', content: 'translated', from: 'en', to: 'zh', userId }]);
326
+ await trx
327
+ .insert(messageTTS)
328
+ .values([{ id: '1', voice: 'voice1', fileId: 'f1', contentMd5: 'md5', userId }]);
329
+ });
330
+
331
+ // Call query method
332
+ const result = await messageModel.query();
333
+
334
+ // Assert result
335
+ expect(result[0].extra!.translate).toEqual({ content: 'translated', from: 'en', to: 'zh' });
336
+ expect(result[0].extra!.tts).toEqual({
337
+ contentMd5: 'md5',
338
+ file: 'f1',
339
+ voice: 'voice1',
340
+ });
341
+ });
342
+
343
+ it('should handle edge cases of pagination parameters', async () => {
344
+ // Create test data
345
+ await serverDB.insert(messages).values([
346
+ { id: '1', userId, role: 'user', content: 'message 1' },
347
+ { id: '2', userId, role: 'user', content: 'message 2' },
348
+ { id: '3', userId, role: 'user', content: 'message 3' },
349
+ ]);
350
+
351
+ // 测试 current 和 pageSize 的边界情况
352
+ const result1 = await messageModel.query({ current: 0, pageSize: 2 });
353
+ expect(result1).toHaveLength(2);
354
+
355
+ const result2 = await messageModel.query({ current: 1, pageSize: 2 });
356
+ expect(result2).toHaveLength(1);
357
+
358
+ const result3 = await messageModel.query({ current: 2, pageSize: 2 });
359
+ expect(result3).toHaveLength(0);
360
+ });
361
+
362
+ describe('query with messageQueries', () => {
363
+ it('should include ragQuery, ragQueryId and ragRawQuery in query results', async () => {
364
+ // Create test data
365
+ const messageId = 'msg-with-query';
366
+ const queryId = uuid();
367
+
368
+ await serverDB.insert(messages).values({
369
+ id: messageId,
370
+ userId,
371
+ role: 'user',
372
+ content: 'test message',
373
+ });
374
+
375
+ await serverDB.insert(messageQueries).values({
376
+ id: queryId,
377
+ messageId,
378
+ userQuery: 'original query',
379
+ rewriteQuery: 'rewritten query',
380
+ userId,
381
+ });
382
+
383
+ // Call query method
384
+ const result = await messageModel.query();
385
+
386
+ // Assert result
387
+ expect(result).toHaveLength(1);
388
+ expect(result[0].id).toBe(messageId);
389
+ expect(result[0].ragQueryId).toBe(queryId);
390
+ expect(result[0].ragQuery).toBe('rewritten query');
391
+ expect(result[0].ragRawQuery).toBe('original query');
392
+ });
393
+
394
+ it('should handle multiple message queries for the same message', async () => {
395
+ // Create test data
396
+ const messageId = 'msg-multi-query';
397
+ const queryId1 = uuid();
398
+ const queryId2 = uuid();
399
+
400
+ await serverDB.insert(messages).values({
401
+ id: messageId,
402
+ userId,
403
+ role: 'user',
404
+ content: 'test message',
405
+ });
406
+
407
+ // 创建两个查询,查询结果应该只包含其中一个
408
+ // Note: 由于 messageQueries 表没有排序字段,返回哪个 query 是不确定的
409
+ // 但应该只返回一个
410
+ await serverDB.insert(messageQueries).values([
411
+ {
412
+ id: queryId1,
413
+ messageId,
414
+ userQuery: 'original query 1',
415
+ rewriteQuery: 'rewritten query 1',
416
+ userId,
417
+ },
418
+ {
419
+ id: queryId2,
420
+ messageId,
421
+ userQuery: 'original query 2',
422
+ rewriteQuery: 'rewritten query 2',
423
+ userId,
424
+ },
425
+ ]);
426
+
427
+ // Call query method
428
+ const result = await messageModel.query();
429
+
430
+ // Assert result - 应该只包含一个查询(具体是哪个取决于数据库实现)
431
+ expect(result).toHaveLength(1);
432
+ expect(result[0].id).toBe(messageId);
433
+ // 验证返回的是两个 query 中的一个
434
+ expect([queryId1, queryId2]).toContain(result[0].ragQueryId);
435
+ expect(['rewritten query 1', 'rewritten query 2']).toContain(result[0].ragQuery);
436
+ expect(['original query 1', 'original query 2']).toContain(result[0].ragRawQuery);
437
+ });
438
+ });
439
+
440
+ it('should handle complex query with multiple joins and file chunks', async () => {
441
+ await serverDB.transaction(async (trx) => {
442
+ const chunk1Id = uuid();
443
+ const query1Id = uuid();
444
+ // 创建基础消息
445
+ await trx.insert(messages).values({
446
+ id: 'msg1',
447
+ userId,
448
+ role: 'user',
449
+ content: 'test message',
450
+ createdAt: new Date('2023-01-01'),
451
+ });
452
+
453
+ // 创建文件
454
+ await trx.insert(files).values([
455
+ {
456
+ id: 'file1',
457
+ userId,
458
+ name: 'test.txt',
459
+ url: 'test-url',
460
+ fileType: 'text/plain',
461
+ size: 100,
462
+ },
463
+ ]);
464
+
465
+ // 创建文件块
466
+ await trx.insert(chunks).values({
467
+ id: chunk1Id,
468
+ text: 'chunk content',
469
+ });
470
+
471
+ // 关联消息和文件
472
+ await trx.insert(messagesFiles).values({
473
+ messageId: 'msg1',
474
+ userId,
475
+ fileId: 'file1',
476
+ });
477
+
478
+ // 创建文件块关联
479
+ await trx.insert(fileChunks).values({
480
+ fileId: 'file1',
481
+ userId,
482
+ chunkId: chunk1Id,
483
+ });
484
+
485
+ // 创建消息查询
486
+ await trx.insert(messageQueries).values({
487
+ id: query1Id,
488
+ messageId: 'msg1',
489
+ userId,
490
+ userQuery: 'original query',
491
+ rewriteQuery: 'rewritten query',
492
+ });
493
+
494
+ // 创建消息查询块关联
495
+ await trx.insert(messageQueryChunks).values({
496
+ messageId: 'msg1',
497
+ queryId: query1Id,
498
+ chunkId: chunk1Id,
499
+ similarity: '0.95',
500
+ userId,
501
+ });
502
+ });
503
+
504
+ const result = await messageModel.query();
505
+
506
+ expect(result).toHaveLength(1);
507
+ expect(result[0].chunksList).toHaveLength(1);
508
+ expect(result[0].chunksList![0]).toMatchObject({
509
+ text: 'chunk content',
510
+ similarity: 0.95,
511
+ });
512
+ });
513
+
514
+ it('should handle null similarity in chunks and convert to number', async () => {
515
+ await serverDB.transaction(async (trx) => {
516
+ const chunk1Id = uuid();
517
+ const query1Id = uuid();
518
+
519
+ await trx.insert(messages).values({
520
+ id: 'msg1',
521
+ userId,
522
+ role: 'user',
523
+ content: 'test message',
524
+ });
525
+
526
+ await trx.insert(files).values({
527
+ id: 'file1',
528
+ userId,
529
+ name: 'test.txt',
530
+ url: 'test-url',
531
+ fileType: 'text/plain',
532
+ size: 100,
533
+ });
534
+
535
+ await trx.insert(chunks).values({
536
+ id: chunk1Id,
537
+ text: 'chunk content',
538
+ });
539
+
540
+ await trx.insert(fileChunks).values({
541
+ fileId: 'file1',
542
+ userId,
543
+ chunkId: chunk1Id,
544
+ });
545
+
546
+ await trx.insert(messageQueries).values({
547
+ id: query1Id,
548
+ messageId: 'msg1',
549
+ userId,
550
+ userQuery: 'query',
551
+ rewriteQuery: 'rewritten',
552
+ });
553
+
554
+ // Insert chunk with null similarity
555
+ await trx.insert(messageQueryChunks).values({
556
+ messageId: 'msg1',
557
+ queryId: query1Id,
558
+ chunkId: chunk1Id,
559
+ similarity: null as any,
560
+ userId,
561
+ });
562
+ });
563
+
564
+ const result = await messageModel.query();
565
+
566
+ expect(result).toHaveLength(1);
567
+ expect(result[0].chunksList).toHaveLength(1);
568
+ // null should be converted to undefined
569
+ expect(result[0].chunksList![0].similarity).toBeUndefined();
570
+ });
571
+
572
+ it('should return empty arrays for files and chunks if none exist', async () => {
573
+ await serverDB.insert(messages).values({
574
+ id: 'msg1',
575
+ userId,
576
+ role: 'user',
577
+ content: 'test message',
578
+ });
579
+
580
+ const result = await messageModel.query();
581
+
582
+ expect(result).toHaveLength(1);
583
+ expect(result[0].fileList).toEqual([]);
584
+ expect(result[0].imageList).toEqual([]);
585
+ expect(result[0].chunksList).toEqual([]);
586
+ });
587
+
588
+ it('should query messages in session with null topicId (only non-topic messages)', async () => {
589
+ await serverDB.insert(sessions).values([{ id: 'session1', userId }]);
590
+ await serverDB.insert(topics).values([
591
+ { id: 'topic1', sessionId: 'session1', userId },
592
+ { id: 'topic2', sessionId: 'session1', userId },
593
+ ]);
594
+
595
+ await serverDB.insert(messages).values([
596
+ {
597
+ id: 'msg-no-topic-1',
598
+ userId,
599
+ sessionId: 'session1',
600
+ topicId: null,
601
+ role: 'user',
602
+ content: 'message without topic 1',
603
+ createdAt: new Date('2023-01-01'),
604
+ },
605
+ {
606
+ id: 'msg-no-topic-2',
607
+ userId,
608
+ sessionId: 'session1',
609
+ topicId: null,
610
+ role: 'assistant',
611
+ content: 'message without topic 2',
612
+ createdAt: new Date('2023-01-02'),
613
+ },
614
+ {
615
+ id: 'msg-topic1',
616
+ userId,
617
+ sessionId: 'session1',
618
+ topicId: 'topic1',
619
+ role: 'user',
620
+ content: 'message in topic1',
621
+ createdAt: new Date('2023-01-03'),
622
+ },
623
+ {
624
+ id: 'msg-topic2',
625
+ userId,
626
+ sessionId: 'session1',
627
+ topicId: 'topic2',
628
+ role: 'assistant',
629
+ content: 'message in topic2',
630
+ createdAt: new Date('2023-01-04'),
631
+ },
632
+ ]);
633
+
634
+ // Query with explicit null topicId should return only non-topic messages
635
+ const result = await messageModel.query({ sessionId: 'session1', topicId: null });
636
+
637
+ expect(result).toHaveLength(2);
638
+ expect(result[0].id).toBe('msg-no-topic-1');
639
+ expect(result[1].id).toBe('msg-no-topic-2');
640
+ });
641
+
642
+ it('should query messages in session with null groupId (only non-group messages)', async () => {
643
+ await serverDB.insert(sessions).values([{ id: 'session1', userId }]);
644
+ await serverDB.insert(chatGroups).values([
645
+ { id: 'group1', userId, title: 'Group 1' },
646
+ { id: 'group2', userId, title: 'Group 2' },
647
+ ]);
648
+
649
+ await serverDB.insert(messages).values([
650
+ {
651
+ id: 'msg-no-group-1',
652
+ userId,
653
+ sessionId: 'session1',
654
+ groupId: null,
655
+ role: 'user',
656
+ content: 'message without group 1',
657
+ createdAt: new Date('2023-01-01'),
658
+ },
659
+ {
660
+ id: 'msg-no-group-2',
661
+ userId,
662
+ sessionId: 'session1',
663
+ groupId: null,
664
+ role: 'assistant',
665
+ content: 'message without group 2',
666
+ createdAt: new Date('2023-01-02'),
667
+ },
668
+ {
669
+ id: 'msg-group1',
670
+ userId,
671
+ sessionId: 'session1',
672
+ groupId: 'group1',
673
+ role: 'user',
674
+ content: 'message in group1',
675
+ createdAt: new Date('2023-01-03'),
676
+ },
677
+ {
678
+ id: 'msg-group2',
679
+ userId,
680
+ sessionId: 'session1',
681
+ groupId: 'group2',
682
+ role: 'assistant',
683
+ content: 'message in group2',
684
+ createdAt: new Date('2023-01-04'),
685
+ },
686
+ ]);
687
+
688
+ // Query with explicit null groupId should return only non-group messages
689
+ const result = await messageModel.query({ sessionId: 'session1', groupId: null });
690
+
691
+ expect(result).toHaveLength(2);
692
+ expect(result[0].id).toBe('msg-no-group-1');
693
+ expect(result[1].id).toBe('msg-no-group-2');
694
+ });
695
+
696
+ it('should query inbox messages with null topicId when no sessionId specified', async () => {
697
+ await serverDB.insert(sessions).values([{ id: 'session1', userId }]);
698
+ await serverDB.insert(topics).values([{ id: 'topic1', sessionId: 'session1', userId }]);
699
+
700
+ await serverDB.insert(messages).values([
701
+ {
702
+ id: 'msg-inbox-no-topic',
703
+ userId,
704
+ sessionId: null, // inbox message
705
+ topicId: null,
706
+ role: 'user',
707
+ content: 'inbox message without topic',
708
+ createdAt: new Date('2023-01-01'),
709
+ },
710
+ {
711
+ id: 'msg-session-no-topic',
712
+ userId,
713
+ sessionId: 'session1',
714
+ topicId: null,
715
+ role: 'user',
716
+ content: 'session message without topic',
717
+ createdAt: new Date('2023-01-02'),
718
+ },
719
+ {
720
+ id: 'msg-session-with-topic',
721
+ userId,
722
+ sessionId: 'session1',
723
+ topicId: 'topic1',
724
+ role: 'user',
725
+ content: 'session message with topic',
726
+ createdAt: new Date('2023-01-03'),
727
+ },
728
+ ]);
729
+
730
+ // When no sessionId specified (defaults to inbox), query with topicId null
731
+ // should return only inbox messages without topics
732
+ const result = await messageModel.query({ topicId: null });
733
+
734
+ expect(result).toHaveLength(1);
735
+ expect(result[0].id).toBe('msg-inbox-no-topic');
736
+ });
737
+
738
+ it('should query messages with combined sessionId and topicId filters', async () => {
739
+ await serverDB.insert(sessions).values([
740
+ { id: 'session1', userId },
741
+ { id: 'session2', userId },
742
+ ]);
743
+ await serverDB.insert(topics).values([
744
+ { id: 'topic1', sessionId: 'session1', userId },
745
+ { id: 'topic2', sessionId: 'session1', userId },
746
+ ]);
747
+
748
+ await serverDB.insert(messages).values([
749
+ {
750
+ id: 'msg-s1-t1',
751
+ userId,
752
+ sessionId: 'session1',
753
+ topicId: 'topic1',
754
+ role: 'user',
755
+ content: 'session1 topic1',
756
+ createdAt: new Date('2023-01-01'),
757
+ },
758
+ {
759
+ id: 'msg-s1-t2',
760
+ userId,
761
+ sessionId: 'session1',
762
+ topicId: 'topic2',
763
+ role: 'user',
764
+ content: 'session1 topic2',
765
+ createdAt: new Date('2023-01-02'),
766
+ },
767
+ {
768
+ id: 'msg-s2',
769
+ userId,
770
+ sessionId: 'session2',
771
+ topicId: null,
772
+ role: 'user',
773
+ content: 'session2 no topic',
774
+ createdAt: new Date('2023-01-03'),
775
+ },
776
+ ]);
777
+
778
+ // Query specific session and topic combination
779
+ const result = await messageModel.query({ sessionId: 'session1', topicId: 'topic1' });
780
+
781
+ expect(result).toHaveLength(1);
782
+ expect(result[0].id).toBe('msg-s1-t1');
783
+ });
784
+ });
785
+
786
+ describe('queryAll', () => {
787
+ it('should return all messages belonging to the user in ascending order', async () => {
788
+ // Create test data
789
+ await serverDB.insert(messages).values([
790
+ {
791
+ id: '1',
792
+ userId,
793
+ role: 'user',
794
+ content: 'message 1',
795
+ createdAt: new Date('2023-01-01'),
796
+ },
797
+ {
798
+ id: '2',
799
+ userId,
800
+ role: 'user',
801
+ content: 'message 2',
802
+ createdAt: new Date('2023-02-01'),
803
+ },
804
+ {
805
+ id: '3',
806
+ userId: otherUserId,
807
+ role: 'user',
808
+ content: 'message 3',
809
+ createdAt: new Date('2023-03-01'),
810
+ },
811
+ ]);
812
+
813
+ // Call queryAll method
814
+ const result = await messageModel.queryAll();
815
+
816
+ // Assert result
817
+ expect(result).toHaveLength(2);
818
+ expect(result[0].id).toBe('1');
819
+ expect(result[1].id).toBe('2');
820
+ });
821
+ });
822
+
823
+ describe('findById', () => {
824
+ it('should find message by ID', async () => {
825
+ // Create test data
826
+ await serverDB.insert(messages).values([
827
+ { id: '1', userId, role: 'user', content: 'message 1' },
828
+ { id: '2', userId: otherUserId, role: 'user', content: 'message 2' },
829
+ ]);
830
+
831
+ // Call findById method
832
+ const result = await messageModel.findById('1');
833
+
834
+ // Assert result
835
+ expect(result?.id).toBe('1');
836
+ expect(result?.content).toBe('message 1');
837
+ });
838
+
839
+ it('should return undefined if message does not belong to user', async () => {
840
+ // Create test data
841
+ await serverDB
842
+ .insert(messages)
843
+ .values([{ id: '1', userId: otherUserId, role: 'user', content: 'message 1' }]);
844
+
845
+ // Call findById method
846
+ const result = await messageModel.findById('1');
847
+
848
+ // Assert result
849
+ expect(result).toBeUndefined();
850
+ });
851
+ });
852
+
853
+ describe('queryBySessionId', () => {
854
+ it('should query messages by sessionId', async () => {
855
+ // Create test data
856
+ const sessionId = 'session1';
857
+ await serverDB.insert(sessions).values([
858
+ { id: 'session1', userId },
859
+ { id: 'session2', userId },
860
+ ]);
861
+ await serverDB.insert(messages).values([
862
+ {
863
+ id: '1',
864
+ userId,
865
+ role: 'user',
866
+ sessionId,
867
+ content: 'message 1',
868
+ createdAt: new Date('2022-01-01'),
869
+ },
870
+ {
871
+ id: '2',
872
+ userId,
873
+ role: 'user',
874
+ sessionId,
875
+ content: 'message 2',
876
+ createdAt: new Date('2023-02-01'),
877
+ },
878
+ { id: '3', userId, role: 'user', sessionId: 'session2', content: 'message 3' },
879
+ ]);
880
+
881
+ // Call queryBySessionId method
882
+ const result = await messageModel.queryBySessionId(sessionId);
883
+
884
+ // Assert result
885
+ expect(result).toHaveLength(2);
886
+ expect(result[0].id).toBe('1');
887
+ expect(result[1].id).toBe('2');
888
+ });
889
+
890
+ it('should query inbox messages when sessionId is null', async () => {
891
+ await serverDB.insert(sessions).values([{ id: 'session1', userId }]);
892
+
893
+ await serverDB.insert(messages).values([
894
+ {
895
+ id: 'inbox-msg-1',
896
+ userId,
897
+ sessionId: null, // inbox message
898
+ role: 'user',
899
+ content: 'inbox message 1',
900
+ createdAt: new Date('2023-01-01'),
901
+ },
902
+ {
903
+ id: 'inbox-msg-2',
904
+ userId,
905
+ sessionId: null, // inbox message
906
+ role: 'assistant',
907
+ content: 'inbox message 2',
908
+ createdAt: new Date('2023-01-02'),
909
+ },
910
+ {
911
+ id: 'session-msg',
912
+ userId,
913
+ sessionId: 'session1',
914
+ role: 'user',
915
+ content: 'session message',
916
+ createdAt: new Date('2023-01-03'),
917
+ },
918
+ ]);
919
+
920
+ // Query with null sessionId should return only inbox messages
921
+ const result = await messageModel.queryBySessionId(null);
922
+
923
+ expect(result).toHaveLength(2);
924
+ expect(result[0].id).toBe('inbox-msg-1');
925
+ expect(result[1].id).toBe('inbox-msg-2');
926
+ });
927
+
928
+ it('should query inbox messages when sessionId is undefined', async () => {
929
+ await serverDB.insert(sessions).values([{ id: 'session1', userId }]);
930
+
931
+ await serverDB.insert(messages).values([
932
+ {
933
+ id: 'inbox-msg',
934
+ userId,
935
+ sessionId: null,
936
+ role: 'user',
937
+ content: 'inbox message',
938
+ createdAt: new Date('2023-01-01'),
939
+ },
940
+ {
941
+ id: 'session-msg',
942
+ userId,
943
+ sessionId: 'session1',
944
+ role: 'user',
945
+ content: 'session message',
946
+ createdAt: new Date('2023-01-02'),
947
+ },
948
+ ]);
949
+
950
+ // Query with undefined sessionId should also return inbox messages
951
+ const result = await messageModel.queryBySessionId(undefined);
952
+
953
+ expect(result).toHaveLength(1);
954
+ expect(result[0].id).toBe('inbox-msg');
955
+ });
956
+
957
+ it('should query inbox messages when sessionId is INBOX_SESSION_ID', async () => {
958
+ await serverDB.insert(sessions).values([{ id: 'session1', userId }]);
959
+
960
+ await serverDB.insert(messages).values([
961
+ {
962
+ id: 'inbox-msg-1',
963
+ userId,
964
+ sessionId: null,
965
+ role: 'user',
966
+ content: 'inbox message 1',
967
+ createdAt: new Date('2023-01-01'),
968
+ },
969
+ {
970
+ id: 'inbox-msg-2',
971
+ userId,
972
+ sessionId: null,
973
+ role: 'assistant',
974
+ content: 'inbox message 2',
975
+ createdAt: new Date('2023-01-02'),
976
+ },
977
+ {
978
+ id: 'session-msg',
979
+ userId,
980
+ sessionId: 'session1',
981
+ role: 'user',
982
+ content: 'session message',
983
+ createdAt: new Date('2023-01-03'),
984
+ },
985
+ ]);
986
+
987
+ // Query with INBOX_SESSION_ID should return only inbox messages
988
+ const result = await messageModel.queryBySessionId(INBOX_SESSION_ID);
989
+
990
+ expect(result).toHaveLength(2);
991
+ expect(result[0].id).toBe('inbox-msg-1');
992
+ expect(result[1].id).toBe('inbox-msg-2');
993
+ });
994
+ });
995
+
996
+ describe('queryByKeyWord', () => {
997
+ it('should query messages by keyword', async () => {
998
+ // Create test data
999
+ await serverDB.insert(messages).values([
1000
+ { id: '1', userId, role: 'user', content: 'apple', createdAt: new Date('2022-02-01') },
1001
+ { id: '2', userId, role: 'user', content: 'banana' },
1002
+ { id: '3', userId, role: 'user', content: 'pear' },
1003
+ { id: '4', userId, role: 'user', content: 'apple pie', createdAt: new Date('2024-02-01') },
1004
+ ]);
1005
+
1006
+ // Test querying messages with specific keyword
1007
+ const result = await messageModel.queryByKeyword('apple');
1008
+
1009
+ // Assert result
1010
+ expect(result).toHaveLength(2);
1011
+ expect(result[0].id).toBe('4');
1012
+ expect(result[1].id).toBe('1');
1013
+ });
1014
+
1015
+ it('should return empty array when keyword is empty', async () => {
1016
+ // Create test data
1017
+ await serverDB.insert(messages).values([
1018
+ { id: '1', userId, role: 'user', content: 'apple' },
1019
+ { id: '2', userId, role: 'user', content: 'banana' },
1020
+ { id: '3', userId, role: 'user', content: 'pear' },
1021
+ { id: '4', userId, role: 'user', content: 'apple pie' },
1022
+ ]);
1023
+
1024
+ // Test returning empty array when keyword is empty
1025
+ const result = await messageModel.queryByKeyword('');
1026
+
1027
+ // Assert result
1028
+ expect(result).toHaveLength(0);
1029
+ });
1030
+ });
1031
+
1032
+ describe('query with files edge cases', () => {
1033
+ it('should handle files with empty fileType', async () => {
1034
+ await serverDB.transaction(async (trx) => {
1035
+ await trx.insert(sessions).values({ id: 'session1', userId });
1036
+
1037
+ // Create files with empty string fileType (tests the || '' branch)
1038
+ await trx.insert(files).values([
1039
+ {
1040
+ id: 'file-empty-type',
1041
+ userId,
1042
+ url: 'unknown.bin',
1043
+ name: 'unknown file',
1044
+ fileType: '',
1045
+ size: 1000,
1046
+ },
1047
+ {
1048
+ id: 'file-image',
1049
+ userId,
1050
+ url: 'image.png',
1051
+ name: 'test image',
1052
+ fileType: 'image/png',
1053
+ size: 2000,
1054
+ },
1055
+ {
1056
+ id: 'file-video',
1057
+ userId,
1058
+ url: 'video.mp4',
1059
+ name: 'test video',
1060
+ fileType: 'video/mp4',
1061
+ size: 3000,
1062
+ },
1063
+ ]);
1064
+
1065
+ const messageId = uuid();
1066
+ await trx.insert(messages).values({
1067
+ id: messageId,
1068
+ userId,
1069
+ role: 'user',
1070
+ content: 'Message with various fileTypes',
1071
+ sessionId: 'session1',
1072
+ });
1073
+
1074
+ await trx.insert(messagesFiles).values([
1075
+ { messageId, fileId: 'file-empty-type', userId },
1076
+ { messageId, fileId: 'file-image', userId },
1077
+ { messageId, fileId: 'file-video', userId },
1078
+ ]);
1079
+ });
1080
+
1081
+ const result = await messageModel.query({ sessionId: 'session1' });
1082
+
1083
+ expect(result).toHaveLength(1);
1084
+ expect(result[0].fileList).toHaveLength(1); // empty fileType should go to fileList
1085
+ expect(result[0].fileList![0].id).toBe('file-empty-type');
1086
+ expect(result[0].imageList).toHaveLength(1);
1087
+ expect(result[0].imageList![0].id).toBe('file-image');
1088
+ expect(result[0].videoList).toHaveLength(1);
1089
+ expect(result[0].videoList![0].id).toBe('file-video');
1090
+ });
1091
+ });
1092
+
1093
+ describe('query with documents', () => {
1094
+ it('should include document content when files have associated documents', async () => {
1095
+ // Create a file with an associated document
1096
+ const fileId = uuid();
1097
+
1098
+ await serverDB.transaction(async (trx) => {
1099
+ await trx.insert(sessions).values({ id: 'session1', userId });
1100
+
1101
+ await trx.insert(files).values({
1102
+ id: fileId,
1103
+ userId,
1104
+ url: 'document.pdf',
1105
+ name: 'test.pdf',
1106
+ fileType: 'application/pdf',
1107
+ size: 5000,
1108
+ });
1109
+
1110
+ await trx.insert(documents).values({
1111
+ fileId,
1112
+ userId,
1113
+ content: 'This is the document content for testing',
1114
+ fileType: 'application/pdf',
1115
+ sourceType: 'file',
1116
+ source: 'document.pdf',
1117
+ totalCharCount: 42,
1118
+ totalLineCount: 1,
1119
+ });
1120
+
1121
+ const messageId = uuid();
1122
+ await trx.insert(messages).values({
1123
+ id: messageId,
1124
+ userId,
1125
+ role: 'user',
1126
+ content: 'Message with document',
1127
+ sessionId: 'session1',
1128
+ });
1129
+
1130
+ await trx.insert(messagesFiles).values({
1131
+ messageId,
1132
+ fileId,
1133
+ userId,
1134
+ });
1135
+ });
1136
+
1137
+ // Query messages - this should trigger the documents processing code
1138
+ const result = await messageModel.query({ sessionId: 'session1' });
1139
+
1140
+ expect(result).toHaveLength(1);
1141
+ expect(result[0].fileList).toBeDefined();
1142
+ expect(result[0].fileList).toHaveLength(1);
1143
+ expect(result[0].fileList![0].id).toBe(fileId);
1144
+ expect(result[0].fileList![0].content).toBe('This is the document content for testing');
1145
+ });
1146
+ });
1147
+
1148
+ describe('findMessageQueriesById', () => {
1149
+ it('should return undefined for non-existent message query', async () => {
1150
+ const result = await messageModel.findMessageQueriesById('non-existent-id');
1151
+ expect(result).toBeUndefined();
1152
+ });
1153
+
1154
+ it('should return message query with embeddings', async () => {
1155
+ const query1Id = uuid();
1156
+ const embeddings1Id = uuid();
1157
+
1158
+ await serverDB.transaction(async (trx) => {
1159
+ await trx.insert(messages).values({ id: 'msg1', userId, role: 'user', content: 'abc' });
1160
+
1161
+ await trx.insert(embeddings).values({
1162
+ id: embeddings1Id,
1163
+ embeddings: codeEmbedding,
1164
+ });
1165
+
1166
+ await trx.insert(messageQueries).values({
1167
+ id: query1Id,
1168
+ messageId: 'msg1',
1169
+ userQuery: 'test query',
1170
+ rewriteQuery: 'rewritten query',
1171
+ embeddingsId: embeddings1Id,
1172
+ userId,
1173
+ });
1174
+ });
1175
+
1176
+ const result = await messageModel.findMessageQueriesById('msg1');
1177
+
1178
+ expect(result).toBeDefined();
1179
+ expect(result).toMatchObject({
1180
+ id: query1Id,
1181
+ userQuery: 'test query',
1182
+ rewriteQuery: 'rewritten query',
1183
+ embeddings: codeEmbedding,
1184
+ });
1185
+ });
1186
+ });
1187
+ });