@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.
- package/.cursor/rules/project-introduce.mdc +1 -56
- package/.cursor/rules/testing-guide/db-model-test.mdc +453 -0
- package/.cursor/rules/testing-guide/electron-ipc-test.mdc +80 -0
- package/.cursor/rules/testing-guide/testing-guide.mdc +401 -0
- package/CHANGELOG.md +50 -0
- package/changelog/v1.json +18 -0
- package/docs/usage/providers/ai21.mdx +1 -1
- package/docs/usage/providers/ai21.zh-CN.mdx +1 -1
- package/docs/usage/providers/ai360.mdx +1 -1
- package/docs/usage/providers/ai360.zh-CN.mdx +1 -1
- package/docs/usage/providers/anthropic.mdx +1 -1
- package/docs/usage/providers/anthropic.zh-CN.mdx +1 -1
- package/docs/usage/providers/azure.mdx +1 -1
- package/docs/usage/providers/azure.zh-CN.mdx +1 -1
- package/docs/usage/providers/baichuan.mdx +1 -1
- package/docs/usage/providers/baichuan.zh-CN.mdx +1 -1
- package/docs/usage/providers/bedrock.mdx +1 -1
- package/docs/usage/providers/bedrock.zh-CN.mdx +1 -1
- package/docs/usage/providers/cloudflare.mdx +1 -1
- package/docs/usage/providers/cloudflare.zh-CN.mdx +1 -1
- package/docs/usage/providers/deepseek.mdx +1 -1
- package/docs/usage/providers/deepseek.zh-CN.mdx +1 -1
- package/docs/usage/providers/fal.mdx +69 -0
- package/docs/usage/providers/fal.zh-CN.mdx +68 -0
- package/docs/usage/providers/fireworksai.mdx +1 -1
- package/docs/usage/providers/fireworksai.zh-CN.mdx +1 -1
- package/docs/usage/providers/giteeai.mdx +1 -1
- package/docs/usage/providers/giteeai.zh-CN.mdx +1 -1
- package/docs/usage/providers/github.mdx +1 -1
- package/docs/usage/providers/github.zh-CN.mdx +1 -1
- package/docs/usage/providers/google.mdx +1 -1
- package/docs/usage/providers/google.zh-CN.mdx +1 -1
- package/docs/usage/providers/groq.mdx +1 -1
- package/docs/usage/providers/groq.zh-CN.mdx +1 -1
- package/docs/usage/providers/hunyuan.mdx +1 -1
- package/docs/usage/providers/hunyuan.zh-CN.mdx +1 -1
- package/docs/usage/providers/internlm.mdx +1 -1
- package/docs/usage/providers/internlm.zh-CN.mdx +1 -1
- package/docs/usage/providers/jina.mdx +1 -1
- package/docs/usage/providers/jina.zh-CN.mdx +1 -1
- package/docs/usage/providers/minimax.mdx +1 -1
- package/docs/usage/providers/minimax.zh-CN.mdx +1 -1
- package/docs/usage/providers/mistral.mdx +1 -1
- package/docs/usage/providers/mistral.zh-CN.mdx +1 -1
- package/docs/usage/providers/moonshot.mdx +1 -1
- package/docs/usage/providers/moonshot.zh-CN.mdx +1 -1
- package/docs/usage/providers/novita.mdx +1 -1
- package/docs/usage/providers/novita.zh-CN.mdx +1 -1
- package/docs/usage/providers/ollama.mdx +1 -1
- package/docs/usage/providers/ollama.zh-CN.mdx +1 -1
- package/docs/usage/providers/openai.mdx +4 -4
- package/docs/usage/providers/openai.zh-CN.mdx +4 -4
- package/docs/usage/providers/openrouter.mdx +1 -1
- package/docs/usage/providers/openrouter.zh-CN.mdx +1 -1
- package/docs/usage/providers/perplexity.mdx +1 -1
- package/docs/usage/providers/perplexity.zh-CN.mdx +1 -1
- package/docs/usage/providers/ppio.mdx +1 -1
- package/docs/usage/providers/ppio.zh-CN.mdx +1 -1
- package/docs/usage/providers/qiniu.mdx +1 -1
- package/docs/usage/providers/qiniu.zh-CN.mdx +1 -1
- package/docs/usage/providers/qwen.mdx +1 -1
- package/docs/usage/providers/qwen.zh-CN.mdx +1 -1
- package/docs/usage/providers/sambanova.mdx +1 -1
- package/docs/usage/providers/sambanova.zh-CN.mdx +1 -1
- package/docs/usage/providers/sensenova.mdx +1 -1
- package/docs/usage/providers/sensenova.zh-CN.mdx +1 -1
- package/docs/usage/providers/siliconcloud.mdx +1 -1
- package/docs/usage/providers/siliconcloud.zh-CN.mdx +1 -1
- package/docs/usage/providers/spark.mdx +1 -1
- package/docs/usage/providers/spark.zh-CN.mdx +1 -1
- package/docs/usage/providers/stepfun.mdx +1 -1
- package/docs/usage/providers/stepfun.zh-CN.mdx +1 -1
- package/docs/usage/providers/taichu.mdx +1 -1
- package/docs/usage/providers/taichu.zh-CN.mdx +1 -1
- package/docs/usage/providers/togetherai.mdx +1 -1
- package/docs/usage/providers/togetherai.zh-CN.mdx +1 -1
- package/docs/usage/providers/upstage.mdx +1 -1
- package/docs/usage/providers/upstage.zh-CN.mdx +1 -1
- package/docs/usage/providers/vllm.mdx +1 -1
- package/docs/usage/providers/vllm.zh-CN.mdx +1 -1
- package/docs/usage/providers/wenxin.mdx +1 -1
- package/docs/usage/providers/wenxin.zh-CN.mdx +1 -1
- package/docs/usage/providers/xai.mdx +1 -1
- package/docs/usage/providers/xai.zh-CN.mdx +1 -1
- package/docs/usage/providers/zeroone.mdx +1 -1
- package/docs/usage/providers/zeroone.zh-CN.mdx +1 -1
- package/docs/usage/providers/zhipu.mdx +1 -1
- package/docs/usage/providers/zhipu.zh-CN.mdx +1 -1
- package/package.json +3 -3
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/components/MultiImagesUpload/index.tsx +9 -4
- package/src/app/[variants]/(main)/image/@menu/features/ConfigPanel/index.tsx +1 -1
- package/src/app/[variants]/(main)/image/features/ImageWorkspace/EmptyState.tsx +12 -26
- package/src/config/aiModels/openai.ts +24 -9
- package/src/libs/model-runtime/BaseAI.ts +1 -0
- package/src/libs/model-runtime/hunyuan/index.ts +4 -6
- package/src/libs/model-runtime/novita/__snapshots__/index.test.ts.snap +18 -0
- package/src/libs/model-runtime/openai/__snapshots__/index.test.ts.snap +28 -0
- package/src/libs/model-runtime/openai/index.test.ts +1 -338
- package/src/libs/model-runtime/openai/index.ts +0 -127
- package/src/libs/model-runtime/openrouter/__snapshots__/index.test.ts.snap +3 -0
- package/src/libs/model-runtime/ppio/__snapshots__/index.test.ts.snap +2 -0
- package/src/libs/model-runtime/utils/modelParse.ts +1 -0
- package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.test.ts +364 -12
- package/src/libs/model-runtime/utils/openaiCompatibleFactory/index.ts +145 -43
- package/src/libs/model-runtime/utils/openaiHelpers.test.ts +151 -0
- package/src/libs/model-runtime/utils/openaiHelpers.ts +26 -1
- package/src/libs/model-runtime/xai/index.ts +1 -4
- package/src/store/aiInfra/slices/aiModel/action.ts +1 -1
- package/src/store/aiInfra/slices/aiProvider/action.ts +5 -2
- package/src/types/aiModel.ts +1 -0
- package/src/types/llm.ts +3 -1
- 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 方法是否被正确调用
|