@lobehub/chat 1.99.1 → 1.99.3

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 (112) hide show
  1. package/.cursor/rules/project-introduce.mdc +1 -56
  2. package/.cursor/rules/testing-guide/db-model-test.mdc +453 -0
  3. package/.cursor/rules/testing-guide/electron-ipc-test.mdc +80 -0
  4. package/.cursor/rules/testing-guide/testing-guide.mdc +401 -0
  5. package/CHANGELOG.md +50 -0
  6. package/changelog/v1.json +18 -0
  7. package/docs/usage/providers/ai21.mdx +1 -1
  8. package/docs/usage/providers/ai21.zh-CN.mdx +1 -1
  9. package/docs/usage/providers/ai360.mdx +1 -1
  10. package/docs/usage/providers/ai360.zh-CN.mdx +1 -1
  11. package/docs/usage/providers/anthropic.mdx +1 -1
  12. package/docs/usage/providers/anthropic.zh-CN.mdx +1 -1
  13. package/docs/usage/providers/azure.mdx +1 -1
  14. package/docs/usage/providers/azure.zh-CN.mdx +1 -1
  15. package/docs/usage/providers/baichuan.mdx +1 -1
  16. package/docs/usage/providers/baichuan.zh-CN.mdx +1 -1
  17. package/docs/usage/providers/bedrock.mdx +1 -1
  18. package/docs/usage/providers/bedrock.zh-CN.mdx +1 -1
  19. package/docs/usage/providers/cloudflare.mdx +1 -1
  20. package/docs/usage/providers/cloudflare.zh-CN.mdx +1 -1
  21. package/docs/usage/providers/deepseek.mdx +1 -1
  22. package/docs/usage/providers/deepseek.zh-CN.mdx +1 -1
  23. package/docs/usage/providers/fal.mdx +69 -0
  24. package/docs/usage/providers/fal.zh-CN.mdx +68 -0
  25. package/docs/usage/providers/fireworksai.mdx +1 -1
  26. package/docs/usage/providers/fireworksai.zh-CN.mdx +1 -1
  27. package/docs/usage/providers/giteeai.mdx +1 -1
  28. package/docs/usage/providers/giteeai.zh-CN.mdx +1 -1
  29. package/docs/usage/providers/github.mdx +1 -1
  30. package/docs/usage/providers/github.zh-CN.mdx +1 -1
  31. package/docs/usage/providers/google.mdx +1 -1
  32. package/docs/usage/providers/google.zh-CN.mdx +1 -1
  33. package/docs/usage/providers/groq.mdx +1 -1
  34. package/docs/usage/providers/groq.zh-CN.mdx +1 -1
  35. package/docs/usage/providers/hunyuan.mdx +1 -1
  36. package/docs/usage/providers/hunyuan.zh-CN.mdx +1 -1
  37. package/docs/usage/providers/internlm.mdx +1 -1
  38. package/docs/usage/providers/internlm.zh-CN.mdx +1 -1
  39. package/docs/usage/providers/jina.mdx +1 -1
  40. package/docs/usage/providers/jina.zh-CN.mdx +1 -1
  41. package/docs/usage/providers/minimax.mdx +1 -1
  42. package/docs/usage/providers/minimax.zh-CN.mdx +1 -1
  43. package/docs/usage/providers/mistral.mdx +1 -1
  44. package/docs/usage/providers/mistral.zh-CN.mdx +1 -1
  45. package/docs/usage/providers/moonshot.mdx +1 -1
  46. package/docs/usage/providers/moonshot.zh-CN.mdx +1 -1
  47. package/docs/usage/providers/novita.mdx +1 -1
  48. package/docs/usage/providers/novita.zh-CN.mdx +1 -1
  49. package/docs/usage/providers/ollama.mdx +1 -1
  50. package/docs/usage/providers/ollama.zh-CN.mdx +1 -1
  51. package/docs/usage/providers/openai.mdx +4 -4
  52. package/docs/usage/providers/openai.zh-CN.mdx +4 -4
  53. package/docs/usage/providers/openrouter.mdx +1 -1
  54. package/docs/usage/providers/openrouter.zh-CN.mdx +1 -1
  55. package/docs/usage/providers/perplexity.mdx +1 -1
  56. package/docs/usage/providers/perplexity.zh-CN.mdx +1 -1
  57. package/docs/usage/providers/ppio.mdx +1 -1
  58. package/docs/usage/providers/ppio.zh-CN.mdx +1 -1
  59. package/docs/usage/providers/qiniu.mdx +1 -1
  60. package/docs/usage/providers/qiniu.zh-CN.mdx +1 -1
  61. package/docs/usage/providers/qwen.mdx +1 -1
  62. package/docs/usage/providers/qwen.zh-CN.mdx +1 -1
  63. package/docs/usage/providers/sambanova.mdx +1 -1
  64. package/docs/usage/providers/sambanova.zh-CN.mdx +1 -1
  65. package/docs/usage/providers/sensenova.mdx +1 -1
  66. package/docs/usage/providers/sensenova.zh-CN.mdx +1 -1
  67. package/docs/usage/providers/siliconcloud.mdx +1 -1
  68. package/docs/usage/providers/siliconcloud.zh-CN.mdx +1 -1
  69. package/docs/usage/providers/spark.mdx +1 -1
  70. package/docs/usage/providers/spark.zh-CN.mdx +1 -1
  71. package/docs/usage/providers/stepfun.mdx +1 -1
  72. package/docs/usage/providers/stepfun.zh-CN.mdx +1 -1
  73. package/docs/usage/providers/taichu.mdx +1 -1
  74. package/docs/usage/providers/taichu.zh-CN.mdx +1 -1
  75. package/docs/usage/providers/togetherai.mdx +1 -1
  76. package/docs/usage/providers/togetherai.zh-CN.mdx +1 -1
  77. package/docs/usage/providers/upstage.mdx +1 -1
  78. package/docs/usage/providers/upstage.zh-CN.mdx +1 -1
  79. package/docs/usage/providers/vllm.mdx +1 -1
  80. package/docs/usage/providers/vllm.zh-CN.mdx +1 -1
  81. package/docs/usage/providers/wenxin.mdx +1 -1
  82. package/docs/usage/providers/wenxin.zh-CN.mdx +1 -1
  83. package/docs/usage/providers/xai.mdx +1 -1
  84. package/docs/usage/providers/xai.zh-CN.mdx +1 -1
  85. package/docs/usage/providers/zeroone.mdx +1 -1
  86. package/docs/usage/providers/zeroone.zh-CN.mdx +1 -1
  87. package/docs/usage/providers/zhipu.mdx +1 -1
  88. package/docs/usage/providers/zhipu.zh-CN.mdx +1 -1
  89. package/package.json +3 -3
  90. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/index.tsx +9 -4
  91. package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/index.tsx +1 -1
  92. package/src/app/[variants]/(main)/image/features/ImageWorkspace/EmptyState.tsx +12 -26
  93. package/src/config/aiModels/openai.ts +24 -9
  94. package/src/libs/model-runtime/BaseAI.ts +1 -0
  95. package/src/libs/model-runtime/hunyuan/index.ts +4 -6
  96. package/src/libs/model-runtime/novita/__snapshots__/index.test.ts.snap +18 -0
  97. package/src/libs/model-runtime/openai/__snapshots__/index.test.ts.snap +28 -0
  98. package/src/libs/model-runtime/openai/index.test.ts +1 -338
  99. package/src/libs/model-runtime/openai/index.ts +0 -127
  100. package/src/libs/model-runtime/openrouter/__snapshots__/index.test.ts.snap +3 -0
  101. package/src/libs/model-runtime/ppio/__snapshots__/index.test.ts.snap +2 -0
  102. package/src/libs/model-runtime/utils/modelParse.ts +1 -0
  103. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.test.ts +364 -12
  104. package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +145 -43
  105. package/src/libs/model-runtime/utils/openaiHelpers.test.ts +151 -0
  106. package/src/libs/model-runtime/utils/openaiHelpers.ts +26 -1
  107. package/src/libs/model-runtime/xai/index.ts +1 -4
  108. package/src/store/aiInfra/slices/aiModel/action.ts +1 -1
  109. package/src/store/aiInfra/slices/aiProvider/action.ts +5 -2
  110. package/src/types/aiModel.ts +1 -0
  111. package/src/types/llm.ts +3 -1
  112. package/.cursor/rules/testing-guide.mdc +0 -881
@@ -55,59 +55,4 @@ pnpm install
55
55
  # !: don't any build script to check weather code can work after modify
56
56
  ```
57
57
 
58
- check [testing guide](./testing-guide.mdc) to learn test scripts.
59
-
60
- ## Project Description
61
-
62
- You are developing an open-source, modern-design AI chat framework: lobe chat.
63
-
64
- Emoji logo: 🤯
65
-
66
- ## Project Technologies Stack
67
-
68
- read [package.json](mdc:package.json) to know all npm packages you can use. read [folder-structure.mdx](mdc:docs/development/basic/folder-structure.mdx) to learn project structure.
69
-
70
- The project uses the following technologies:
71
-
72
- - pnpm as package manager
73
- - Next.js 15 for frontend and backend, using app router instead of pages router
74
- - react 19, using hooks, functional components, react server components
75
- - TypeScript programming language
76
- - antd, @lobehub/ui for component framework
77
- - antd-style for css-in-js framework
78
- - react-layout-kit for flex layout
79
- - react-i18next for i18n
80
- - lucide-react, @ant-design/icons for icons
81
- - @lobehub/icons for AI provider/model logo icon
82
- - @formkit/auto-animate for react list animation
83
- - zustand for global state management
84
- - nuqs for type-safe search params state manager
85
- - SWR for react data fetch
86
- - aHooks for react hooks library
87
- - dayjs for date and time library
88
- - lodash-es for utility library
89
- - fast-deep-equal for deep comparison of JavaScript objects
90
- - zod for data validation
91
- - TRPC for type safe backend
92
- - PGLite for client DB and PostgreSQL for backend DB
93
- - Drizzle ORM
94
- - Vitest for testing, testing-library for react component test
95
- - Prettier for code formatting
96
- - ESLint for code linting
97
- - Cursor AI for code editing and AI coding assistance
98
-
99
- Note: All tools and libraries used are the latest versions. The application only needs to be compatible with the latest browsers;
100
-
101
- ## Often used npm scripts
102
-
103
- ```bash
104
- # type check
105
- bun type-check
106
-
107
- # install dependencies
108
- pnpm install
109
-
110
- # !: don't any build script to check weather code can work after modify
111
- ```
112
-
113
- check [testing guide](./testing-guide.mdc) to learn test scripts.
58
+ check [testing guide](./testing-guide/testing-guide.mdc) to learn test scripts.
@@ -0,0 +1,453 @@
1
+ ---
2
+ globs: src/database/**/*.test.ts
3
+ alwaysApply: false
4
+ ---
5
+
6
+ ## 🗃️ 数据库 Model 测试指南
7
+
8
+ ### 测试环境选择 💡
9
+
10
+ 数据库 Model 层通过环境变量控制数据库类型,在两种测试环境下有不同的数据库后端:客户端环境 (PGLite) 和 服务端环境 (PostgreSQL)
11
+
12
+ ### ⚠️ 双环境验证要求
13
+
14
+ **对于所有 Model 测试,必须在两个环境下都验证通过**:
15
+
16
+ #### 完整验证流程
17
+
18
+ ```bash
19
+ # 1. 先在客户端环境测试(快速验证)
20
+ npx vitest run --config vitest.config.ts src/database/models/__tests__/myModel.test.ts
21
+
22
+ # 2. 再在服务端环境测试(兼容性验证)
23
+ npx vitest run --config vitest.config.server.ts src/database/models/__tests__/myModel.test.ts
24
+ ```
25
+
26
+ ### 创建新 Model 测试的最佳实践 📋
27
+
28
+ #### 1. 参考现有实现和测试模板
29
+
30
+ 创建新 Model 测试前,**必须先参考现有的实现模式**:
31
+
32
+ - **Model 实现参考**:
33
+ - **测试模板参考**:
34
+ - **复杂示例参考**:
35
+
36
+ #### 2. 用户权限检查 - 安全第一 🔒
37
+
38
+ 这是**最关键的安全要求**。所有涉及用户数据的操作都必须包含用户权限检查:
39
+
40
+ **❌ 错误示例 - 存在安全漏洞**:
41
+
42
+ ```typescript
43
+ // 危险:缺少用户权限检查,任何用户都能操作任何数据
44
+ update = async (id: string, data: Partial<MyModel>) => {
45
+ return this.db
46
+ .update(myTable)
47
+ .set(data)
48
+ .where(eq(myTable.id, id)) // ❌ 只检查 ID,没有检查 userId
49
+ .returning();
50
+ };
51
+ ```
52
+
53
+ **✅ 正确示例 - 安全的实现**:
54
+
55
+ ```typescript
56
+ // 安全:必须同时匹配 ID 和 userId
57
+ update = async (id: string, data: Partial<MyModel>) => {
58
+ return this.db
59
+ .update(myTable)
60
+ .set(data)
61
+ .where(
62
+ and(
63
+ eq(myTable.id, id),
64
+ eq(myTable.userId, this.userId), // ✅ 用户权限检查
65
+ ),
66
+ )
67
+ .returning();
68
+ };
69
+ ```
70
+
71
+ **必须进行用户权限检查的方法**:
72
+
73
+ - `update()` - 更新操作
74
+ - `delete()` - 删除操作
75
+ - `findById()` - 查找特定记录
76
+ - 任何涉及特定记录的查询或修改操作
77
+
78
+ #### 3. 测试文件结构和必测场景
79
+
80
+ **基本测试结构**:
81
+
82
+ ```typescript
83
+ // @vitest-environment node
84
+ describe('MyModel', () => {
85
+ describe('create', () => {
86
+ it('should create a new record');
87
+ it('should handle edge cases');
88
+ });
89
+
90
+ describe('queryAll', () => {
91
+ it('should return records for current user only');
92
+ it('should handle empty results');
93
+ });
94
+
95
+ describe('update', () => {
96
+ it('should update own records');
97
+ it('should NOT update other users records'); // 🔒 安全测试
98
+ });
99
+
100
+ describe('delete', () => {
101
+ it('should delete own records');
102
+ it('should NOT delete other users records'); // 🔒 安全测试
103
+ });
104
+
105
+ describe('user isolation', () => {
106
+ it('should enforce user data isolation'); // 🔒 核心安全测试
107
+ });
108
+ });
109
+ ```
110
+
111
+ **必须测试的安全场景** 🔒:
112
+
113
+ ```typescript
114
+ it('should not update records of other users', async () => {
115
+ // 创建其他用户的记录
116
+ const [otherUserRecord] = await serverDB
117
+ .insert(myTable)
118
+ .values({ userId: 'other-user', data: 'original' })
119
+ .returning();
120
+
121
+ // 尝试更新其他用户的记录
122
+ const result = await myModel.update(otherUserRecord.id, { data: 'hacked' });
123
+
124
+ // 应该返回 undefined 或空数组(因为权限检查失败)
125
+ expect(result).toBeUndefined();
126
+
127
+ // 验证原始数据未被修改
128
+ const unchanged = await serverDB.query.myTable.findFirst({
129
+ where: eq(myTable.id, otherUserRecord.id),
130
+ });
131
+ expect(unchanged?.data).toBe('original'); // 数据应该保持不变
132
+ });
133
+ ```
134
+
135
+ #### 4. Mock 外部依赖服务
136
+
137
+ 如果 Model 依赖外部服务(如 FileService),需要正确 Mock:
138
+
139
+ **设置 Mock**:
140
+
141
+ ```typescript
142
+ // 在文件顶部设置 Mock
143
+ const mockGetFullFileUrl = vi.fn();
144
+ vi.mock('@/server/services/file', () => ({
145
+ FileService: vi.fn().mockImplementation(() => ({
146
+ getFullFileUrl: mockGetFullFileUrl,
147
+ })),
148
+ }));
149
+
150
+ // 在 beforeEach 中重置和配置 Mock
151
+ beforeEach(async () => {
152
+ vi.clearAllMocks();
153
+ mockGetFullFileUrl.mockImplementation((url: string) => `https://example.com/${url}`);
154
+ });
155
+ ```
156
+
157
+ **验证 Mock 调用**:
158
+
159
+ ```typescript
160
+ it('should process URLs through FileService', async () => {
161
+ // ... 测试逻辑
162
+
163
+ // 验证 Mock 被正确调用
164
+ expect(mockGetFullFileUrl).toHaveBeenCalledWith('expected-url');
165
+ expect(mockGetFullFileUrl).toHaveBeenCalledTimes(1);
166
+ });
167
+ ```
168
+
169
+ #### 5. 数据库状态管理
170
+
171
+ **正确的数据清理模式**:
172
+
173
+ ```typescript
174
+ const userId = 'test-user';
175
+ const otherUserId = 'other-user';
176
+
177
+ beforeEach(async () => {
178
+ // 清理用户表(级联删除相关数据)
179
+ await serverDB.delete(users);
180
+
181
+ // 创建测试用户
182
+ await serverDB.insert(users).values([{ id: userId }, { id: otherUserId }]);
183
+ });
184
+
185
+ afterEach(async () => {
186
+ // 清理测试数据
187
+ await serverDB.delete(users);
188
+ });
189
+ ```
190
+
191
+ #### 6. 测试数据类型和外键约束处理 ⚠️
192
+
193
+ **必须使用 Schema 导出的类型**:
194
+
195
+ ```typescript
196
+ // ✅ 正确:使用 schema 导出的类型
197
+ import { NewGeneration, NewGenerationBatch } from '../../schemas';
198
+
199
+ const testBatch: NewGenerationBatch = {
200
+ userId,
201
+ generationTopicId: 'test-topic-id',
202
+ provider: 'test-provider',
203
+ model: 'test-model',
204
+ prompt: 'Test prompt for image generation',
205
+ width: 1024,
206
+ height: 1024,
207
+ config: {
208
+ /* ... */
209
+ },
210
+ };
211
+
212
+ const testGeneration: NewGeneration = {
213
+ id: 'test-gen-id',
214
+ generationBatchId: 'test-batch-id',
215
+ asyncTaskId: null, // 处理外键约束
216
+ fileId: null, // 处理外键约束
217
+ seed: 12345,
218
+ userId,
219
+ };
220
+ ```
221
+
222
+ ```typescript
223
+ // ❌ 错误:没有类型声明或使用错误类型
224
+ const testBatch = {
225
+ // 缺少类型声明
226
+ generationTopicId: 'test-topic-id',
227
+ // ...
228
+ };
229
+
230
+ const testGeneration = {
231
+ // 缺少类型声明
232
+ asyncTaskId: 'invalid-uuid', // 外键约束错误
233
+ fileId: 'non-existent-file', // 外键约束错误
234
+ // ...
235
+ };
236
+ ```
237
+
238
+ **外键约束处理策略**:
239
+
240
+ 1. **使用 null 值**: 对于可选的外键字段,使用 null 避免约束错误
241
+ 2. **创建关联记录**: 如果需要测试关联关系,先创建被引用的记录
242
+ 3. **理解约束关系**: 了解哪些字段有外键约束,避免引用不存在的记录
243
+
244
+ ```typescript
245
+ // 外键约束处理示例
246
+ beforeEach(async () => {
247
+ // 清理数据库
248
+ await serverDB.delete(users);
249
+
250
+ // 创建测试用户
251
+ await serverDB.insert(users).values([{ id: userId }]);
252
+
253
+ // 如果需要测试文件关联,创建文件记录
254
+ if (needsFileAssociation) {
255
+ await serverDB.insert(files).values({
256
+ id: 'test-file-id',
257
+ userId,
258
+ name: 'test.jpg',
259
+ url: 'test-url',
260
+ size: 1024,
261
+ fileType: 'image/jpeg',
262
+ });
263
+ }
264
+ });
265
+ ```
266
+
267
+ **排序测试的可预测性**:
268
+
269
+ ```typescript
270
+ // ✅ 正确:使用明确的时间戳确保排序结果可预测
271
+ it('should find batches by topic id in correct order', async () => {
272
+ const oldDate = new Date('2024-01-01T10:00:00Z');
273
+ const newDate = new Date('2024-01-02T10:00:00Z');
274
+
275
+ const batch1 = { ...testBatch, prompt: 'First batch', userId, createdAt: oldDate };
276
+ const batch2 = { ...testBatch, prompt: 'Second batch', userId, createdAt: newDate };
277
+
278
+ await serverDB.insert(generationBatches).values([batch1, batch2]);
279
+
280
+ const results = await generationBatchModel.findByTopicId(testTopic.id);
281
+
282
+ expect(results[0].prompt).toBe('Second batch'); // 最新优先 (desc order)
283
+ expect(results[1].prompt).toBe('First batch');
284
+ });
285
+ ```
286
+
287
+ ```typescript
288
+ // ❌ 错误:依赖数据库的默认时间戳,结果不可预测
289
+ it('should find batches by topic id', async () => {
290
+ const batch1 = { ...testBatch, prompt: 'First batch', userId };
291
+ const batch2 = { ...testBatch, prompt: 'Second batch', userId };
292
+
293
+ await serverDB.insert(generationBatches).values([batch1, batch2]);
294
+
295
+ // 插入顺序和数据库时间戳可能不一致,导致测试不稳定
296
+ const results = await generationBatchModel.findByTopicId(testTopic.id);
297
+ expect(results[0].prompt).toBe('Second batch'); // 可能失败
298
+ });
299
+ ```
300
+
301
+ ### 常见问题和解决方案 💡
302
+
303
+ #### 问题 1:权限检查缺失导致安全漏洞
304
+
305
+ **现象**: 测试失败,用户能修改其他用户的数据 **解决**: 在 Model 的 `update` 和 `delete` 方法中添加 `and(eq(table.id, id), eq(table.userId, this.userId))`
306
+
307
+ #### 问题 2:Mock 未生效或验证失败
308
+
309
+ **现象**: `undefined is not a spy` 错误 **解决**: 检查 Mock 设置位置和方式,确保在测试文件顶部设置,在 `beforeEach` 中重置
310
+
311
+ #### 问题 3:测试数据污染
312
+
313
+ **现象**: 测试间相互影响,结果不稳定 **解决**: 在 `beforeEach` 和 `afterEach` 中正确清理数据库状态
314
+
315
+ #### 问题 4:外部依赖导致测试失败
316
+
317
+ **现象**: 因为真实的外部服务调用导致测试不稳定 **解决**: Mock 所有外部依赖,使测试更可控和快速
318
+
319
+ #### 问题 5:外键约束违反导致测试失败
320
+
321
+ **现象**: `insert or update on table "xxx" violates foreign key constraint` **解决**:
322
+
323
+ - 将可选外键字段设为 `null` 而不是无效的字符串值
324
+ - 或者先创建被引用的记录,再创建当前记录
325
+
326
+ ```typescript
327
+ // ❌ 错误:无效的外键值
328
+ const testData = {
329
+ asyncTaskId: 'invalid-uuid', // 表中不存在此记录
330
+ fileId: 'non-existent-file', // 表中不存在此记录
331
+ };
332
+
333
+ // ✅ 正确:使用 null 值
334
+ const testData = {
335
+ asyncTaskId: null, // 避免外键约束
336
+ fileId: null, // 避免外键约束
337
+ };
338
+
339
+ // ✅ 或者:先创建被引用的记录
340
+ beforeEach(async () => {
341
+ const [asyncTask] = await serverDB.insert(asyncTasks).values({
342
+ id: 'valid-task-id',
343
+ status: 'pending',
344
+ type: 'generation',
345
+ }).returning();
346
+
347
+ const testData = {
348
+ asyncTaskId: asyncTask.id, // 使用有效的外键值
349
+ };
350
+ });
351
+ ```
352
+
353
+ #### 问题 6:排序测试结果不一致
354
+
355
+ **现象**: 相同的测试有时通过,有时失败,特别是涉及排序的测试 **解决**: 使用明确的时间戳,不要依赖数据库的默认时间戳
356
+
357
+ ```typescript
358
+ // ❌ 错误:依赖插入顺序和默认时间戳
359
+ await serverDB.insert(table).values([data1, data2]); // 时间戳不可预测
360
+
361
+ // ✅ 正确:明确指定时间戳
362
+ const oldDate = new Date('2024-01-01T10:00:00Z');
363
+ const newDate = new Date('2024-01-02T10:00:00Z');
364
+ await serverDB.insert(table).values([
365
+ { ...data1, createdAt: oldDate },
366
+ { ...data2, createdAt: newDate },
367
+ ]);
368
+ ```
369
+
370
+ #### 问题 7:Mock 验证失败或调用次数不匹配
371
+
372
+ **现象**: `expect(mockFunction).toHaveBeenCalledWith(...)` 失败 **解决**:
373
+
374
+ - 检查 Mock 函数的实际调用参数和期望参数是否完全匹配
375
+ - 确认 Mock 在正确的时机被重置和配置
376
+ - 使用 `toHaveBeenCalledTimes()` 验证调用次数
377
+
378
+ ```typescript
379
+ // 在 beforeEach 中正确配置 Mock
380
+ beforeEach(() => {
381
+ vi.clearAllMocks(); // 重置所有 Mock
382
+
383
+ mockGetFullFileUrl.mockImplementation((url: string) => `https://example.com/${url}`);
384
+ mockTransformGeneration.mockResolvedValue({
385
+ id: 'test-id',
386
+ // ... 其他字段
387
+ });
388
+ });
389
+
390
+ // 测试中验证 Mock 调用
391
+ it('should call FileService with correct parameters', async () => {
392
+ await model.someMethod();
393
+
394
+ // 验证调用参数
395
+ expect(mockGetFullFileUrl).toHaveBeenCalledWith('expected-url');
396
+ // 验证调用次数
397
+ expect(mockGetFullFileUrl).toHaveBeenCalledTimes(1);
398
+ });
399
+ ```
400
+
401
+ ### Model 测试检查清单 ✅
402
+
403
+ 创建 Model 测试时,请确保以下各项都已完成:
404
+
405
+ #### 🔧 基础配置
406
+
407
+ - [ ] **双环境验证** - 在客户端环境 (vitest.config.ts) 和服务端环境 (vitest.config.server.ts) 下都测试通过
408
+ - [ ] 参考了 `_template.ts` 和现有 Model 的实现模式
409
+ - [ ] **使用正确的 Schema 类型** - 测试数据使用 `NewXxx` 类型声明,如 `NewGenerationBatch`、`NewGeneration`
410
+
411
+ #### 🔒 安全测试
412
+
413
+ - [ ] **所有涉及用户数据的操作都包含用户权限检查**
414
+ - [ ] 包含了用户权限隔离的安全测试
415
+ - [ ] 测试了用户无法访问其他用户数据的场景
416
+
417
+ #### 🗃️ 数据处理
418
+
419
+ - [ ] **正确处理外键约束** - 使用 `null` 值或先创建被引用记录
420
+ - [ ] **排序测试使用明确时间戳** - 不依赖数据库默认时间,确保结果可预测
421
+ - [ ] 在 `beforeEach` 和 `afterEach` 中正确管理数据库状态
422
+ - [ ] 所有测试都能独立运行且互不干扰
423
+
424
+ #### 🎭 Mock 和外部依赖
425
+
426
+ - [ ] 正确 Mock 了外部依赖服务 (如 FileService、GenerationModel)
427
+ - [ ] 在 `beforeEach` 中重置和配置 Mock
428
+ - [ ] 验证了 Mock 服务的调用参数和次数
429
+ - [ ] 测试了外部服务错误场景的处理
430
+
431
+ #### 📋 测试覆盖
432
+
433
+ - [ ] 测试覆盖了所有主要方法 (create, query, update, delete)
434
+ - [ ] 测试了边界条件和错误场景
435
+ - [ ] 包含了空结果处理的测试
436
+ - [ ] **确认两个环境下的测试结果一致**
437
+
438
+ #### 🚨 常见问题检查
439
+
440
+ - [ ] 没有外键约束违反错误
441
+ - [ ] 排序测试结果稳定可预测
442
+ - [ ] Mock 验证无失败
443
+ - [ ] 无测试数据污染问题
444
+
445
+ ### 安全警告 ⚠️
446
+
447
+ **数据库 Model 层是安全的第一道防线**。如果 Model 层缺少用户权限检查:
448
+
449
+ 1. **任何用户都能访问和修改其他用户的数据**
450
+ 2. **即使上层有权限检查,也可能被绕过**
451
+ 3. **可能导致严重的数据泄露和安全事故**
452
+
453
+ 因此,**每个涉及用户数据的 Model 方法都必须包含用户权限检查,且必须有对应的安全测试来验证这些检查的有效性**。
@@ -0,0 +1,80 @@
1
+ ---
2
+ description: Electron IPC 接口测试策略
3
+ alwaysApply: false
4
+ ---
5
+
6
+ ### Electron IPC 接口测试策略 🖥️
7
+
8
+ 对于涉及 Electron IPC 接口的测试,由于提供真实的 Electron 环境比较复杂,采用 **Mock 返回值** 的方式进行测试。
9
+
10
+ #### 基本 Mock 设置
11
+
12
+ ```typescript
13
+ import { vi } from 'vitest';
14
+
15
+ import { electronIpcClient } from '@/server/modules/ElectronIPCClient';
16
+
17
+ // Mock Electron IPC 客户端
18
+ vi.mock('@/server/modules/ElectronIPCClient', () => ({
19
+ electronIpcClient: {
20
+ getFilePathById: vi.fn(),
21
+ deleteFiles: vi.fn(),
22
+ // 根据需要添加其他 IPC 方法
23
+ },
24
+ }));
25
+ ```
26
+
27
+ #### 在测试中设置 Mock 行为
28
+
29
+ ```typescript
30
+ beforeEach(() => {
31
+ // 重置所有 Mock
32
+ vi.resetAllMocks();
33
+
34
+ // 设置默认的 Mock 返回值
35
+ vi.mocked(electronIpcClient.getFilePathById).mockResolvedValue('/path/to/file.txt');
36
+ vi.mocked(electronIpcClient.deleteFiles).mockResolvedValue({
37
+ success: true,
38
+ });
39
+ });
40
+ ```
41
+
42
+ #### 测试不同场景的示例
43
+
44
+ ```typescript
45
+ it('应该处理文件删除成功的情况', async () => {
46
+ // 设置成功场景的 Mock
47
+ vi.mocked(electronIpcClient.deleteFiles).mockResolvedValue({
48
+ success: true,
49
+ });
50
+
51
+ const result = await service.deleteFiles(['desktop://file1.txt']);
52
+
53
+ expect(electronIpcClient.deleteFiles).toHaveBeenCalledWith(['desktop://file1.txt']);
54
+ expect(result.success).toBe(true);
55
+ });
56
+
57
+ it('应该处理文件删除失败的情况', async () => {
58
+ // 设置失败场景的 Mock
59
+ vi.mocked(electronIpcClient.deleteFiles).mockRejectedValue(new Error('删除失败'));
60
+
61
+ const result = await service.deleteFiles(['desktop://file1.txt']);
62
+
63
+ expect(result.success).toBe(false);
64
+ expect(result.errors).toBeDefined();
65
+ });
66
+ ```
67
+
68
+ #### Mock 策略的优势
69
+
70
+ 1. **环境简化**: 避免了复杂的 Electron 环境搭建
71
+ 2. **测试可控**: 可以精确控制 IPC 调用的返回值和行为
72
+ 3. **场景覆盖**: 容易测试各种成功/失败场景
73
+ 4. **执行速度**: Mock 调用比真实 IPC 调用更快
74
+
75
+ #### 注意事项
76
+
77
+ - **Mock 准确性**: 确保 Mock 的行为与真实 IPC 接口行为一致
78
+ - **类型安全**: 使用 `vi.mocked()` 确保类型安全
79
+ - **Mock 重置**: 在 `beforeEach` 中重置 Mock 状态,避免测试间干扰
80
+ - **调用验证**: 不仅要验证返回值,还要验证 IPC 方法是否被正确调用