@lobehub/lobehub 2.0.0-next.173 → 2.0.0-next.175
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/db-migrations.mdc +10 -4
- package/.cursor/rules/drizzle-schema-style-guide.mdc +33 -17
- package/.cursor/rules/hotkey.mdc +161 -0
- package/.cursor/rules/i18n.mdc +2 -5
- package/.cursor/rules/project-introduce.mdc +3 -2
- package/.cursor/rules/project-structure.mdc +33 -37
- package/.cursor/rules/react.mdc +155 -0
- package/.cursor/rules/recent-data-usage.mdc +138 -0
- package/.cursor/rules/rules-index.mdc +1 -1
- package/.cursor/rules/testing-guide/agent-runtime-e2e.mdc +285 -0
- package/.cursor/rules/testing-guide/zustand-store-action-test.mdc +11 -16
- package/.cursor/rules/typescript.mdc +0 -4
- package/.cursor/rules/zustand-action-patterns.mdc +137 -169
- package/.cursor/rules/zustand-slice-organization.mdc +16 -8
- package/.husky/pre-commit +1 -1
- package/.i18nrc.js +3 -1
- package/AGENTS.md +3 -2
- package/CHANGELOG.md +42 -0
- package/CLAUDE.md +10 -3
- package/GEMINI.md +2 -1
- package/README.md +9 -9
- package/README.zh-CN.md +12 -12
- package/changelog/v1.json +14 -0
- package/docs/development/database-schema.dbml +18 -3
- package/docs/development/state-management/state-management-selectors.mdx +1 -1
- package/glossary.json +8 -0
- package/package.json +3 -3
- package/packages/database/migrations/0063_add_columns_for_several_tables.sql +30 -0
- package/packages/database/migrations/0064_add_agents_session_group_id.sql +7 -0
- package/packages/database/migrations/meta/0063_snapshot.json +9113 -0
- package/packages/database/migrations/meta/0064_snapshot.json +9143 -0
- package/packages/database/migrations/meta/_journal.json +14 -0
- package/packages/database/src/core/migrations.json +39 -0
- package/packages/database/src/schemas/_helpers.ts +5 -1
- package/packages/database/src/schemas/agent.ts +8 -1
- package/packages/database/src/schemas/aiInfra.ts +18 -3
- package/packages/database/src/schemas/message.ts +11 -6
- package/packages/database/src/schemas/topic.ts +17 -2
- package/packages/database/src/schemas/user.ts +5 -2
- package/packages/database/src/schemas/userMemories.ts +9 -8
- package/packages/types/src/topic/thread.ts +24 -0
- package/packages/types/src/user/index.ts +1 -0
- package/packages/types/src/user/onboarding.ts +16 -0
- package/vitest.config.mts +3 -0
- package/.cursor/rules/group-chat.mdc +0 -35
- package/.cursor/rules/react-component.mdc +0 -173
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
# Recent Data 使用指南
|
|
2
|
+
|
|
3
|
+
## 概述
|
|
4
|
+
|
|
5
|
+
Recent 数据(recentTopics, recentResources, recentPages)存储在 session store 中,可以在应用的任何地方访问。
|
|
6
|
+
|
|
7
|
+
## 数据初始化
|
|
8
|
+
|
|
9
|
+
在应用顶层(如 `RecentHydration.tsx`)中初始化所有 recent 数据:
|
|
10
|
+
|
|
11
|
+
```tsx
|
|
12
|
+
import { useInitRecentPage } from '@/hooks/useInitRecentPage';
|
|
13
|
+
import { useInitRecentResource } from '@/hooks/useInitRecentResource';
|
|
14
|
+
import { useInitRecentTopic } from '@/hooks/useInitRecentTopic';
|
|
15
|
+
|
|
16
|
+
const App = () => {
|
|
17
|
+
// 初始化所有 recent 数据
|
|
18
|
+
useInitRecentTopic();
|
|
19
|
+
useInitRecentResource();
|
|
20
|
+
useInitRecentPage();
|
|
21
|
+
|
|
22
|
+
return <YourComponents />;
|
|
23
|
+
};
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## 使用方式
|
|
27
|
+
|
|
28
|
+
### 方式一:直接从 Store 读取(推荐用于多处使用)
|
|
29
|
+
|
|
30
|
+
在任何组件中直接访问 store 中的数据:
|
|
31
|
+
|
|
32
|
+
```tsx
|
|
33
|
+
import { useSessionStore } from '@/store/session';
|
|
34
|
+
import { recentSelectors } from '@/store/session/selectors';
|
|
35
|
+
|
|
36
|
+
const Component = () => {
|
|
37
|
+
// 读取数据
|
|
38
|
+
const recentTopics = useSessionStore(recentSelectors.recentTopics);
|
|
39
|
+
const isInit = useSessionStore(recentSelectors.isRecentTopicsInit);
|
|
40
|
+
|
|
41
|
+
if (!isInit) return <div>Loading...</div>;
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div>
|
|
45
|
+
{recentTopics.map(topic => (
|
|
46
|
+
<div key={topic.id}>{topic.title}</div>
|
|
47
|
+
))}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
};
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### 方式二:使用 Hook 返回的数据(用于单一组件)
|
|
54
|
+
|
|
55
|
+
```tsx
|
|
56
|
+
import { useInitRecentTopic } from '@/hooks/useInitRecentTopic';
|
|
57
|
+
|
|
58
|
+
const Component = () => {
|
|
59
|
+
const { data: recentTopics, isLoading } = useInitRecentTopic();
|
|
60
|
+
|
|
61
|
+
if (isLoading) return <div>Loading...</div>;
|
|
62
|
+
|
|
63
|
+
return <div>{/* 使用 recentTopics */}</div>;
|
|
64
|
+
};
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## 可用的 Selectors
|
|
68
|
+
|
|
69
|
+
### Recent Topics (最近话题)
|
|
70
|
+
|
|
71
|
+
```tsx
|
|
72
|
+
import { recentSelectors } from '@/store/session/selectors';
|
|
73
|
+
|
|
74
|
+
// 数据
|
|
75
|
+
const recentTopics = useSessionStore(recentSelectors.recentTopics);
|
|
76
|
+
// 类型: RecentTopic[]
|
|
77
|
+
|
|
78
|
+
// 初始化状态
|
|
79
|
+
const isInit = useSessionStore(recentSelectors.isRecentTopicsInit);
|
|
80
|
+
// 类型: boolean
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**RecentTopic 类型:**
|
|
84
|
+
```typescript
|
|
85
|
+
interface RecentTopic {
|
|
86
|
+
agent: {
|
|
87
|
+
avatar: string | null;
|
|
88
|
+
backgroundColor: string | null;
|
|
89
|
+
id: string;
|
|
90
|
+
title: string | null;
|
|
91
|
+
} | null;
|
|
92
|
+
id: string;
|
|
93
|
+
title: string | null;
|
|
94
|
+
updatedAt: Date;
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Recent Resources (最近文件)
|
|
99
|
+
|
|
100
|
+
```tsx
|
|
101
|
+
import { recentSelectors } from '@/store/session/selectors';
|
|
102
|
+
|
|
103
|
+
// 数据
|
|
104
|
+
const recentResources = useSessionStore(recentSelectors.recentResources);
|
|
105
|
+
// 类型: FileListItem[]
|
|
106
|
+
|
|
107
|
+
// 初始化状态
|
|
108
|
+
const isInit = useSessionStore(recentSelectors.isRecentResourcesInit);
|
|
109
|
+
// 类型: boolean
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Recent Pages (最近页面)
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
import { recentSelectors } from '@/store/session/selectors';
|
|
116
|
+
|
|
117
|
+
// 数据
|
|
118
|
+
const recentPages = useSessionStore(recentSelectors.recentPages);
|
|
119
|
+
// 类型: any[]
|
|
120
|
+
|
|
121
|
+
// 初始化状态
|
|
122
|
+
const isInit = useSessionStore(recentSelectors.isRecentPagesInit);
|
|
123
|
+
// 类型: boolean
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## 特性
|
|
127
|
+
|
|
128
|
+
1. **自动登录检测**:只有在用户登录时才会加载数据
|
|
129
|
+
2. **数据缓存**:数据存储在 store 中,多处使用无需重复加载
|
|
130
|
+
3. **自动刷新**:使用 SWR,在用户重新聚焦时自动刷新(5分钟间隔)
|
|
131
|
+
4. **类型安全**:完整的 TypeScript 类型定义
|
|
132
|
+
|
|
133
|
+
## 最佳实践
|
|
134
|
+
|
|
135
|
+
1. **初始化位置**:在应用顶层统一初始化所有 recent 数据
|
|
136
|
+
2. **数据访问**:使用 selectors 从 store 读取数据
|
|
137
|
+
3. **多处使用**:同一数据在多个组件中使用时,推荐使用方式一(直接从 store 读取)
|
|
138
|
+
4. **性能优化**:使用 selector 确保只有相关数据变化时才重新渲染
|
|
@@ -14,7 +14,7 @@ All following rules are saved under `.cursor/rules/` directory:
|
|
|
14
14
|
|
|
15
15
|
## Frontend
|
|
16
16
|
|
|
17
|
-
- `react
|
|
17
|
+
- `react.mdc` – React component style guide and conventions
|
|
18
18
|
- `i18n.mdc` – Internationalization guide using react-i18next
|
|
19
19
|
- `typescript.mdc` – TypeScript code style guide
|
|
20
20
|
- `packages/react-layout-kit.mdc` – Usage guide for react-layout-kit
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
# Agent Runtime E2E 测试指南
|
|
2
|
+
|
|
3
|
+
本文档描述 Agent Runtime 端到端测试的核心原则和实施方法。
|
|
4
|
+
|
|
5
|
+
## 核心原则
|
|
6
|
+
|
|
7
|
+
### 1. 最小化 Mock 原则
|
|
8
|
+
|
|
9
|
+
E2E 测试的目标是尽可能接近真实运行环境。因此,我们只 Mock **三个外部依赖**:
|
|
10
|
+
|
|
11
|
+
| 依赖 | Mock 方式 | 说明 |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| **Database** | PGLite | 使用 `@lobechat/database/test-utils` 提供的内存数据库 |
|
|
14
|
+
| **Redis** | InMemoryAgentStateManager | Mock `AgentStateManager` 使用内存实现 |
|
|
15
|
+
| **Redis** | InMemoryStreamEventManager | Mock `StreamEventManager` 使用内存实现 |
|
|
16
|
+
|
|
17
|
+
**不 Mock 的部分:**
|
|
18
|
+
|
|
19
|
+
- `model-bank` - 使用真实的模型配置数据
|
|
20
|
+
- `Mecha` (AgentToolsEngine, ContextEngineering) - 使用真实逻辑
|
|
21
|
+
- `AgentRuntimeService` - 使用真实逻辑
|
|
22
|
+
- `AgentRuntimeCoordinator` - 使用真实逻辑
|
|
23
|
+
|
|
24
|
+
### 2. 使用 vi.spyOn 而非 vi.mock
|
|
25
|
+
|
|
26
|
+
不同测试场景需要不同的 LLM 响应。使用 `vi.spyOn` 可以:
|
|
27
|
+
|
|
28
|
+
- 在每个测试中灵活控制返回值
|
|
29
|
+
- 便于测试不同场景(纯文本、tool calls、错误等)
|
|
30
|
+
- 避免全局 mock 导致的测试隔离问题
|
|
31
|
+
|
|
32
|
+
### 3. 默认模型使用 gpt-5
|
|
33
|
+
|
|
34
|
+
- `model-bank` 中肯定有该模型的数据
|
|
35
|
+
- 避免短期内因模型更新需要修改测试
|
|
36
|
+
|
|
37
|
+
## 技术实现
|
|
38
|
+
|
|
39
|
+
### 数据库设置
|
|
40
|
+
|
|
41
|
+
```typescript
|
|
42
|
+
import { LobeChatDatabase } from '@lobechat/database';
|
|
43
|
+
import { getTestDB } from '@lobechat/database/test-utils';
|
|
44
|
+
|
|
45
|
+
let testDB: LobeChatDatabase;
|
|
46
|
+
|
|
47
|
+
beforeEach(async () => {
|
|
48
|
+
testDB = await getTestDB();
|
|
49
|
+
});
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### OpenAI Response Mock Helper
|
|
53
|
+
|
|
54
|
+
创建一个 helper 函数来生成 OpenAI 格式的流式响应:
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
/**
|
|
58
|
+
* 创建 OpenAI 格式的流式响应
|
|
59
|
+
*/
|
|
60
|
+
export const createOpenAIStreamResponse = (options: {
|
|
61
|
+
content?: string;
|
|
62
|
+
toolCalls?: Array<{
|
|
63
|
+
id: string;
|
|
64
|
+
name: string;
|
|
65
|
+
arguments: string;
|
|
66
|
+
}>;
|
|
67
|
+
finishReason?: 'stop' | 'tool_calls';
|
|
68
|
+
}) => {
|
|
69
|
+
const { content, toolCalls, finishReason = 'stop' } = options;
|
|
70
|
+
|
|
71
|
+
return new Response(
|
|
72
|
+
new ReadableStream({
|
|
73
|
+
start(controller) {
|
|
74
|
+
const encoder = new TextEncoder();
|
|
75
|
+
|
|
76
|
+
// 发送内容 chunk
|
|
77
|
+
if (content) {
|
|
78
|
+
const chunk = {
|
|
79
|
+
id: 'chatcmpl-mock',
|
|
80
|
+
object: 'chat.completion.chunk',
|
|
81
|
+
model: 'gpt-5',
|
|
82
|
+
choices: [
|
|
83
|
+
{
|
|
84
|
+
index: 0,
|
|
85
|
+
delta: { content },
|
|
86
|
+
finish_reason: null,
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
};
|
|
90
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 发送 tool_calls chunk
|
|
94
|
+
if (toolCalls) {
|
|
95
|
+
for (const tool of toolCalls) {
|
|
96
|
+
const chunk = {
|
|
97
|
+
id: 'chatcmpl-mock',
|
|
98
|
+
object: 'chat.completion.chunk',
|
|
99
|
+
model: 'gpt-5',
|
|
100
|
+
choices: [
|
|
101
|
+
{
|
|
102
|
+
index: 0,
|
|
103
|
+
delta: {
|
|
104
|
+
tool_calls: [
|
|
105
|
+
{
|
|
106
|
+
index: 0,
|
|
107
|
+
id: tool.id,
|
|
108
|
+
type: 'function',
|
|
109
|
+
function: {
|
|
110
|
+
name: tool.name,
|
|
111
|
+
arguments: tool.arguments,
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
finish_reason: null,
|
|
117
|
+
},
|
|
118
|
+
],
|
|
119
|
+
};
|
|
120
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 发送完成 chunk
|
|
125
|
+
const finishChunk = {
|
|
126
|
+
id: 'chatcmpl-mock',
|
|
127
|
+
object: 'chat.completion.chunk',
|
|
128
|
+
model: 'gpt-5',
|
|
129
|
+
choices: [
|
|
130
|
+
{
|
|
131
|
+
index: 0,
|
|
132
|
+
delta: {},
|
|
133
|
+
finish_reason: finishReason,
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
};
|
|
137
|
+
controller.enqueue(encoder.encode(`data: ${JSON.stringify(finishChunk)}\n\n`));
|
|
138
|
+
controller.enqueue(encoder.encode('data: [DONE]\n\n'));
|
|
139
|
+
controller.close();
|
|
140
|
+
},
|
|
141
|
+
}),
|
|
142
|
+
{ headers: { 'content-type': 'text/event-stream' } },
|
|
143
|
+
);
|
|
144
|
+
};
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### 内存状态管理
|
|
148
|
+
|
|
149
|
+
使用依赖注入替代 Redis:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import {
|
|
153
|
+
InMemoryAgentStateManager,
|
|
154
|
+
InMemoryStreamEventManager,
|
|
155
|
+
} from '@/server/modules/AgentRuntime';
|
|
156
|
+
import { AgentRuntimeService } from '@/server/services/agentRuntime';
|
|
157
|
+
|
|
158
|
+
const stateManager = new InMemoryAgentStateManager();
|
|
159
|
+
const streamEventManager = new InMemoryStreamEventManager();
|
|
160
|
+
|
|
161
|
+
const service = new AgentRuntimeService(serverDB, userId, {
|
|
162
|
+
coordinatorOptions: {
|
|
163
|
+
stateManager,
|
|
164
|
+
streamEventManager,
|
|
165
|
+
},
|
|
166
|
+
queueService: null, // 禁用 QStash 队列,使用 executeSync
|
|
167
|
+
streamEventManager,
|
|
168
|
+
});
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Mock OpenAI API
|
|
172
|
+
|
|
173
|
+
在测试中使用 `vi.spyOn` mock fetch:
|
|
174
|
+
|
|
175
|
+
```typescript
|
|
176
|
+
import { vi } from 'vitest';
|
|
177
|
+
|
|
178
|
+
// 在测试文件顶部或 beforeEach 中
|
|
179
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch');
|
|
180
|
+
|
|
181
|
+
// 在具体测试中设置返回值
|
|
182
|
+
it('should handle text response', async () => {
|
|
183
|
+
fetchSpy.mockResolvedValueOnce(createOpenAIStreamResponse({ content: '杭州今天天气晴朗' }));
|
|
184
|
+
|
|
185
|
+
// ... 执行测试
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('should handle tool calls', async () => {
|
|
189
|
+
fetchSpy.mockResolvedValueOnce(
|
|
190
|
+
createOpenAIStreamResponse({
|
|
191
|
+
toolCalls: [
|
|
192
|
+
{
|
|
193
|
+
id: 'call_123',
|
|
194
|
+
name: 'lobe-web-browsing____search____builtin',
|
|
195
|
+
arguments: JSON.stringify({ query: '杭州天气' }),
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
finishReason: 'tool_calls',
|
|
199
|
+
}),
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// ... 执行测试
|
|
203
|
+
});
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
## 测试场景
|
|
207
|
+
|
|
208
|
+
### 1. 基本对话测试
|
|
209
|
+
|
|
210
|
+
```typescript
|
|
211
|
+
describe('Basic Chat', () => {
|
|
212
|
+
it('should complete a simple conversation', async () => {
|
|
213
|
+
fetchSpy.mockResolvedValueOnce(
|
|
214
|
+
createOpenAIStreamResponse({ content: 'Hello! How can I help you?' }),
|
|
215
|
+
);
|
|
216
|
+
|
|
217
|
+
const result = await service.createOperation({
|
|
218
|
+
agentConfig: { model: 'gpt-5', provider: 'openai' },
|
|
219
|
+
initialMessages: [{ role: 'user', content: 'Hi' }],
|
|
220
|
+
// ...
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const finalState = await service.executeSync(result.operationId);
|
|
224
|
+
expect(finalState.status).toBe('done');
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### 2. Tool 调用测试
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
describe('Tool Calls', () => {
|
|
233
|
+
it('should execute web-browsing tool', async () => {
|
|
234
|
+
// 第一次调用:LLM 返回 tool_calls
|
|
235
|
+
fetchSpy.mockResolvedValueOnce(
|
|
236
|
+
createOpenAIStreamResponse({
|
|
237
|
+
toolCalls: [
|
|
238
|
+
{
|
|
239
|
+
id: 'call_123',
|
|
240
|
+
name: 'lobe-web-browsing____search____builtin',
|
|
241
|
+
arguments: JSON.stringify({ query: '杭州天气' }),
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
finishReason: 'tool_calls',
|
|
245
|
+
}),
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
// 第二次调用:处理 tool 结果后的响应
|
|
249
|
+
fetchSpy.mockResolvedValueOnce(
|
|
250
|
+
createOpenAIStreamResponse({ content: '根据搜索结果,杭州今天...' }),
|
|
251
|
+
);
|
|
252
|
+
|
|
253
|
+
// ... 执行测试
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### 3. 错误处理测试
|
|
259
|
+
|
|
260
|
+
```typescript
|
|
261
|
+
describe('Error Handling', () => {
|
|
262
|
+
it('should handle API errors gracefully', async () => {
|
|
263
|
+
fetchSpy.mockRejectedValueOnce(new Error('API rate limit exceeded'));
|
|
264
|
+
|
|
265
|
+
// ... 执行测试并验证错误处理
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
## 文件组织
|
|
271
|
+
|
|
272
|
+
```
|
|
273
|
+
src/server/routers/lambda/__tests__/integration/
|
|
274
|
+
├── setup.ts # 测试设置工具
|
|
275
|
+
├── aiAgent.integration.test.ts # 现有集成测试
|
|
276
|
+
├── aiAgent.e2e.test.ts # E2E 测试
|
|
277
|
+
└── helpers/
|
|
278
|
+
└── openaiMock.ts # OpenAI mock helper
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## 注意事项
|
|
282
|
+
|
|
283
|
+
1. **测试隔离**:每个测试后清理 `InMemoryAgentStateManager` 和 `InMemoryStreamEventManager`
|
|
284
|
+
2. **超时设置**:E2E 测试可能需要更长的超时时间
|
|
285
|
+
3. **调试**:使用 `DEBUG=lobe-server:*` 环境变量查看详细日志
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
description: Best practices for testing Zustand store actions
|
|
3
|
-
globs:
|
|
3
|
+
globs: 'src/store/**/*.test.ts'
|
|
4
4
|
alwaysApply: false
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -15,6 +15,7 @@ import { act, renderHook } from '@testing-library/react';
|
|
|
15
15
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
16
16
|
|
|
17
17
|
import { messageService } from '@/services/message';
|
|
18
|
+
|
|
18
19
|
import { useChatStore } from '../../store';
|
|
19
20
|
|
|
20
21
|
// Keep zustand mock as it's needed globally
|
|
@@ -229,8 +230,7 @@ it('should handle topic creation flow', async () => {
|
|
|
229
230
|
const { result } = renderHook(() => useChatStore());
|
|
230
231
|
|
|
231
232
|
// Spy on action dependencies
|
|
232
|
-
const createTopicSpy = vi.spyOn(result.current, 'createTopic')
|
|
233
|
-
.mockResolvedValue('new-topic-id');
|
|
233
|
+
const createTopicSpy = vi.spyOn(result.current, 'createTopic').mockResolvedValue('new-topic-id');
|
|
234
234
|
const toggleLoadingSpy = vi.spyOn(result.current, 'internal_toggleMessageLoading');
|
|
235
235
|
|
|
236
236
|
// Execute
|
|
@@ -251,9 +251,7 @@ When testing streaming responses, simulate the flow properly:
|
|
|
251
251
|
```typescript
|
|
252
252
|
it('should handle streaming chunks', async () => {
|
|
253
253
|
const { result } = renderHook(() => useChatStore());
|
|
254
|
-
const messages = [
|
|
255
|
-
{ id: 'msg-1', role: 'user', content: 'Hello', sessionId: 'test-session' },
|
|
256
|
-
];
|
|
254
|
+
const messages = [{ id: 'msg-1', role: 'user', content: 'Hello', sessionId: 'test-session' }];
|
|
257
255
|
|
|
258
256
|
const streamSpy = vi
|
|
259
257
|
.spyOn(chatService, 'createAssistantMessageStream')
|
|
@@ -287,9 +285,7 @@ Always test error scenarios:
|
|
|
287
285
|
it('should handle errors gracefully', async () => {
|
|
288
286
|
const { result } = renderHook(() => useChatStore());
|
|
289
287
|
|
|
290
|
-
vi.spyOn(messageService, 'createMessage').mockRejectedValue(
|
|
291
|
-
new Error('create message error'),
|
|
292
|
-
);
|
|
288
|
+
vi.spyOn(messageService, 'createMessage').mockRejectedValue(new Error('create message error'));
|
|
293
289
|
|
|
294
290
|
await act(async () => {
|
|
295
291
|
try {
|
|
@@ -330,8 +326,7 @@ it('should test something', async () => {
|
|
|
330
326
|
it('should call internal methods', async () => {
|
|
331
327
|
const { result } = renderHook(() => useChatStore());
|
|
332
328
|
|
|
333
|
-
const internalMethodSpy = vi.spyOn(result.current, 'internal_method')
|
|
334
|
-
.mockResolvedValue();
|
|
329
|
+
const internalMethodSpy = vi.spyOn(result.current, 'internal_method').mockResolvedValue();
|
|
335
330
|
|
|
336
331
|
await act(async () => {
|
|
337
332
|
await result.current.publicMethod();
|
|
@@ -456,6 +451,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
456
451
|
|
|
457
452
|
import { discoverService } from '@/services/discover';
|
|
458
453
|
import { globalHelpers } from '@/store/global/helpers';
|
|
454
|
+
|
|
459
455
|
import { useDiscoverStore as useStore } from '../../store';
|
|
460
456
|
|
|
461
457
|
vi.mock('zustand/traditional');
|
|
@@ -486,6 +482,7 @@ describe('SWR Hook Actions', () => {
|
|
|
486
482
|
```
|
|
487
483
|
|
|
488
484
|
**Key points**:
|
|
485
|
+
|
|
489
486
|
- **DO NOT mock useSWR** - let it use the real implementation
|
|
490
487
|
- Only mock the **service methods** (fetchers)
|
|
491
488
|
- Use `waitFor` from `@testing-library/react` to wait for async operations
|
|
@@ -559,21 +556,19 @@ it('should not fetch when required parameter is missing', () => {
|
|
|
559
556
|
7. **Type assertions**: Use `as any` for test mock data where type definitions are strict
|
|
560
557
|
|
|
561
558
|
**Why this matters**:
|
|
559
|
+
|
|
562
560
|
- The fetcher (service method) is what we're testing - it must be called
|
|
563
561
|
- Hardcoding the return value bypasses the actual fetcher logic
|
|
564
562
|
- SWR returns Promises in real usage, tests should mirror this behavior
|
|
565
563
|
|
|
566
564
|
## Benefits of This Approach
|
|
567
565
|
|
|
568
|
-
✅ **Clear test layers** - Each test only spies on direct dependencies
|
|
569
|
-
✅ **Correct mocks** - Mocks match actual implementation
|
|
570
|
-
✅ **Better maintainability** - Changes to implementation require fewer test updates
|
|
571
|
-
✅ **Improved coverage** - Structured approach ensures all branches are tested
|
|
572
|
-
✅ **Reduced coupling** - Tests are independent and can run in any order
|
|
566
|
+
✅ **Clear test layers** - Each test only spies on direct dependencies ✅ **Correct mocks** - Mocks match actual implementation ✅ **Better maintainability** - Changes to implementation require fewer test updates ✅ **Improved coverage** - Structured approach ensures all branches are tested ✅ **Reduced coupling** - Tests are independent and can run in any order
|
|
573
567
|
|
|
574
568
|
## Reference
|
|
575
569
|
|
|
576
570
|
See example implementation in:
|
|
571
|
+
|
|
577
572
|
- `src/store/chat/slices/aiChat/actions/__tests__/generateAIChat.test.ts` (Regular actions)
|
|
578
573
|
- `src/store/discover/slices/plugin/action.test.ts` (SWR hooks)
|
|
579
574
|
- `src/store/discover/slices/mcp/action.test.ts` (SWR hooks)
|
|
@@ -16,10 +16,6 @@ alwaysApply: false
|
|
|
16
16
|
- prefer `@ts-expect-error` over `@ts-ignore` over `as any`
|
|
17
17
|
- Avoid meaningless null/undefined parameters; design strict function contracts.
|
|
18
18
|
|
|
19
|
-
## Imports and Modules
|
|
20
|
-
|
|
21
|
-
- When importing a directory module, prefer the explicit index path like `@/db/index` instead of `@/db`.
|
|
22
|
-
|
|
23
19
|
## Asynchronous Patterns and Concurrency
|
|
24
20
|
|
|
25
21
|
- Prefer `async`/`await` over callbacks or chained `.then` promises.
|