@lobehub/chat 0.162.25 → 0.163.0

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 (84) hide show
  1. package/.github/workflows/release.yml +21 -2
  2. package/.github/workflows/sync.yml +1 -1
  3. package/.github/workflows/test.yml +35 -4
  4. package/CHANGELOG.md +25 -0
  5. package/LICENSE +38 -21
  6. package/codecov.yml +11 -0
  7. package/drizzle.config.ts +29 -0
  8. package/next.config.mjs +3 -0
  9. package/package.json +24 -4
  10. package/scripts/migrateServerDB/index.ts +30 -0
  11. package/src/app/(main)/(mobile)/me/(home)/features/useCategory.tsx +2 -1
  12. package/src/app/(main)/chat/@session/features/SessionListContent/List/Item/Actions.tsx +95 -88
  13. package/src/app/(main)/chat/settings/features/HeaderContent.tsx +37 -31
  14. package/src/app/api/webhooks/clerk/__tests__/fixtures/createUser.json +73 -0
  15. package/src/app/api/webhooks/clerk/route.ts +159 -0
  16. package/src/app/api/webhooks/clerk/validateRequest.ts +22 -0
  17. package/src/app/trpc/edge/[trpc]/route.ts +1 -1
  18. package/src/app/trpc/lambda/[trpc]/route.ts +26 -0
  19. package/src/config/auth.ts +2 -0
  20. package/src/config/db.ts +13 -1
  21. package/src/database/server/core/db.ts +44 -0
  22. package/src/database/server/core/dbForTest.ts +45 -0
  23. package/src/database/server/index.ts +1 -0
  24. package/src/database/server/migrations/0000_init.sql +439 -0
  25. package/src/database/server/migrations/0001_add_client_id.sql +9 -0
  26. package/src/database/server/migrations/0002_amusing_puma.sql +9 -0
  27. package/src/database/server/migrations/meta/0000_snapshot.json +1583 -0
  28. package/src/database/server/migrations/meta/0001_snapshot.json +1636 -0
  29. package/src/database/server/migrations/meta/0002_snapshot.json +1630 -0
  30. package/src/database/server/migrations/meta/_journal.json +27 -0
  31. package/src/database/server/models/__tests__/file.test.ts +140 -0
  32. package/src/database/server/models/__tests__/message.test.ts +847 -0
  33. package/src/database/server/models/__tests__/plugin.test.ts +172 -0
  34. package/src/database/server/models/__tests__/session.test.ts +595 -0
  35. package/src/database/server/models/__tests__/topic.test.ts +623 -0
  36. package/src/database/server/models/__tests__/user.test.ts +173 -0
  37. package/src/database/server/models/_template.ts +44 -0
  38. package/src/database/server/models/file.ts +51 -0
  39. package/src/database/server/models/message.ts +378 -0
  40. package/src/database/server/models/plugin.ts +63 -0
  41. package/src/database/server/models/session.ts +290 -0
  42. package/src/database/server/models/sessionGroup.ts +69 -0
  43. package/src/database/server/models/topic.ts +265 -0
  44. package/src/database/server/models/user.ts +138 -0
  45. package/src/database/server/modules/DataImporter/__tests__/fixtures/messages.json +1101 -0
  46. package/src/database/server/modules/DataImporter/__tests__/index.test.ts +954 -0
  47. package/src/database/server/modules/DataImporter/index.ts +333 -0
  48. package/src/database/server/schemas/_id.ts +15 -0
  49. package/src/database/server/schemas/lobechat.ts +601 -0
  50. package/src/database/server/utils/idGenerator.test.ts +39 -0
  51. package/src/database/server/utils/idGenerator.ts +26 -0
  52. package/src/features/User/UserPanel/useMenu.tsx +43 -37
  53. package/src/libs/trpc/client.ts +52 -3
  54. package/src/server/files/s3.ts +21 -1
  55. package/src/server/keyVaultsEncrypt/index.test.ts +62 -0
  56. package/src/server/keyVaultsEncrypt/index.ts +93 -0
  57. package/src/server/mock.ts +1 -1
  58. package/src/server/routers/{index.ts → edge/index.ts} +3 -3
  59. package/src/server/routers/lambda/file.ts +49 -0
  60. package/src/server/routers/lambda/importer.ts +54 -0
  61. package/src/server/routers/lambda/index.ts +28 -0
  62. package/src/server/routers/lambda/message.ts +165 -0
  63. package/src/server/routers/lambda/plugin.ts +100 -0
  64. package/src/server/routers/lambda/session.ts +194 -0
  65. package/src/server/routers/lambda/sessionGroup.ts +77 -0
  66. package/src/server/routers/lambda/topic.ts +134 -0
  67. package/src/server/routers/lambda/user.ts +57 -0
  68. package/src/services/file/index.ts +4 -7
  69. package/src/services/file/server.ts +45 -0
  70. package/src/services/import/index.ts +4 -1
  71. package/src/services/import/server.ts +115 -0
  72. package/src/services/message/index.ts +4 -8
  73. package/src/services/message/server.ts +93 -0
  74. package/src/services/plugin/index.ts +4 -9
  75. package/src/services/plugin/server.ts +46 -0
  76. package/src/services/session/index.ts +4 -8
  77. package/src/services/session/server.ts +148 -0
  78. package/src/services/topic/index.ts +4 -9
  79. package/src/services/topic/server.ts +68 -0
  80. package/src/services/user/index.ts +4 -9
  81. package/src/services/user/server.ts +28 -0
  82. package/tests/setup-db.ts +7 -0
  83. package/vitest.config.ts +2 -1
  84. package/vitest.server.config.ts +23 -0
@@ -0,0 +1,595 @@
1
+ import { eq, inArray } from 'drizzle-orm';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+
4
+ import { getTestDBInstance } from '@/database/server/core/dbForTest';
5
+
6
+ import {
7
+ NewSession,
8
+ SessionItem,
9
+ agents,
10
+ agentsToSessions,
11
+ messages,
12
+ plugins,
13
+ sessionGroups,
14
+ sessions,
15
+ topics,
16
+ users,
17
+ } from '../../schemas/lobechat';
18
+ import { idGenerator } from '../../utils/idGenerator';
19
+ import { SessionModel } from '../session';
20
+
21
+ let serverDB = await getTestDBInstance();
22
+
23
+ vi.mock('@/database/server/core/db', async () => ({
24
+ get serverDB() {
25
+ return serverDB;
26
+ },
27
+ }));
28
+
29
+ const userId = 'session-user';
30
+ const sessionModel = new SessionModel(userId);
31
+
32
+ beforeEach(async () => {
33
+ await serverDB.delete(plugins);
34
+ await serverDB.delete(users);
35
+ // 并创建初始用户
36
+ await serverDB.insert(users).values({ id: userId });
37
+ });
38
+
39
+ afterEach(async () => {
40
+ // 在每个测试用例之后, 清空用户表 (应该会自动级联删除所有数据)
41
+ await serverDB.delete(users);
42
+ });
43
+
44
+ describe('SessionModel', () => {
45
+ describe('query', () => {
46
+ it('should query sessions by user ID', async () => {
47
+ // 创建一些测试数据
48
+ await serverDB.insert(users).values([{ id: '456' }]);
49
+
50
+ await serverDB.insert(sessions).values([
51
+ { id: '1', userId, updatedAt: new Date('2023-01-01') },
52
+ { id: '2', userId, updatedAt: new Date('2023-02-01') },
53
+ { id: '3', userId: '456', updatedAt: new Date('2023-03-01') },
54
+ ]);
55
+
56
+ // 调用 query 方法
57
+ const result = await sessionModel.query();
58
+
59
+ // 断言结果
60
+ expect(result).toHaveLength(2);
61
+ expect(result[0].id).toBe('2');
62
+ expect(result[1].id).toBe('1');
63
+ });
64
+
65
+ it('should query sessions with pagination', async () => {
66
+ // create test data
67
+ await serverDB.insert(sessions).values([
68
+ { id: '1', userId, updatedAt: new Date('2023-01-01') },
69
+ { id: '2', userId, updatedAt: new Date('2023-02-01') },
70
+ { id: '3', userId, updatedAt: new Date('2023-03-01') },
71
+ ]);
72
+
73
+ // should return 2 sessions
74
+ const result1 = await sessionModel.query({ current: 0, pageSize: 2 });
75
+ expect(result1).toHaveLength(2);
76
+
77
+ // should return only 1 session and it's the 2nd one
78
+ const result2 = await sessionModel.query({ current: 1, pageSize: 1 });
79
+ expect(result2).toHaveLength(1);
80
+ expect(result2[0].id).toBe('2');
81
+ });
82
+ });
83
+
84
+ describe('queryWithGroups', () => {
85
+ it('should return sessions grouped by group', async () => {
86
+ // 创建测试数据
87
+ await serverDB.transaction(async (trx) => {
88
+ await trx.insert(users).values([{ id: '456' }]);
89
+ await trx.insert(sessionGroups).values([
90
+ { userId, name: 'Group 1', id: 'group1' },
91
+ { userId, name: 'Group 2', id: 'group2' },
92
+ ]);
93
+ await trx.insert(sessions).values([
94
+ { id: '1', userId, groupId: 'group1' },
95
+ { id: '2', userId, groupId: 'group1' },
96
+ { id: '23', userId, groupId: 'group1', pinned: true },
97
+ { id: '3', userId, groupId: 'group2' },
98
+ { id: '4', userId },
99
+ { id: '5', userId, pinned: true },
100
+ { id: '7', userId: '456' },
101
+ ]);
102
+ });
103
+
104
+ // 调用 queryWithGroups 方法
105
+ const result = await sessionModel.queryWithGroups();
106
+
107
+ // 断言结果
108
+ expect(result.sessions).toHaveLength(6);
109
+ expect(result.sessionGroups).toHaveLength(2);
110
+ expect(result.sessionGroups[0].id).toBe('group1');
111
+ expect(result.sessionGroups[0].name).toBe('Group 1');
112
+
113
+ expect(result.sessionGroups[1].id).toBe('group2');
114
+ });
115
+
116
+ it('should return empty groups if no sessions', async () => {
117
+ // 调用 queryWithGroups 方法
118
+ const result = await sessionModel.queryWithGroups();
119
+
120
+ // 断言结果
121
+ expect(result.sessions).toHaveLength(0);
122
+ expect(result.sessionGroups).toHaveLength(0);
123
+ });
124
+ });
125
+
126
+ describe('findById', () => {
127
+ it('should find session by ID', async () => {
128
+ await serverDB.insert(sessions).values([
129
+ { id: '1', userId },
130
+ { id: '2', userId },
131
+ ]);
132
+
133
+ const result = await sessionModel.findByIdOrSlug('1');
134
+ expect(result?.id).toBe('1');
135
+ });
136
+
137
+ it('should return undefined if session not found', async () => {
138
+ await serverDB.insert(sessions).values([{ id: '1', userId }]);
139
+
140
+ const result = await sessionModel.findByIdOrSlug('2');
141
+ expect(result).toBeUndefined();
142
+ });
143
+
144
+ it('should find with agents', async () => {
145
+ await serverDB.transaction(async (trx) => {
146
+ await trx.insert(sessions).values([
147
+ { id: '1', userId },
148
+ { id: '2', userId },
149
+ ]);
150
+ await trx.insert(agents).values([
151
+ { id: 'a1', title: 'Agent1', userId },
152
+ { id: 'a2', title: 'Agent2', userId },
153
+ ]);
154
+
155
+ // @ts-ignore
156
+ await trx.insert(agentsToSessions).values([
157
+ { sessionId: '1', agentId: 'a1', userId },
158
+ { sessionId: '2', agentId: 'a2', userId },
159
+ ]);
160
+ });
161
+
162
+ const result = await sessionModel.findByIdOrSlug('2');
163
+
164
+ expect(result?.agent).toBeDefined();
165
+ expect(result?.agent.id).toEqual('a2');
166
+ });
167
+ });
168
+
169
+ // describe('getAgentConfigById', () => {
170
+ // it('should return agent config by id', async () => {
171
+ // await serverDB.transaction(async (trx) => {
172
+ // await trx.insert(agents).values([
173
+ // { id: '1', userId, model: 'gpt-3.5-turbo' },
174
+ // { id: '2', userId, model: 'gpt-3.5' },
175
+ // ]);
176
+ //
177
+ // // @ts-ignore
178
+ // await trx.insert(plugins).values([
179
+ // { id: 1, userId, identifier: 'abc', title: 'A1', locale: 'en-US', manifest: {} },
180
+ // { id: 2, userId, identifier: 'b2', title: 'A2', locale: 'en-US', manifest: {} },
181
+ // ]);
182
+ //
183
+ // await trx.insert(agentsPlugins).values([
184
+ // { agentId: '1', pluginId: 1 },
185
+ // { agentId: '2', pluginId: 2 },
186
+ // { agentId: '1', pluginId: 2 },
187
+ // ]);
188
+ // });
189
+ //
190
+ // const result = await sessionModel.getAgentConfigById('1');
191
+ //
192
+ // expect(result?.id).toBe('1');
193
+ // expect(result?.plugins).toBe(['abc', 'b2']);
194
+ // expect(result?.model).toEqual('gpt-3.5-turbo');
195
+ // expect(result?.chatConfig).toBeDefined();
196
+ // });
197
+ // });
198
+ describe('count', () => {
199
+ it('should return the count of sessions for the user', async () => {
200
+ // 创建测试数据
201
+ await serverDB.insert(users).values([{ id: '456' }]);
202
+ await serverDB.insert(sessions).values([
203
+ { id: '1', userId },
204
+ { id: '2', userId },
205
+ { id: '3', userId: '456' },
206
+ ]);
207
+
208
+ // 调用 count 方法
209
+ const result = await sessionModel.count();
210
+
211
+ // 断言结果
212
+ expect(result).toBe(2);
213
+ });
214
+
215
+ it('should return 0 if no sessions exist for the user', async () => {
216
+ // 创建测试数据
217
+ await serverDB.insert(users).values([{ id: '456' }]);
218
+ await serverDB.insert(sessions).values([{ id: '3', userId: '456' }]);
219
+
220
+ // 调用 count 方法
221
+ const result = await sessionModel.count();
222
+
223
+ // 断言结果
224
+ expect(result).toBe(0);
225
+ });
226
+ });
227
+
228
+ describe('queryByKeyword', () => {
229
+ it('should return an empty array if keyword is empty', async () => {
230
+ const result = await sessionModel.queryByKeyword('');
231
+ expect(result).toEqual([]);
232
+ });
233
+
234
+ it('should return sessions with matching title', async () => {
235
+ await serverDB.insert(sessions).values([
236
+ { id: '1', userId, title: 'Hello World', description: 'Some description' },
237
+ { id: '2', userId, title: 'Another Session', description: 'Another description' },
238
+ ]);
239
+
240
+ const result = await sessionModel.queryByKeyword('hello');
241
+ expect(result).toHaveLength(1);
242
+ expect(result[0].id).toBe('1');
243
+ });
244
+
245
+ it('should return sessions with matching description', async () => {
246
+ await serverDB.insert(sessions).values([
247
+ { id: '1', userId, title: 'Session 1', description: 'Description with keyword' },
248
+ { id: '2', userId, title: 'Session 2', description: 'Another description' },
249
+ ]);
250
+
251
+ const result = await sessionModel.queryByKeyword('keyword');
252
+ expect(result).toHaveLength(1);
253
+ expect(result[0].id).toBe('1');
254
+ });
255
+
256
+ it('should return sessions with matching title or description', async () => {
257
+ await serverDB.insert(sessions).values([
258
+ { id: '1', userId, title: 'Title with keyword', description: 'Some description' },
259
+ { id: '2', userId, title: 'Another Session', description: 'Description with keyword' },
260
+ { id: '3', userId, title: 'Third Session', description: 'Third description' },
261
+ ]);
262
+
263
+ const result = await sessionModel.queryByKeyword('keyword');
264
+ expect(result).toHaveLength(2);
265
+ expect(result.map((s) => s.id)).toEqual(['1', '2']);
266
+ });
267
+ });
268
+
269
+ describe('create', () => {
270
+ it('should create a new session', async () => {
271
+ // 调用 create 方法
272
+ const result = await sessionModel.create({
273
+ type: 'agent',
274
+ session: {
275
+ title: 'New Session',
276
+ },
277
+ config: { model: 'gpt-3.5-turbo' },
278
+ });
279
+
280
+ // 断言结果
281
+ const sessionId = result.id;
282
+ expect(sessionId).toBeDefined();
283
+ expect(sessionId.startsWith('ssn_')).toBeTruthy();
284
+ expect(result.userId).toBe(userId);
285
+ expect(result.type).toBe('agent');
286
+
287
+ const session = await sessionModel.findByIdOrSlug(sessionId);
288
+ expect(session).toBeDefined();
289
+ expect(session?.title).toEqual('New Session');
290
+ expect(session?.pinned).toBe(false);
291
+ expect(session?.agent?.model).toEqual('gpt-3.5-turbo');
292
+ });
293
+
294
+ it('should create a new session with custom ID', async () => {
295
+ // 调用 create 方法,传入自定义 ID
296
+ const customId = 'custom-id';
297
+ const result = await sessionModel.create({
298
+ type: 'agent',
299
+ config: { model: 'gpt-3.5-turbo' },
300
+ session: { title: 'New Session' },
301
+ id: customId,
302
+ });
303
+
304
+ // 断言结果
305
+ expect(result.id).toBe(customId);
306
+ });
307
+ });
308
+
309
+ describe.skip('batchCreate', () => {
310
+ it('should batch create sessions', async () => {
311
+ // 调用 batchCreate 方法
312
+ const sessions: NewSession[] = [
313
+ {
314
+ id: '1',
315
+ userId,
316
+ type: 'agent',
317
+ // config: { model: 'gpt-3.5-turbo' },
318
+ title: 'Session 1',
319
+ },
320
+ {
321
+ id: '2',
322
+ userId,
323
+ type: 'agent',
324
+ // config: { model: 'gpt-4' },
325
+ title: 'Session 2',
326
+ },
327
+ ];
328
+ const result = await sessionModel.batchCreate(sessions);
329
+
330
+ // 断言结果
331
+ expect(result.rowCount).toEqual(2);
332
+ });
333
+
334
+ it.skip('should set group to default if group does not exist', async () => {
335
+ // 调用 batchCreate 方法,传入不存在的 group
336
+ const sessions: NewSession[] = [
337
+ {
338
+ id: '1',
339
+ userId,
340
+ type: 'agent',
341
+ // config: { model: 'gpt-3.5-turbo' },
342
+ title: 'Session 1',
343
+ groupId: 'non-existent-group',
344
+ },
345
+ ];
346
+ const result = await sessionModel.batchCreate(sessions);
347
+
348
+ // 断言结果
349
+ // expect(result[0].group).toBe('default');
350
+ });
351
+ });
352
+
353
+ describe('duplicate', () => {
354
+ it.skip('should duplicate a session', async () => {
355
+ // 创建一个用户和一个 session
356
+ await serverDB.transaction(async (trx) => {
357
+ await trx
358
+ .insert(sessions)
359
+ .values({ id: '1', userId, type: 'agent', title: 'Original Session', pinned: true });
360
+ await trx.insert(agents).values({ id: 'agent-1', userId, model: 'gpt-3.5-turbo' });
361
+ await trx.insert(agentsToSessions).values({ agentId: 'agent-1', sessionId: '1' });
362
+ });
363
+
364
+ // 调用 duplicate 方法
365
+ const result = (await sessionModel.duplicate('1', 'Duplicated Session')) as SessionItem;
366
+
367
+ // 断言结果
368
+ expect(result.id).not.toBe('1');
369
+ expect(result.userId).toBe(userId);
370
+ expect(result.type).toBe('agent');
371
+
372
+ const session = await sessionModel.findByIdOrSlug(result.id);
373
+
374
+ expect(session).toBeDefined();
375
+ expect(session?.title).toEqual('Duplicated Session');
376
+ expect(session?.pinned).toBe(true);
377
+ expect(session?.agent?.model).toEqual('gpt-3.5-turbo');
378
+ });
379
+
380
+ it('should return undefined if session does not exist', async () => {
381
+ // 调用 duplicate 方法,传入不存在的 session ID
382
+ const result = await sessionModel.duplicate('non-existent-id');
383
+
384
+ // 断言结果
385
+ expect(result).toBeUndefined();
386
+ });
387
+ });
388
+
389
+ describe('update', () => {
390
+ it('should update a session', async () => {
391
+ // 创建一个测试 session
392
+ const sessionId = '123';
393
+ await serverDB.insert(sessions).values({ userId, id: sessionId, title: 'Test Session' });
394
+
395
+ // 调用 update 方法更新 session
396
+ const updatedSessions = await sessionModel.update(sessionId, {
397
+ title: 'Updated Test Session',
398
+ description: 'This is an updated test session',
399
+ });
400
+
401
+ // 断言更新后的结果
402
+ expect(updatedSessions).toHaveLength(1);
403
+ expect(updatedSessions[0].title).toBe('Updated Test Session');
404
+ expect(updatedSessions[0].description).toBe('This is an updated test session');
405
+ });
406
+
407
+ it('should not update a session if user ID does not match', async () => {
408
+ // 创建一个测试 session,但使用不同的 user ID
409
+ await serverDB.insert(users).values([{ id: '777' }]);
410
+
411
+ const sessionId = '123';
412
+
413
+ await serverDB
414
+ .insert(sessions)
415
+ .values({ userId: '777', id: sessionId, title: 'Test Session' });
416
+
417
+ // 尝试更新这个 session,应该不会有任何更新
418
+ const updatedSessions = await sessionModel.update(sessionId, {
419
+ title: 'Updated Test Session',
420
+ });
421
+
422
+ expect(updatedSessions).toHaveLength(0);
423
+ });
424
+ });
425
+
426
+ describe('delete', () => {
427
+ it('should handle deleting a session with no associated messages or topics', async () => {
428
+ // 创建测试数据
429
+ await serverDB.insert(sessions).values({ id: '1', userId });
430
+
431
+ // 调用 delete 方法
432
+ await sessionModel.delete('1');
433
+
434
+ // 断言删除结果
435
+ const result = await serverDB.select({ id: sessions.id }).from(sessions);
436
+
437
+ expect(result).toHaveLength(0);
438
+ });
439
+
440
+ it('should handle concurrent deletions gracefully', async () => {
441
+ // 创建测试数据
442
+ await serverDB.insert(sessions).values({ id: '1', userId });
443
+
444
+ // 并发调用 delete 方法
445
+ await Promise.all([sessionModel.delete('1'), sessionModel.delete('1')]);
446
+
447
+ // 断言删除结果
448
+ const result = await serverDB.select({ id: sessions.id }).from(sessions);
449
+
450
+ expect(result).toHaveLength(0);
451
+ });
452
+
453
+ it('should delete a session and its associated topics and messages', async () => {
454
+ // Create a session
455
+ const sessionId = '1';
456
+ await serverDB.insert(sessions).values({ id: sessionId, userId });
457
+
458
+ // Create some topics and messages associated with the session
459
+ await serverDB.insert(topics).values([
460
+ { id: '1', sessionId, userId },
461
+ { id: '2', sessionId, userId },
462
+ ]);
463
+ await serverDB.insert(messages).values([
464
+ { id: '1', sessionId, userId, role: 'user' },
465
+ { id: '2', sessionId, userId, role: 'assistant' },
466
+ ]);
467
+
468
+ // Delete the session
469
+ await sessionModel.delete(sessionId);
470
+
471
+ // Check that the session, topics, and messages are deleted
472
+ expect(await serverDB.select().from(sessions).where(eq(sessions.id, sessionId))).toHaveLength(
473
+ 0,
474
+ );
475
+ expect(
476
+ await serverDB.select().from(topics).where(eq(topics.sessionId, sessionId)),
477
+ ).toHaveLength(0);
478
+ expect(
479
+ await serverDB.select().from(messages).where(eq(messages.sessionId, sessionId)),
480
+ ).toHaveLength(0);
481
+ });
482
+
483
+ it('should not delete sessions belonging to other users', async () => {
484
+ // Create two users
485
+ const anotherUserId = idGenerator('user');
486
+ await serverDB.insert(users).values({ id: anotherUserId });
487
+
488
+ // Create a session for each user
489
+ await serverDB.insert(sessions).values([
490
+ { id: '1', userId },
491
+ { id: '2', userId: anotherUserId },
492
+ ]);
493
+
494
+ // Delete the session belonging to the current user
495
+ await sessionModel.delete('1');
496
+
497
+ // Check that only the session belonging to the current user is deleted
498
+ expect(await serverDB.select().from(sessions).where(eq(sessions.id, '1'))).toHaveLength(0);
499
+ expect(await serverDB.select().from(sessions).where(eq(sessions.id, '2'))).toHaveLength(1);
500
+ });
501
+ });
502
+
503
+ describe('batchDelete', () => {
504
+ it('should handle deleting sessions with no associated messages or topics', async () => {
505
+ // 创建测试数据
506
+ await serverDB.insert(sessions).values([
507
+ { id: '1', userId },
508
+ { id: '2', userId },
509
+ ]);
510
+
511
+ // 调用 batchDelete 方法
512
+ await sessionModel.batchDelete(['1', '2']);
513
+
514
+ // 断言删除结果
515
+ const result = await serverDB.select({ id: sessions.id }).from(sessions);
516
+
517
+ expect(result).toHaveLength(0);
518
+ });
519
+
520
+ it('should handle concurrent batch deletions gracefully', async () => {
521
+ // 创建测试数据
522
+ await serverDB.insert(sessions).values([
523
+ { id: '1', userId },
524
+ { id: '2', userId },
525
+ ]);
526
+
527
+ // 并发调用 batchDelete 方法
528
+ await Promise.all([
529
+ sessionModel.batchDelete(['1', '2']),
530
+ sessionModel.batchDelete(['1', '2']),
531
+ ]);
532
+
533
+ // 断言删除结果
534
+ const result = await serverDB.select({ id: sessions.id }).from(sessions);
535
+
536
+ expect(result).toHaveLength(0);
537
+ });
538
+
539
+ it('should delete multiple sessions and their associated topics and messages', async () => {
540
+ // Create some sessions
541
+ const sessionIds = ['1', '2', '3'];
542
+ await serverDB.insert(sessions).values(sessionIds.map((id) => ({ id, userId })));
543
+
544
+ // Create some topics and messages associated with the sessions
545
+ await serverDB.insert(topics).values([
546
+ { id: '1', sessionId: '1', userId },
547
+ { id: '2', sessionId: '2', userId },
548
+ { id: '3', sessionId: '3', userId },
549
+ ]);
550
+ await serverDB.insert(messages).values([
551
+ { id: '1', sessionId: '1', userId, role: 'user' },
552
+ { id: '2', sessionId: '2', userId, role: 'assistant' },
553
+ { id: '3', sessionId: '3', userId, role: 'user' },
554
+ ]);
555
+
556
+ // Delete the sessions
557
+ await sessionModel.batchDelete(sessionIds);
558
+
559
+ // Check that the sessions, topics, and messages are deleted
560
+ expect(
561
+ await serverDB.select().from(sessions).where(inArray(sessions.id, sessionIds)),
562
+ ).toHaveLength(0);
563
+ expect(
564
+ await serverDB.select().from(topics).where(inArray(topics.sessionId, sessionIds)),
565
+ ).toHaveLength(0);
566
+ expect(
567
+ await serverDB.select().from(messages).where(inArray(messages.sessionId, sessionIds)),
568
+ ).toHaveLength(0);
569
+ });
570
+
571
+ it('should not delete sessions belonging to other users', async () => {
572
+ // Create two users
573
+ await serverDB.insert(users).values([{ id: '456' }]);
574
+
575
+ // Create some sessions for each user
576
+ await serverDB.insert(sessions).values([
577
+ { id: '1', userId },
578
+ { id: '2', userId },
579
+ { id: '3', userId: '456' },
580
+ ]);
581
+
582
+ // Delete the sessions belonging to the current user
583
+ await sessionModel.batchDelete(['1', '2']);
584
+
585
+ // Check that only the sessions belonging to the current user are deleted
586
+ expect(
587
+ await serverDB
588
+ .select()
589
+ .from(sessions)
590
+ .where(inArray(sessions.id, ['1', '2'])),
591
+ ).toHaveLength(0);
592
+ expect(await serverDB.select().from(sessions).where(eq(sessions.id, '3'))).toHaveLength(1);
593
+ });
594
+ });
595
+ });