@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.
- package/CHANGELOG.md +50 -0
- package/CLAUDE.md +38 -0
- package/changelog/v1.json +14 -0
- package/locales/ar/modelProvider.json +2 -0
- package/locales/bg-BG/modelProvider.json +2 -0
- package/locales/de-DE/modelProvider.json +2 -0
- package/locales/en-US/modelProvider.json +2 -0
- package/locales/es-ES/modelProvider.json +2 -0
- package/locales/fa-IR/modelProvider.json +2 -0
- package/locales/fr-FR/modelProvider.json +2 -0
- package/locales/it-IT/modelProvider.json +2 -0
- package/locales/ja-JP/modelProvider.json +2 -0
- package/locales/ko-KR/modelProvider.json +2 -0
- package/locales/nl-NL/modelProvider.json +2 -0
- package/locales/pl-PL/modelProvider.json +2 -0
- package/locales/pt-BR/modelProvider.json +2 -0
- package/locales/ru-RU/modelProvider.json +2 -0
- package/locales/tr-TR/modelProvider.json +2 -0
- package/locales/vi-VN/modelProvider.json +2 -0
- package/locales/zh-CN/modelProvider.json +2 -0
- package/locales/zh-TW/modelProvider.json +2 -0
- package/package.json +1 -1
- package/packages/conversation-flow/src/transformation/BranchResolver.ts +24 -14
- package/packages/conversation-flow/src/transformation/ContextTreeBuilder.ts +6 -1
- package/packages/conversation-flow/src/transformation/FlatListBuilder.ts +15 -0
- package/packages/conversation-flow/src/transformation/__tests__/BranchResolver.test.ts +66 -3
- package/packages/conversation-flow/src/transformation/__tests__/ContextTreeBuilder.test.ts +64 -0
- package/packages/conversation-flow/src/transformation/__tests__/FlatListBuilder.test.ts +47 -0
- package/packages/database/src/models/__tests__/agent.test.ts +102 -3
- package/packages/database/src/models/__tests__/document.test.ts +163 -0
- package/packages/database/src/models/__tests__/embedding.test.ts +294 -0
- package/packages/database/src/models/__tests__/oauthHandoff.test.ts +261 -0
- package/packages/database/src/models/__tests__/thread.test.ts +327 -0
- 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(
|