@lobehub/lobehub 2.0.0-next.137 → 2.0.0-next.139

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 (34) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/CLAUDE.md +38 -0
  3. package/changelog/v1.json +14 -0
  4. package/locales/ar/modelProvider.json +2 -0
  5. package/locales/bg-BG/modelProvider.json +2 -0
  6. package/locales/de-DE/modelProvider.json +2 -0
  7. package/locales/en-US/modelProvider.json +2 -0
  8. package/locales/es-ES/modelProvider.json +2 -0
  9. package/locales/fa-IR/modelProvider.json +2 -0
  10. package/locales/fr-FR/modelProvider.json +2 -0
  11. package/locales/it-IT/modelProvider.json +2 -0
  12. package/locales/ja-JP/modelProvider.json +2 -0
  13. package/locales/ko-KR/modelProvider.json +2 -0
  14. package/locales/nl-NL/modelProvider.json +2 -0
  15. package/locales/pl-PL/modelProvider.json +2 -0
  16. package/locales/pt-BR/modelProvider.json +2 -0
  17. package/locales/ru-RU/modelProvider.json +2 -0
  18. package/locales/tr-TR/modelProvider.json +2 -0
  19. package/locales/vi-VN/modelProvider.json +2 -0
  20. package/locales/zh-CN/modelProvider.json +2 -0
  21. package/locales/zh-TW/modelProvider.json +2 -0
  22. package/package.json +1 -1
  23. package/packages/conversation-flow/src/transformation/BranchResolver.ts +24 -14
  24. package/packages/conversation-flow/src/transformation/ContextTreeBuilder.ts +6 -1
  25. package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +15 -0
  26. package/packages/conversation-flow/src/transformation/__tests__/BranchResolver.test.ts +66 -3
  27. package/packages/conversation-flow/src/transformation/__tests__/ContextTreeBuilder.test.ts +64 -0
  28. package/packages/conversation-flow/src/transformation/__tests__/FlatListBuilder.test.ts +47 -0
  29. package/packages/database/src/models/__tests__/agent.test.ts +102 -3
  30. package/packages/database/src/models/__tests__/document.test.ts +163 -0
  31. package/packages/database/src/models/__tests__/embedding.test.ts +294 -0
  32. package/packages/database/src/models/__tests__/oauthHandoff.test.ts +261 -0
  33. package/packages/database/src/models/__tests__/thread.test.ts +327 -0
  34. package/packages/database/src/models/__tests__/user.test.ts +372 -0
@@ -311,6 +311,70 @@ describe('ContextTreeBuilder', () => {
311
311
  expect(result).toHaveLength(0);
312
312
  });
313
313
 
314
+ it('should set activeBranchIndex to children.length for optimistic update', () => {
315
+ const messageMap = new Map<string, Message>([
316
+ [
317
+ 'msg-1',
318
+ {
319
+ content: 'Hello',
320
+ createdAt: 0,
321
+ id: 'msg-1',
322
+ meta: {},
323
+ // activeBranchIndex = 2 means optimistic update (pointing to not-yet-created branch)
324
+ metadata: { activeBranchIndex: 2 },
325
+ role: 'user',
326
+ updatedAt: 0,
327
+ },
328
+ ],
329
+ [
330
+ 'msg-2',
331
+ {
332
+ content: 'Response 1',
333
+ createdAt: 0,
334
+ id: 'msg-2',
335
+ meta: {},
336
+ role: 'assistant',
337
+ updatedAt: 0,
338
+ },
339
+ ],
340
+ [
341
+ 'msg-3',
342
+ {
343
+ content: 'Response 2',
344
+ createdAt: 0,
345
+ id: 'msg-3',
346
+ meta: {},
347
+ role: 'assistant',
348
+ updatedAt: 0,
349
+ },
350
+ ],
351
+ ]);
352
+
353
+ const builder = createBuilder(messageMap);
354
+ const idNodes: IdNode[] = [
355
+ {
356
+ children: [
357
+ { children: [], id: 'msg-2' },
358
+ { children: [], id: 'msg-3' },
359
+ ],
360
+ id: 'msg-1',
361
+ },
362
+ ];
363
+
364
+ const result = builder.transformAll(idNodes);
365
+
366
+ expect(result).toHaveLength(2);
367
+ expect(result[0]).toEqual({ id: 'msg-1', type: 'message' });
368
+ // When activeBranchIndex === children.length (optimistic update),
369
+ // BranchResolver returns undefined, and ContextTreeBuilder uses children.length as index
370
+ expect(result[1]).toMatchObject({
371
+ activeBranchIndex: 2, // children.length = 2
372
+ branches: [[{ id: 'msg-2', type: 'message' }], [{ id: 'msg-3', type: 'message' }]],
373
+ parentMessageId: 'msg-1',
374
+ type: 'branch',
375
+ });
376
+ });
377
+
314
378
  it('should continue with active column children in compare mode', () => {
315
379
  const messageMap = new Map<string, Message>([
316
380
  [
@@ -507,5 +507,52 @@ describe('FlatListBuilder', () => {
507
507
  expect(result[1].role).toBe('assistantGroup');
508
508
  expect(result[2].id).toBe('msg-3');
509
509
  });
510
+
511
+ it('should handle optimistic update for user message with branches', () => {
512
+ // Scenario: User has sent a new message, activeBranchIndex points to a branch
513
+ // that is being created but doesn't exist yet (optimistic update)
514
+ const messages: Message[] = [
515
+ {
516
+ content: 'User',
517
+ createdAt: 0,
518
+ id: 'msg-1',
519
+ meta: {},
520
+ // activeBranchIndex = 2 means pointing to a not-yet-created branch (optimistic update)
521
+ // when there are only 2 existing children (msg-2, msg-3)
522
+ metadata: { activeBranchIndex: 2 },
523
+ role: 'user',
524
+ updatedAt: 0,
525
+ },
526
+ {
527
+ content: 'Branch 1',
528
+ createdAt: 0,
529
+ id: 'msg-2',
530
+ meta: {},
531
+ parentId: 'msg-1',
532
+ role: 'assistant',
533
+ updatedAt: 0,
534
+ },
535
+ {
536
+ content: 'Branch 2',
537
+ createdAt: 0,
538
+ id: 'msg-3',
539
+ meta: {},
540
+ parentId: 'msg-1',
541
+ role: 'assistant',
542
+ updatedAt: 0,
543
+ },
544
+ ];
545
+
546
+ const builder = createBuilder(messages);
547
+ const result = builder.flatten(messages);
548
+
549
+ // When activeBranchIndex === children.length (optimistic update),
550
+ // BranchResolver returns undefined, and FlatListBuilder just adds the user message
551
+ // without branch info and doesn't continue to any branch
552
+ expect(result).toHaveLength(1);
553
+ expect(result[0].id).toBe('msg-1');
554
+ // User message should not have branch info since we're in optimistic update mode
555
+ expect((result[0] as any).siblingCount).toBeUndefined();
556
+ });
510
557
  });
511
558
  });
@@ -20,9 +20,12 @@ import { getTestDB } from './_util';
20
20
  const serverDB: LobeChatDatabase = await getTestDB();
21
21
 
22
22
  const userId = 'agent-model-test-user-id';
23
+ const userId2 = 'agent-model-test-user-id-2';
23
24
  const agentModel = new AgentModel(serverDB, userId);
25
+ const agentModel2 = new AgentModel(serverDB, userId2);
24
26
 
25
27
  const knowledgeBase = { id: 'kb1', userId, name: 'knowledgeBase' };
28
+ const knowledgeBase2 = { id: 'kb2', userId: userId2, name: 'knowledgeBase2' };
26
29
  const fileList = [
27
30
  {
28
31
  id: '1',
@@ -42,11 +45,22 @@ const fileList = [
42
45
  },
43
46
  ];
44
47
 
48
+ const fileList2 = [
49
+ {
50
+ id: '3',
51
+ name: 'other.pdf',
52
+ url: 'https://a.com/other.pdf',
53
+ size: 1000,
54
+ fileType: 'application/pdf',
55
+ userId: userId2,
56
+ },
57
+ ];
58
+
45
59
  beforeEach(async () => {
46
60
  await serverDB.delete(users);
47
- await serverDB.insert(users).values([{ id: userId }]);
48
- await serverDB.insert(knowledgeBases).values(knowledgeBase);
49
- await serverDB.insert(files).values(fileList);
61
+ await serverDB.insert(users).values([{ id: userId }, { id: userId2 }]);
62
+ await serverDB.insert(knowledgeBases).values([knowledgeBase, knowledgeBase2]);
63
+ await serverDB.insert(files).values([...fileList, ...fileList2]);
50
64
  });
51
65
 
52
66
  afterEach(async () => {
@@ -226,6 +240,27 @@ describe('AgentModel', () => {
226
240
 
227
241
  expect(result).toBeUndefined();
228
242
  });
243
+
244
+ it('should not delete another user agent knowledge base association', async () => {
245
+ const agent = await serverDB
246
+ .insert(agents)
247
+ .values({ userId })
248
+ .returning()
249
+ .then((res) => res[0]);
250
+ await serverDB
251
+ .insert(agentsKnowledgeBases)
252
+ .values({ agentId: agent.id, knowledgeBaseId: knowledgeBase.id, userId });
253
+
254
+ // Try to delete with another user's model
255
+ await agentModel2.deleteAgentKnowledgeBase(agent.id, knowledgeBase.id);
256
+
257
+ const result = await serverDB.query.agentsKnowledgeBases.findFirst({
258
+ where: eq(agentsKnowledgeBases.agentId, agent.id),
259
+ });
260
+
261
+ // Should still exist
262
+ expect(result).toBeDefined();
263
+ });
229
264
  });
230
265
 
231
266
  describe('toggleKnowledgeBase', () => {
@@ -248,6 +283,28 @@ describe('AgentModel', () => {
248
283
 
249
284
  expect(result?.enabled).toBe(false);
250
285
  });
286
+
287
+ it('should not toggle another user agent knowledge base association', async () => {
288
+ const agent = await serverDB
289
+ .insert(agents)
290
+ .values({ userId })
291
+ .returning()
292
+ .then((res) => res[0]);
293
+
294
+ await serverDB
295
+ .insert(agentsKnowledgeBases)
296
+ .values({ agentId: agent.id, knowledgeBaseId: knowledgeBase.id, userId, enabled: true });
297
+
298
+ // Try to toggle with another user's model
299
+ await agentModel2.toggleKnowledgeBase(agent.id, knowledgeBase.id, false);
300
+
301
+ const result = await serverDB.query.agentsKnowledgeBases.findFirst({
302
+ where: eq(agentsKnowledgeBases.agentId, agent.id),
303
+ });
304
+
305
+ // Should still be enabled
306
+ expect(result?.enabled).toBe(true);
307
+ });
251
308
  });
252
309
 
253
310
  describe('createAgentFiles', () => {
@@ -363,6 +420,26 @@ describe('AgentModel', () => {
363
420
 
364
421
  expect(result).toBeUndefined();
365
422
  });
423
+
424
+ it('should not delete another user agent file association', async () => {
425
+ const agent = await serverDB
426
+ .insert(agents)
427
+ .values({ userId })
428
+ .returning()
429
+ .then((res) => res[0]);
430
+
431
+ await serverDB.insert(agentsFiles).values({ agentId: agent.id, fileId: '1', userId });
432
+
433
+ // Try to delete with another user's model
434
+ await agentModel2.deleteAgentFile(agent.id, '1');
435
+
436
+ const result = await serverDB.query.agentsFiles.findFirst({
437
+ where: eq(agentsFiles.agentId, agent.id),
438
+ });
439
+
440
+ // Should still exist
441
+ expect(result).toBeDefined();
442
+ });
366
443
  });
367
444
 
368
445
  describe('toggleFile', () => {
@@ -385,5 +462,27 @@ describe('AgentModel', () => {
385
462
 
386
463
  expect(result?.enabled).toBe(false);
387
464
  });
465
+
466
+ it('should not toggle another user agent file association', async () => {
467
+ const agent = await serverDB
468
+ .insert(agents)
469
+ .values({ userId })
470
+ .returning()
471
+ .then((res) => res[0]);
472
+
473
+ await serverDB
474
+ .insert(agentsFiles)
475
+ .values({ agentId: agent.id, fileId: '1', userId, enabled: true });
476
+
477
+ // Try to toggle with another user's model
478
+ await agentModel2.toggleFile(agent.id, '1', false);
479
+
480
+ const result = await serverDB.query.agentsFiles.findFirst({
481
+ where: eq(agentsFiles.agentId, agent.id),
482
+ });
483
+
484
+ // Should still be enabled
485
+ expect(result?.enabled).toBe(true);
486
+ });
388
487
  });
389
488
  });
@@ -54,6 +54,169 @@ const createTestDocument = async (model: DocumentModel, fModel: FileModel, conte
54
54
  };
55
55
 
56
56
  describe('DocumentModel', () => {
57
+ describe('create', () => {
58
+ it('should create a new document', async () => {
59
+ const { id: fileId } = await fileModel.create({
60
+ fileType: 'text/plain',
61
+ name: 'test.txt',
62
+ size: 100,
63
+ url: 'https://example.com/test.txt',
64
+ });
65
+
66
+ const file = await fileModel.findById(fileId);
67
+ if (!file) throw new Error('File not found');
68
+
69
+ const result = await documentModel.create({
70
+ content: 'Test content',
71
+ fileId: file.id,
72
+ fileType: 'text/plain',
73
+ source: file.url,
74
+ sourceType: 'file',
75
+ totalCharCount: 12,
76
+ totalLineCount: 1,
77
+ });
78
+
79
+ expect(result).toBeDefined();
80
+ expect(result.content).toBe('Test content');
81
+ expect(result.fileId).toBe(file.id);
82
+ });
83
+ });
84
+
85
+ describe('delete', () => {
86
+ it('should delete a document', async () => {
87
+ const { documentId } = await createTestDocument(documentModel, fileModel, 'Test content');
88
+
89
+ await documentModel.delete(documentId);
90
+
91
+ const deleted = await documentModel.findById(documentId);
92
+ expect(deleted).toBeUndefined();
93
+ });
94
+
95
+ it('should not delete document belonging to another user', async () => {
96
+ const { documentId } = await createTestDocument(documentModel, fileModel, 'Test content');
97
+
98
+ // Try to delete with another user's model
99
+ await documentModel2.delete(documentId);
100
+
101
+ // Document should still exist
102
+ const stillExists = await documentModel.findById(documentId);
103
+ expect(stillExists).toBeDefined();
104
+ });
105
+ });
106
+
107
+ describe('deleteAll', () => {
108
+ it('should delete all documents for the user', async () => {
109
+ await createTestDocument(documentModel, fileModel, 'First document');
110
+ await createTestDocument(documentModel, fileModel, 'Second document');
111
+ await createTestDocument(documentModel2, fileModel2, 'Other user document');
112
+
113
+ await documentModel.deleteAll();
114
+
115
+ const userDocs = await documentModel.query();
116
+ const otherUserDocs = await documentModel2.query();
117
+
118
+ expect(userDocs).toHaveLength(0);
119
+ expect(otherUserDocs).toHaveLength(1);
120
+ });
121
+ });
122
+
123
+ describe('query', () => {
124
+ it('should return all documents for the user', async () => {
125
+ await createTestDocument(documentModel, fileModel, 'First document');
126
+ await createTestDocument(documentModel, fileModel, 'Second document');
127
+
128
+ const result = await documentModel.query();
129
+
130
+ expect(result).toHaveLength(2);
131
+ });
132
+
133
+ it('should only return documents for the current user', async () => {
134
+ await createTestDocument(documentModel, fileModel, 'User 1 document');
135
+ await createTestDocument(documentModel2, fileModel2, 'User 2 document');
136
+
137
+ const result = await documentModel.query();
138
+
139
+ expect(result).toHaveLength(1);
140
+ expect(result[0].content).toBe('User 1 document');
141
+ });
142
+
143
+ it('should return documents ordered by updatedAt desc', async () => {
144
+ const { documentId: doc1Id } = await createTestDocument(
145
+ documentModel,
146
+ fileModel,
147
+ 'First document',
148
+ );
149
+ const { documentId: doc2Id } = await createTestDocument(
150
+ documentModel,
151
+ fileModel,
152
+ 'Second document',
153
+ );
154
+
155
+ // Wait a bit to ensure timestamp difference
156
+ await new Promise((resolve) => setTimeout(resolve, 50));
157
+
158
+ // Update first document to make it more recent
159
+ await documentModel.update(doc1Id, { content: 'Updated first document' });
160
+
161
+ const result = await documentModel.query();
162
+
163
+ expect(result[0].id).toBe(doc1Id);
164
+ expect(result[1].id).toBe(doc2Id);
165
+ });
166
+ });
167
+
168
+ describe('findById', () => {
169
+ it('should find document by id', async () => {
170
+ const { documentId } = await createTestDocument(documentModel, fileModel, 'Test content');
171
+
172
+ const found = await documentModel.findById(documentId);
173
+
174
+ expect(found).toBeDefined();
175
+ expect(found?.id).toBe(documentId);
176
+ expect(found?.content).toBe('Test content');
177
+ });
178
+
179
+ it('should return undefined for non-existent document', async () => {
180
+ const found = await documentModel.findById('non-existent-id');
181
+
182
+ expect(found).toBeUndefined();
183
+ });
184
+
185
+ it('should not find document belonging to another user', async () => {
186
+ const { documentId } = await createTestDocument(documentModel, fileModel, 'Test content');
187
+
188
+ const found = await documentModel2.findById(documentId);
189
+
190
+ expect(found).toBeUndefined();
191
+ });
192
+ });
193
+
194
+ describe('update', () => {
195
+ it('should update a document', async () => {
196
+ const { documentId } = await createTestDocument(documentModel, fileModel, 'Original content');
197
+
198
+ await documentModel.update(documentId, {
199
+ content: 'Updated content',
200
+ totalCharCount: 15,
201
+ });
202
+
203
+ const updated = await documentModel.findById(documentId);
204
+
205
+ expect(updated?.content).toBe('Updated content');
206
+ expect(updated?.totalCharCount).toBe(15);
207
+ });
208
+
209
+ it('should not update document belonging to another user', async () => {
210
+ const { documentId } = await createTestDocument(documentModel, fileModel, 'Original content');
211
+
212
+ await documentModel2.update(documentId, { content: 'Hacked content' });
213
+
214
+ const unchanged = await documentModel.findById(documentId);
215
+
216
+ expect(unchanged?.content).toBe('Original content');
217
+ });
218
+ });
219
+
57
220
  describe('findByFileId', () => {
58
221
  it('should find document by fileId', async () => {
59
222
  const { documentId, file } = await createTestDocument(