@lobehub/lobehub 2.0.0-next.137 → 2.0.0-next.138
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 +25 -0
- package/CLAUDE.md +38 -0
- package/changelog/v1.json +5 -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
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import { UserPreference } from '@lobechat/types';
|
|
2
|
+
import { eq } from 'drizzle-orm';
|
|
3
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
+
|
|
5
|
+
import { nextauthAccounts, userSettings, users } from '../../schemas';
|
|
6
|
+
import { LobeChatDatabase } from '../../type';
|
|
7
|
+
import { UserModel, UserNotFoundError } from '../user';
|
|
8
|
+
import { getTestDB } from './_util';
|
|
9
|
+
|
|
10
|
+
const userId = 'user-model-test';
|
|
11
|
+
const otherUserId = 'other-user-test';
|
|
12
|
+
|
|
13
|
+
const serverDB: LobeChatDatabase = await getTestDB();
|
|
14
|
+
const userModel = new UserModel(serverDB, userId);
|
|
15
|
+
|
|
16
|
+
// Mock decryptor function
|
|
17
|
+
const mockDecryptor = vi.fn().mockResolvedValue({});
|
|
18
|
+
|
|
19
|
+
describe('UserModel', () => {
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
await serverDB.delete(users);
|
|
22
|
+
await serverDB.insert(users).values([
|
|
23
|
+
{ id: userId, email: 'test@example.com', fullName: 'Test User' },
|
|
24
|
+
{ id: otherUserId, email: 'other@example.com' },
|
|
25
|
+
]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(async () => {
|
|
29
|
+
await serverDB.delete(users);
|
|
30
|
+
vi.clearAllMocks();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('getUserRegistrationDuration', () => {
|
|
34
|
+
it('should return registration duration for existing user', async () => {
|
|
35
|
+
const thirtyDaysAgo = new Date();
|
|
36
|
+
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30);
|
|
37
|
+
|
|
38
|
+
await serverDB.update(users).set({ createdAt: thirtyDaysAgo }).where(eq(users.id, userId));
|
|
39
|
+
|
|
40
|
+
const result = await userModel.getUserRegistrationDuration();
|
|
41
|
+
|
|
42
|
+
expect(result.duration).toBeGreaterThanOrEqual(30);
|
|
43
|
+
expect(result.createdAt).toBeDefined();
|
|
44
|
+
expect(result.updatedAt).toBeDefined();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('should return default duration for non-existent user', async () => {
|
|
48
|
+
const nonExistentUserModel = new UserModel(serverDB, 'non-existent');
|
|
49
|
+
|
|
50
|
+
const result = await nonExistentUserModel.getUserRegistrationDuration();
|
|
51
|
+
|
|
52
|
+
expect(result.duration).toBe(1);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('getUserState', () => {
|
|
57
|
+
it('should return user state with settings', async () => {
|
|
58
|
+
// Create user settings
|
|
59
|
+
await serverDB.insert(userSettings).values({
|
|
60
|
+
id: userId,
|
|
61
|
+
general: { fontSize: 14 },
|
|
62
|
+
tts: { voice: 'default' },
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const result = await userModel.getUserState(mockDecryptor);
|
|
66
|
+
|
|
67
|
+
expect(result.userId).toBe(userId);
|
|
68
|
+
expect(result.email).toBe('test@example.com');
|
|
69
|
+
expect(result.fullName).toBe('Test User');
|
|
70
|
+
expect(result.settings.general).toEqual({ fontSize: 14 });
|
|
71
|
+
expect(result.settings.tts).toEqual({ voice: 'default' });
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should throw UserNotFoundError for non-existent user', async () => {
|
|
75
|
+
const nonExistentUserModel = new UserModel(serverDB, 'non-existent');
|
|
76
|
+
|
|
77
|
+
await expect(nonExistentUserModel.getUserState(mockDecryptor)).rejects.toThrow(
|
|
78
|
+
UserNotFoundError,
|
|
79
|
+
);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should handle decryptor errors gracefully', async () => {
|
|
83
|
+
await serverDB.insert(userSettings).values({
|
|
84
|
+
id: userId,
|
|
85
|
+
keyVaults: 'encrypted-data',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const failingDecryptor = vi.fn().mockRejectedValue(new Error('Decryption failed'));
|
|
89
|
+
|
|
90
|
+
const result = await userModel.getUserState(failingDecryptor);
|
|
91
|
+
|
|
92
|
+
expect(result.settings.keyVaults).toEqual({});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe('getUserSSOProviders', () => {
|
|
97
|
+
it('should return SSO providers for user', async () => {
|
|
98
|
+
await serverDB.insert(nextauthAccounts).values({
|
|
99
|
+
userId,
|
|
100
|
+
provider: 'google',
|
|
101
|
+
providerAccountId: 'google-123',
|
|
102
|
+
type: 'oauth' as any,
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const result = await userModel.getUserSSOProviders();
|
|
106
|
+
|
|
107
|
+
expect(result).toHaveLength(1);
|
|
108
|
+
expect(result[0].provider).toBe('google');
|
|
109
|
+
expect(result[0].providerAccountId).toBe('google-123');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should return empty array when no SSO providers', async () => {
|
|
113
|
+
const result = await userModel.getUserSSOProviders();
|
|
114
|
+
|
|
115
|
+
expect(result).toHaveLength(0);
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('getUserSettings', () => {
|
|
120
|
+
it('should return user settings', async () => {
|
|
121
|
+
await serverDB.insert(userSettings).values({
|
|
122
|
+
id: userId,
|
|
123
|
+
general: { fontSize: 14 },
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
const result = await userModel.getUserSettings();
|
|
127
|
+
|
|
128
|
+
expect(result).toBeDefined();
|
|
129
|
+
expect(result?.general).toEqual({ fontSize: 14 });
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('should return undefined when no settings exist', async () => {
|
|
133
|
+
const result = await userModel.getUserSettings();
|
|
134
|
+
|
|
135
|
+
expect(result).toBeUndefined();
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
describe('updateUser', () => {
|
|
140
|
+
it('should update user properties', async () => {
|
|
141
|
+
await userModel.updateUser({
|
|
142
|
+
fullName: 'Updated Name',
|
|
143
|
+
avatar: 'https://example.com/avatar.jpg',
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
const updated = await serverDB.query.users.findFirst({
|
|
147
|
+
where: eq(users.id, userId),
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
expect(updated?.fullName).toBe('Updated Name');
|
|
151
|
+
expect(updated?.avatar).toBe('https://example.com/avatar.jpg');
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe('deleteSetting', () => {
|
|
156
|
+
it('should delete user settings', async () => {
|
|
157
|
+
await serverDB.insert(userSettings).values({
|
|
158
|
+
id: userId,
|
|
159
|
+
general: { fontSize: 14 },
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
await userModel.deleteSetting();
|
|
163
|
+
|
|
164
|
+
const settings = await serverDB.query.userSettings.findFirst({
|
|
165
|
+
where: eq(userSettings.id, userId),
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(settings).toBeUndefined();
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('updateSetting', () => {
|
|
173
|
+
it('should create settings if not exist', async () => {
|
|
174
|
+
await userModel.updateSetting({
|
|
175
|
+
general: { fontSize: 16 },
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
const settings = await serverDB.query.userSettings.findFirst({
|
|
179
|
+
where: eq(userSettings.id, userId),
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
expect(settings?.general).toEqual({ fontSize: 16 });
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('should update existing settings', async () => {
|
|
186
|
+
await serverDB.insert(userSettings).values({
|
|
187
|
+
id: userId,
|
|
188
|
+
general: { fontSize: 14 },
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
await userModel.updateSetting({
|
|
192
|
+
general: { fontSize: 18 },
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const settings = await serverDB.query.userSettings.findFirst({
|
|
196
|
+
where: eq(userSettings.id, userId),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
expect(settings?.general).toEqual({ fontSize: 18 });
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('updatePreference', () => {
|
|
204
|
+
it('should update user preference', async () => {
|
|
205
|
+
await userModel.updatePreference({
|
|
206
|
+
telemetry: false,
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
const user = await serverDB.query.users.findFirst({
|
|
210
|
+
where: eq(users.id, userId),
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
expect((user?.preference as UserPreference)?.telemetry).toBe(false);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should merge with existing preference', async () => {
|
|
217
|
+
await serverDB
|
|
218
|
+
.update(users)
|
|
219
|
+
.set({ preference: { telemetry: true, useCmdEnterToSend: true } })
|
|
220
|
+
.where(eq(users.id, userId));
|
|
221
|
+
|
|
222
|
+
await userModel.updatePreference({
|
|
223
|
+
telemetry: false,
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
const user = await serverDB.query.users.findFirst({
|
|
227
|
+
where: eq(users.id, userId),
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
const preference = user?.preference as UserPreference;
|
|
231
|
+
expect(preference?.telemetry).toBe(false);
|
|
232
|
+
expect(preference?.useCmdEnterToSend).toBe(true);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it('should do nothing for non-existent user', async () => {
|
|
236
|
+
const nonExistentUserModel = new UserModel(serverDB, 'non-existent');
|
|
237
|
+
|
|
238
|
+
await expect(
|
|
239
|
+
nonExistentUserModel.updatePreference({ telemetry: false }),
|
|
240
|
+
).resolves.toBeUndefined();
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
describe('updateGuide', () => {
|
|
245
|
+
it('should update user guide preference', async () => {
|
|
246
|
+
await userModel.updateGuide({
|
|
247
|
+
moveSettingsToAvatar: true,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const user = await serverDB.query.users.findFirst({
|
|
251
|
+
where: eq(users.id, userId),
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
const preference = user?.preference as UserPreference;
|
|
255
|
+
expect(preference?.guide?.moveSettingsToAvatar).toBe(true);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('should do nothing for non-existent user', async () => {
|
|
259
|
+
const nonExistentUserModel = new UserModel(serverDB, 'non-existent');
|
|
260
|
+
|
|
261
|
+
await expect(
|
|
262
|
+
nonExistentUserModel.updateGuide({ moveSettingsToAvatar: true }),
|
|
263
|
+
).resolves.toBeUndefined();
|
|
264
|
+
});
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
describe('static methods', () => {
|
|
268
|
+
describe('makeSureUserExist', () => {
|
|
269
|
+
it('should create user if not exists', async () => {
|
|
270
|
+
await UserModel.makeSureUserExist(serverDB, 'new-user-id');
|
|
271
|
+
|
|
272
|
+
const user = await serverDB.query.users.findFirst({
|
|
273
|
+
where: eq(users.id, 'new-user-id'),
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
expect(user).toBeDefined();
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('should not throw if user already exists', async () => {
|
|
280
|
+
await expect(UserModel.makeSureUserExist(serverDB, userId)).resolves.not.toThrow();
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe('createUser', () => {
|
|
285
|
+
it('should create a new user', async () => {
|
|
286
|
+
const result = await UserModel.createUser(serverDB, {
|
|
287
|
+
id: 'brand-new-user',
|
|
288
|
+
email: 'new@example.com',
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
expect(result.duplicate).toBe(false);
|
|
292
|
+
expect(result.user?.id).toBe('brand-new-user');
|
|
293
|
+
expect(result.user?.email).toBe('new@example.com');
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
it('should return duplicate flag for existing user', async () => {
|
|
297
|
+
const result = await UserModel.createUser(serverDB, {
|
|
298
|
+
id: userId,
|
|
299
|
+
email: 'duplicate@example.com',
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
expect(result.duplicate).toBe(true);
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
describe('deleteUser', () => {
|
|
307
|
+
it('should delete a user', async () => {
|
|
308
|
+
await UserModel.deleteUser(serverDB, userId);
|
|
309
|
+
|
|
310
|
+
const user = await serverDB.query.users.findFirst({
|
|
311
|
+
where: eq(users.id, userId),
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
expect(user).toBeUndefined();
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
describe('findById', () => {
|
|
319
|
+
it('should find user by id', async () => {
|
|
320
|
+
const user = await UserModel.findById(serverDB, userId);
|
|
321
|
+
|
|
322
|
+
expect(user).toBeDefined();
|
|
323
|
+
expect(user?.email).toBe('test@example.com');
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('should return undefined for non-existent user', async () => {
|
|
327
|
+
const user = await UserModel.findById(serverDB, 'non-existent');
|
|
328
|
+
|
|
329
|
+
expect(user).toBeUndefined();
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
describe('findByEmail', () => {
|
|
334
|
+
it('should find user by email', async () => {
|
|
335
|
+
const user = await UserModel.findByEmail(serverDB, 'test@example.com');
|
|
336
|
+
|
|
337
|
+
expect(user).toBeDefined();
|
|
338
|
+
expect(user?.id).toBe(userId);
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('should return undefined for non-existent email', async () => {
|
|
342
|
+
const user = await UserModel.findByEmail(serverDB, 'nonexistent@example.com');
|
|
343
|
+
|
|
344
|
+
expect(user).toBeUndefined();
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
describe('getUserApiKeys', () => {
|
|
349
|
+
it('should return decrypted API keys', async () => {
|
|
350
|
+
await serverDB.insert(userSettings).values({
|
|
351
|
+
id: userId,
|
|
352
|
+
keyVaults: 'encrypted-keys',
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
const decryptor = vi.fn().mockResolvedValue({
|
|
356
|
+
openai: 'sk-xxx',
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const result = await UserModel.getUserApiKeys(serverDB, userId, decryptor);
|
|
360
|
+
|
|
361
|
+
expect(decryptor).toHaveBeenCalledWith('encrypted-keys', userId);
|
|
362
|
+
expect(result).toEqual({ openai: 'sk-xxx' });
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it('should throw UserNotFoundError when settings not found', async () => {
|
|
366
|
+
await expect(
|
|
367
|
+
UserModel.getUserApiKeys(serverDB, 'non-existent', mockDecryptor),
|
|
368
|
+
).rejects.toThrow(UserNotFoundError);
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
});
|
|
372
|
+
});
|