mirror of
https://github.com/lobehub/lobe-chat.git
synced 2026-06-14 03:30:19 +00:00
✨ feat(share): add topic sharing functionality (#11448)
This commit is contained in:
@@ -43,4 +43,4 @@ DROP TABLE "old_table";
|
||||
CREATE INDEX "users_email_idx" ON "users" ("email");
|
||||
```
|
||||
|
||||
**Important**: After modifying migration SQL (e.g., adding `IF NOT EXISTS` clauses), run `bun run db:generate-client` to update the hash in `packages/database/src/core/migrations.json`.
|
||||
**Important**: After modifying migration SQL (e.g., adding `IF NOT EXISTS` clauses), run `bun run db:generate:client` to update the hash in `packages/database/src/core/migrations.json`.
|
||||
|
||||
@@ -3,9 +3,10 @@ description: 包含添加 console.log 日志请求时
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Debug 包使用指南
|
||||
|
||||
本项目使用 [debug](mdc:https:/github.com/debug-js/debug) 包进行调试日志记录。使用此规则来确保团队成员统一调试日志格式。
|
||||
本项目使用 `debug` 包进行调试日志记录。使用此规则来确保团队成员统一调试日志格式。
|
||||
|
||||
## 基本用法
|
||||
|
||||
@@ -15,14 +16,14 @@ alwaysApply: false
|
||||
import debug from 'debug';
|
||||
```
|
||||
|
||||
2. 创建一个命名空间的日志记录器:
|
||||
1. 创建一个命名空间的日志记录器:
|
||||
|
||||
```typescript
|
||||
// 格式: lobe:[模块]:[子模块]
|
||||
const log = debug('lobe-[模块名]:[子模块名]');
|
||||
```
|
||||
|
||||
3. 使用日志记录器:
|
||||
1. 使用日志记录器:
|
||||
|
||||
```typescript
|
||||
log('简单消息');
|
||||
@@ -46,7 +47,7 @@ log('格式化数字: %d', number);
|
||||
|
||||
## 示例
|
||||
|
||||
查看 [market/index.ts](mdc:src/server/routers/edge/market/index.ts) 中的使用示例:
|
||||
查看 `src/server/routers/edge/market/index.ts` 中的使用示例:
|
||||
|
||||
```typescript
|
||||
import debug from 'debug';
|
||||
@@ -63,8 +64,9 @@ log('getAgent input: %O', input);
|
||||
### 在浏览器中
|
||||
|
||||
在控制台执行:
|
||||
|
||||
```javascript
|
||||
localStorage.debug = 'lobe-*'
|
||||
localStorage.debug = 'lobe-*';
|
||||
```
|
||||
|
||||
### 在 Node.js 环境中
|
||||
|
||||
@@ -3,13 +3,14 @@ description: 桌面端测试
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# 桌面端控制器单元测试指南
|
||||
|
||||
## 测试框架与目录结构
|
||||
|
||||
LobeChat 桌面端使用 Vitest 作为测试框架。控制器的单元测试应放置在对应控制器文件同级的 `__tests__` 目录下,并以原控制器文件名加 `.test.ts` 作为文件名。
|
||||
|
||||
```
|
||||
```plaintext
|
||||
apps/desktop/src/main/controllers/
|
||||
├── __tests__/
|
||||
│ ├── index.test.ts
|
||||
|
||||
@@ -3,7 +3,8 @@ description: 当要做 electron 相关工作时
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
**桌面端新功能实现指南**
|
||||
|
||||
# 桌面端新功能实现指南
|
||||
|
||||
## 桌面端应用架构概述
|
||||
|
||||
@@ -26,6 +27,7 @@ LobeChat 桌面端基于 Electron 框架构建,采用主进程-渲染进程架
|
||||
### 1. 确定功能需求与设计
|
||||
|
||||
首先确定新功能的需求和设计,包括:
|
||||
|
||||
- 功能描述和用例
|
||||
- 是否需要系统级API(如文件系统、网络等)
|
||||
- UI/UX设计(如必要)
|
||||
@@ -64,9 +66,10 @@ LobeChat 桌面端基于 Electron 框架构建,采用主进程-渲染进程架
|
||||
|
||||
```typescript
|
||||
// src/services/electron/newFeatureService.ts
|
||||
import { ensureElectronIpc } from '@/utils/electron/ipc';
|
||||
import type { NewFeatureParams } from '@lobechat/electron-client-ipc';
|
||||
|
||||
import { ensureElectronIpc } from '@/utils/electron/ipc';
|
||||
|
||||
const ipc = ensureElectronIpc();
|
||||
|
||||
export const newFeatureService = async (params: NewFeatureParams) => {
|
||||
@@ -84,7 +87,7 @@ LobeChat 桌面端基于 Electron 框架构建,采用主进程-渲染进程架
|
||||
|
||||
### 5. 如果是新增内置工具,遵循工具实现流程
|
||||
|
||||
参考 [desktop-local-tools-implement.mdc](mdc:desktop-local-tools-implement.mdc) 了解更多关于添加内置工具的详细步骤。
|
||||
参考 `desktop-local-tools-implement.mdc` 了解更多关于添加内置工具的详细步骤。
|
||||
|
||||
### 6. 添加测试
|
||||
|
||||
@@ -120,12 +123,13 @@ LobeChat 桌面端基于 Electron 框架构建,采用主进程-渲染进程架
|
||||
|
||||
```typescript
|
||||
// apps/desktop/src/main/controllers/NotificationCtr.ts
|
||||
import { Notification } from 'electron';
|
||||
import { ControllerModule, IpcMethod } from '@/controllers';
|
||||
import type {
|
||||
DesktopNotificationResult,
|
||||
ShowDesktopNotificationParams,
|
||||
} from '@lobechat/electron-client-ipc';
|
||||
import { Notification } from 'electron';
|
||||
|
||||
import { ControllerModule, IpcMethod } from '@/controllers';
|
||||
|
||||
export default class NotificationCtr extends ControllerModule {
|
||||
static override readonly groupName = 'notification';
|
||||
|
||||
@@ -3,78 +3,79 @@ description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
**新增桌面端工具流程:**
|
||||
|
||||
1. **定义工具接口 (Manifest):**
|
||||
* **文件:** `src/tools/[tool_category]/index.ts` (例如: `src/tools/local-files/index.ts`)
|
||||
* **操作:**
|
||||
* 在 `ApiName` 对象(例如 `LocalFilesApiName`)中添加一个新的、唯一的 API 名称。
|
||||
* 在 `Manifest` 对象(例如 `LocalFilesManifest`)的 `api` 数组中,新增一个对象来定义新工具的接口。
|
||||
* **关键字段:**
|
||||
* `name`: 使用上一步定义的 API 名称。
|
||||
* `description`: 清晰描述工具的功能,供 Agent 理解和向用户展示。
|
||||
* `parameters`: 使用 JSON Schema 定义工具所需的输入参数。
|
||||
* `type`: 通常是 'object'。
|
||||
* `properties`: 定义每个参数的名称、`description`、`type` (string, number, boolean, array, etc.),使用英文。
|
||||
* `required`: 一个字符串数组,列出必须提供的参数名称。
|
||||
1. **定义工具接口 (Manifest):**
|
||||
- **文件:** `src/tools/[tool_category]/index.ts` (例如: `src/tools/local-files/index.ts`)
|
||||
- **操作:**
|
||||
- 在 `ApiName` 对象(例如 `LocalFilesApiName`)中添加一个新的、唯一的 API 名称。
|
||||
- 在 `Manifest` 对象(例如 `LocalFilesManifest`)的 `api` 数组中,新增一个对象来定义新工具的接口。
|
||||
- **关键字段:**
|
||||
- `name`: 使用上一步定义的 API 名称。
|
||||
- `description`: 清晰描述工具的功能,供 Agent 理解和向用户展示。
|
||||
- `parameters`: 使用 JSON Schema 定义工具所需的输入参数。
|
||||
- `type`: 通常是 'object'。
|
||||
- `properties`: 定义每个参数的名称、`description`、`type` (string, number, boolean, array, etc.),使用英文。
|
||||
- `required`: 一个字符串数组,列出必须提供的参数名称。
|
||||
|
||||
2. **定义相关类型:**
|
||||
* **文件 1:** `packages/electron-client-ipc/src/types.ts` (或类似的共享 IPC 类型文件)
|
||||
* **操作:** 定义传递给 IPC 事件的参数类型接口 (例如: `RenameLocalFileParams`, `MoveLocalFileParams`)。确保与 Manifest 中定义的 `parameters` 一致。
|
||||
* **文件 2:** `src/tools/[tool_category]/type.ts` (例如: `src/tools/local-files/type.ts`)
|
||||
* **操作:** 定义此工具执行后,存储在前端 Zustand Store 中的状态类型接口 (例如: `LocalRenameFileState`, `LocalMoveFileState`)。这通常包含操作结果(成功/失败)、错误信息以及相关数据(如旧路径、新路径等)。
|
||||
2. **定义相关类型:**
|
||||
- **文件 1:** `packages/electron-client-ipc/src/types.ts` (或类似的共享 IPC 类型文件)
|
||||
- **操作:** 定义传递给 IPC 事件的参数类型接口 (例如: `RenameLocalFileParams`, `MoveLocalFileParams`)。确保与 Manifest 中定义的 `parameters` 一致。
|
||||
- **文件 2:** `src/tools/[tool_category]/type.ts` (例如: `src/tools/local-files/type.ts`)
|
||||
- **操作:** 定义此工具执行后,存储在前端 Zustand Store 中的状态类型接口 (例如: `LocalRenameFileState`, `LocalMoveFileState`)。这通常包含操作结果(成功/失败)、错误信息以及相关数据(如旧路径、新路径等)。
|
||||
|
||||
3. **实现前端状态管理 (Store Action):**
|
||||
* **文件:** `src/store/chat/slices/builtinTool/actions/[tool_category].ts` (例如: `src/store/chat/slices/builtinTool/actions/localFile.ts`)
|
||||
* **操作:**
|
||||
* 导入在步骤 2 中定义的 IPC 参数类型和状态类型。
|
||||
* 在 Action 接口 (例如: `LocalFileAction`) 中添加新 Action 的方法签名,使用对应的 IPC 参数类型。
|
||||
* 在 `createSlice` (例如: `localFileSlice`) 中实现该 Action 方法:
|
||||
* 接收 `id` (消息 ID) 和 `params` (符合 IPC 参数类型)。
|
||||
* 设置加载状态 (`toggleLocalFileLoading(id, true)`)。
|
||||
* 调用对应的 `Service` 层方法 (见步骤 4),传递 `params`。
|
||||
* 使用 `try...catch` 处理 `Service` 调用可能发生的错误。
|
||||
* **成功时:**
|
||||
* 调用 `updatePluginState(id, {...})` 更新插件状态,使用步骤 2 中定义的状态类型。
|
||||
* 调用 `internal_updateMessageContent(id, JSON.stringify({...}))` 更新消息内容,通常包含成功确认信息。
|
||||
* **失败时:**
|
||||
* 记录错误 (`console.error`)。
|
||||
* 调用 `updatePluginState(id, {...})` 更新插件状态,包含错误信息。
|
||||
* 调用 `internal_updateMessagePluginError(id, {...})` 设置消息的错误状态。
|
||||
* 调用 `internal_updateMessageContent(id, JSON.stringify({...}))` 更新消息内容,包含错误信息。
|
||||
* 在 `finally` 块中取消加载状态 (`toggleLocalFileLoading(id, false)`)。
|
||||
* 返回操作是否成功 (`boolean`)。
|
||||
3. **实现前端状态管理 (Store Action):**
|
||||
- **文件:** `src/store/chat/slices/builtinTool/actions/[tool_category].ts` (例如: `src/store/chat/slices/builtinTool/actions/localFile.ts`)
|
||||
- **操作:**
|
||||
- 导入在步骤 2 中定义的 IPC 参数类型和状态类型。
|
||||
- 在 Action 接口 (例如: `LocalFileAction`) 中添加新 Action 的方法签名,使用对应的 IPC 参数类型。
|
||||
- 在 `createSlice` (例如: `localFileSlice`) 中实现该 Action 方法:
|
||||
- 接收 `id` (消息 ID) 和 `params` (符合 IPC 参数类型)。
|
||||
- 设置加载状态 (`toggleLocalFileLoading(id, true)`)。
|
||||
- 调用对应的 `Service` 层方法 (见步骤 4),传递 `params`。
|
||||
- 使用 `try...catch` 处理 `Service` 调用可能发生的错误。
|
||||
- **成功时:**
|
||||
- 调用 `updatePluginState(id, {...})` 更新插件状态,使用步骤 2 中定义的状态类型。
|
||||
- 调用 `internal_updateMessageContent(id, JSON.stringify({...}))` 更新消息内容,通常包含成功确认信息。
|
||||
- **失败时:**
|
||||
- 记录错误 (`console.error`)。
|
||||
- 调用 `updatePluginState(id, {...})` 更新插件状态,包含错误信息。
|
||||
- 调用 `internal_updateMessagePluginError(id, {...})` 设置消息的错误状态。
|
||||
- 调用 `internal_updateMessageContent(id, JSON.stringify({...}))` 更新消息内容,包含错误信息。
|
||||
- 在 `finally` 块中取消加载状态 (`toggleLocalFileLoading(id, false)`)。
|
||||
- 返回操作是否成功 (`boolean`)。
|
||||
|
||||
4. **实现 Service 层 (调用 IPC):**
|
||||
* **文件:** `src/services/electron/[tool_category]Service.ts` (例如: `src/services/electron/localFileService.ts`)
|
||||
* **操作:**
|
||||
* 导入在步骤 2 中定义的 IPC 参数类型。
|
||||
* 添加一个新的 `async` 方法,方法名通常与 Action 名称对应 (例如: `renameLocalFile`)。
|
||||
* 方法接收 `params` (符合 IPC 参数类型)。
|
||||
* 通过 `ensureElectronIpc()` 获取 IPC 代理 (`const ipc = ensureElectronIpc();`),调用与 Manifest 中 `name` 字段匹配的链式方法,并将 `params` 传递过去。
|
||||
* 定义方法的返回类型,通常是 `Promise<{ success: boolean; error?: string }>`,与后端 Controller 返回的结构一致。
|
||||
4. **实现 Service 层 (调用 IPC):**
|
||||
- **文件:** `src/services/electron/[tool_category]Service.ts` (例如: `src/services/electron/localFileService.ts`)
|
||||
- **操作:**
|
||||
- 导入在步骤 2 中定义的 IPC 参数类型。
|
||||
- 添加一个新的 `async` 方法,方法名通常与 Action 名称对应 (例如: `renameLocalFile`)。
|
||||
- 方法接收 `params` (符合 IPC 参数类型)。
|
||||
- 通过 `ensureElectronIpc()` 获取 IPC 代理 (`const ipc = ensureElectronIpc();`),调用与 Manifest 中 `name` 字段匹配的链式方法,并将 `params` 传递过去。
|
||||
- 定义方法的返回类型,通常是 `Promise<{ success: boolean; error?: string }>`,与后端 Controller 返回的结构一致。
|
||||
|
||||
5. **实现后端逻辑 (Controller / IPC Handler):**
|
||||
* **文件:** `apps/desktop/src/main/controllers/[ToolName]Ctr.ts` (例如: `apps/desktop/src/main/controllers/LocalFileCtr.ts`)
|
||||
* **操作:**
|
||||
* 导入 Node.js 相关模块 (`fs`, `path` 等) 和 IPC 相关依赖 (`ControllerModule`, `IpcMethod`、参数类型等)。
|
||||
* 添加一个新的 `async` 方法,方法名通常以 `handle` 开头 (例如: `handleRenameFile`)。
|
||||
* 使用 `@IpcMethod()` 装饰器将此方法注册为对应 IPC 事件的处理器,确保方法名与 Manifest 中的 `name` 以及 Service 层的链式调用一致。
|
||||
* 方法的参数应解构自 Service 层传递过来的对象,类型与步骤 2 中定义的 IPC 参数类型匹配。
|
||||
* 实现核心业务逻辑:
|
||||
* 进行必要的输入验证。
|
||||
* 执行文件系统操作或其他后端任务 (例如: `fs.promises.rename`)。
|
||||
* 使用 `try...catch` 捕获执行过程中的错误。
|
||||
* 处理特定错误码 (`error.code`) 以提供更友好的错误消息。
|
||||
* 返回一个包含 `success` (boolean) 和可选 `error` (string) 字段的对象。
|
||||
5. **实现后端逻辑 (Controller / IPC Handler):**
|
||||
- **文件:** `apps/desktop/src/main/controllers/[ToolName]Ctr.ts` (例如: `apps/desktop/src/main/controllers/LocalFileCtr.ts`)
|
||||
- **操作:**
|
||||
- 导入 Node.js 相关模块 (`fs`, `path` 等) 和 IPC 相关依赖 (`ControllerModule`, `IpcMethod`、参数类型等)。
|
||||
- 添加一个新的 `async` 方法,方法名通常以 `handle` 开头 (例如: `handleRenameFile`)。
|
||||
- 使用 `@IpcMethod()` 装饰器将此方法注册为对应 IPC 事件的处理器,确保方法名与 Manifest 中的 `name` 以及 Service 层的链式调用一致。
|
||||
- 方法的参数应解构自 Service 层传递过来的对象,类型与步骤 2 中定义的 IPC 参数类型匹配。
|
||||
- 实现核心业务逻辑:
|
||||
- 进行必要的输入验证。
|
||||
- 执行文件系统操作或其他后端任务 (例如: `fs.promises.rename`)。
|
||||
- 使用 `try...catch` 捕获执行过程中的错误。
|
||||
- 处理特定错误码 (`error.code`) 以提供更友好的错误消息。
|
||||
- 返回一个包含 `success` (boolean) 和可选 `error` (string) 字段的对象。
|
||||
|
||||
6. **更新 Agent 文档 (System Role):**
|
||||
* **文件:** `src/tools/[tool_category]/systemRole.ts` (例如: `src/tools/local-files/systemRole.ts`)
|
||||
* **操作:**
|
||||
* 在 `<core_capabilities>` 部分添加新工具的简要描述。
|
||||
* 如果需要,更新 `<workflow>`。
|
||||
* 在 `<tool_usage_guidelines>` 部分为新工具添加详细的使用说明,解释其参数、用途和预期行为。
|
||||
* 如有必要,更新 `<security_considerations>`。
|
||||
* 如有必要(例如工具返回了新的数据结构或路径),更新 `<response_format>` 中的示例。
|
||||
6. **更新 Agent 文档 (System Role):**
|
||||
- **文件:** `src/tools/[tool_category]/systemRole.ts` (例如: `src/tools/local-files/systemRole.ts`)
|
||||
- **操作:**
|
||||
- 在 `<core_capabilities>` 部分添加新工具的简要描述。
|
||||
- 如果需要,更新 `<workflow>`。
|
||||
- 在 `<tool_usage_guidelines>` 部分为新工具添加详细的使用说明,解释其参数、用途和预期行为。
|
||||
- 如有必要,更新 `<security_considerations>`。
|
||||
- 如有必要(例如工具返回了新的数据结构或路径),更新 `<response_format>` 中的示例。
|
||||
|
||||
通过遵循这些步骤,可以系统地将新的桌面端工具集成到 LobeChat 的插件系统中。
|
||||
|
||||
@@ -3,7 +3,8 @@ description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
**桌面端菜单配置指南**
|
||||
|
||||
# 桌面端菜单配置指南
|
||||
|
||||
## 菜单系统概述
|
||||
|
||||
@@ -15,7 +16,7 @@ LobeChat 桌面应用有三种主要的菜单类型:
|
||||
|
||||
## 菜单相关文件结构
|
||||
|
||||
```
|
||||
```plaintext
|
||||
apps/desktop/src/main/
|
||||
├── menus/ # 菜单定义
|
||||
│ ├── appMenu.ts # 应用菜单配置
|
||||
@@ -33,8 +34,9 @@ apps/desktop/src/main/
|
||||
应用菜单在 `apps/desktop/src/main/menus/appMenu.ts` 中定义:
|
||||
|
||||
1. **导入依赖**
|
||||
|
||||
```typescript
|
||||
import { app, BrowserWindow, Menu, MenuItem, MenuItemConstructorOptions } from 'electron';
|
||||
import { BrowserWindow, Menu, MenuItem, MenuItemConstructorOptions, app } from 'electron';
|
||||
import { is } from 'electron-util';
|
||||
```
|
||||
|
||||
@@ -43,6 +45,7 @@ apps/desktop/src/main/
|
||||
- 每个菜单项可以包含:label, accelerator (快捷键), role, submenu, click 等属性
|
||||
|
||||
3. **创建菜单工厂函数**
|
||||
|
||||
```typescript
|
||||
export const createAppMenu = (win: BrowserWindow) => {
|
||||
const template = [
|
||||
@@ -61,6 +64,7 @@ apps/desktop/src/main/
|
||||
上下文菜单通常在特定元素上右键点击时显示:
|
||||
|
||||
1. **在主进程中定义菜单模板**
|
||||
|
||||
```typescript
|
||||
// apps/desktop/src/main/menus/contextMenu.ts
|
||||
export const createContextMenu = () => {
|
||||
@@ -73,6 +77,7 @@ apps/desktop/src/main/
|
||||
```
|
||||
|
||||
2. **在适当的事件处理器中显示菜单**
|
||||
|
||||
```typescript
|
||||
const menu = createContextMenu();
|
||||
menu.popup();
|
||||
@@ -83,11 +88,13 @@ apps/desktop/src/main/
|
||||
托盘菜单在 `TrayMenuCtr.ts` 中配置:
|
||||
|
||||
1. **创建托盘图标**
|
||||
|
||||
```typescript
|
||||
this.tray = new Tray(trayIconPath);
|
||||
```
|
||||
|
||||
2. **定义托盘菜单**
|
||||
|
||||
```typescript
|
||||
const contextMenu = Menu.buildFromTemplate([
|
||||
{ label: '显示主窗口', click: this.showMainWindow },
|
||||
@@ -97,6 +104,7 @@ apps/desktop/src/main/
|
||||
```
|
||||
|
||||
3. **设置托盘菜单**
|
||||
|
||||
```typescript
|
||||
this.tray.setContextMenu(contextMenu);
|
||||
```
|
||||
@@ -106,11 +114,13 @@ apps/desktop/src/main/
|
||||
为菜单添加多语言支持:
|
||||
|
||||
1. **导入本地化工具**
|
||||
|
||||
```typescript
|
||||
import { i18n } from '../locales';
|
||||
```
|
||||
|
||||
2. **使用翻译函数**
|
||||
|
||||
```typescript
|
||||
const template = [
|
||||
{
|
||||
@@ -118,14 +128,13 @@ apps/desktop/src/main/
|
||||
submenu: [
|
||||
{ label: i18n.t('menu.new'), click: createNew },
|
||||
// ...
|
||||
]
|
||||
],
|
||||
},
|
||||
// ...
|
||||
];
|
||||
```
|
||||
|
||||
3. **在语言切换时更新菜单**
|
||||
在 `MenuCtr.ts` 中监听语言变化事件并重新创建菜单
|
||||
3. **在语言切换时更新菜单** 在 `MenuCtr.ts` 中监听语言变化事件并重新创建菜单
|
||||
|
||||
## 添加新菜单项流程
|
||||
|
||||
@@ -134,6 +143,7 @@ apps/desktop/src/main/
|
||||
- 确定在菜单中的位置(主菜单项或子菜单项)
|
||||
|
||||
2. **定义菜单项**
|
||||
|
||||
```typescript
|
||||
const newMenuItem: MenuItemConstructorOptions = {
|
||||
label: '新功能',
|
||||
@@ -141,12 +151,11 @@ apps/desktop/src/main/
|
||||
click: (_, window) => {
|
||||
// 处理点击事件
|
||||
if (window) window.webContents.send('trigger-new-feature');
|
||||
}
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
3. **添加到菜单模板**
|
||||
将新菜单项添加到相应的菜单模板中
|
||||
3. **添加到菜单模板** 将新菜单项添加到相应的菜单模板中
|
||||
|
||||
4. **对于与渲染进程交互的功能**
|
||||
- 使用 `window.webContents.send()` 发送 IPC 消息到渲染进程
|
||||
@@ -157,6 +166,7 @@ apps/desktop/src/main/
|
||||
动态控制菜单项状态:
|
||||
|
||||
1. **保存对菜单项的引用**
|
||||
|
||||
```typescript
|
||||
this.menuItems = {};
|
||||
const menu = Menu.buildFromTemplate(template);
|
||||
@@ -164,6 +174,7 @@ apps/desktop/src/main/
|
||||
```
|
||||
|
||||
2. **根据条件更新状态**
|
||||
|
||||
```typescript
|
||||
updateMenuState(state) {
|
||||
if (this.menuItems.newFeature) {
|
||||
@@ -179,6 +190,7 @@ apps/desktop/src/main/
|
||||
|
||||
2. **平台特定菜单**
|
||||
- 使用 `process.platform` 检查为不同平台提供不同菜单
|
||||
|
||||
```typescript
|
||||
if (process.platform === 'darwin') {
|
||||
template.unshift({ role: 'appMenu' });
|
||||
|
||||
@@ -3,7 +3,8 @@ description:
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
**桌面端窗口管理指南**
|
||||
|
||||
# 桌面端窗口管理指南
|
||||
|
||||
## 窗口管理概述
|
||||
|
||||
@@ -16,7 +17,7 @@ LobeChat 桌面应用使用 Electron 的 `BrowserWindow` 管理应用窗口。
|
||||
|
||||
## 相关文件结构
|
||||
|
||||
```
|
||||
```plaintext
|
||||
apps/desktop/src/main/
|
||||
├── appBrowsers.ts # 窗口管理的核心文件
|
||||
├── controllers/
|
||||
@@ -63,6 +64,7 @@ export const createMainWindow = () => {
|
||||
实现窗口状态持久化保存和恢复:
|
||||
|
||||
1. **保存窗口状态**
|
||||
|
||||
```typescript
|
||||
const saveWindowState = (window: BrowserWindow) => {
|
||||
if (!window.isMinimized() && !window.isMaximized()) {
|
||||
@@ -80,6 +82,7 @@ export const createMainWindow = () => {
|
||||
```
|
||||
|
||||
2. **恢复窗口状态**
|
||||
|
||||
```typescript
|
||||
const restoreWindowState = (window: BrowserWindow) => {
|
||||
const savedState = settings.get('windowState');
|
||||
@@ -96,6 +99,7 @@ export const createMainWindow = () => {
|
||||
```
|
||||
|
||||
3. **监听窗口事件**
|
||||
|
||||
```typescript
|
||||
window.on('close', () => saveWindowState(window));
|
||||
window.on('moved', () => saveWindowState(window));
|
||||
@@ -107,6 +111,7 @@ export const createMainWindow = () => {
|
||||
对于需要多窗口支持的功能:
|
||||
|
||||
1. **跟踪窗口**
|
||||
|
||||
```typescript
|
||||
export class WindowManager {
|
||||
private windows: Map<string, BrowserWindow> = new Map();
|
||||
@@ -133,6 +138,7 @@ export const createMainWindow = () => {
|
||||
```
|
||||
|
||||
2. **窗口间通信**
|
||||
|
||||
```typescript
|
||||
// 从一个窗口向另一个窗口发送消息
|
||||
sendMessageToWindow(targetWindowId, channel, data) {
|
||||
@@ -148,9 +154,11 @@ export const createMainWindow = () => {
|
||||
通过 IPC 实现窗口操作:
|
||||
|
||||
1. **在主进程中注册 IPC 处理器**
|
||||
|
||||
```typescript
|
||||
// apps/desktop/src/main/controllers/BrowserWindowsCtr.ts
|
||||
import { BrowserWindow } from 'electron';
|
||||
|
||||
import { ControllerModule, IpcMethod } from '@/controllers';
|
||||
|
||||
export default class BrowserWindowsCtr extends ControllerModule {
|
||||
@@ -178,10 +186,12 @@ export const createMainWindow = () => {
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `@IpcMethod()` 根据控制器的 `groupName` 自动将方法映射为 `windows.minimizeWindow` 形式的通道名称。
|
||||
- 控制器需继承 `ControllerModule`,并在 `controllers/registry.ts` 中通过 `controllerIpcConstructors` 注册,便于类型生成。
|
||||
|
||||
2. **在渲染进程中调用**
|
||||
|
||||
```typescript
|
||||
// src/services/electron/windowService.ts
|
||||
import { ensureElectronIpc } from '@/utils/electron/ipc';
|
||||
@@ -194,6 +204,7 @@ export const createMainWindow = () => {
|
||||
close: () => ipc.windows.closeWindow(),
|
||||
};
|
||||
```
|
||||
|
||||
- `ensureElectronIpc()` 会基于 `DesktopIpcServices` 运行时生成 Proxy,并通过 `window.electronAPI.invoke` 与主进程通信;不再直接使用 `dispatch`。
|
||||
|
||||
### 5. 自定义窗口控制 (无边框窗口)
|
||||
@@ -201,6 +212,7 @@ export const createMainWindow = () => {
|
||||
对于自定义窗口标题栏:
|
||||
|
||||
1. **创建无边框窗口**
|
||||
|
||||
```typescript
|
||||
const window = new BrowserWindow({
|
||||
frame: false,
|
||||
@@ -210,6 +222,7 @@ export const createMainWindow = () => {
|
||||
```
|
||||
|
||||
2. **在渲染进程中实现拖拽区域**
|
||||
|
||||
```css
|
||||
/* CSS */
|
||||
.titlebar {
|
||||
@@ -229,6 +242,7 @@ export const createMainWindow = () => {
|
||||
|
||||
2. **安全性**
|
||||
- 始终设置适当的 `webPreferences` 确保安全
|
||||
|
||||
```typescript
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, '../preload/index.js'),
|
||||
@@ -255,6 +269,7 @@ export const createMainWindow = () => {
|
||||
```typescript
|
||||
// apps/desktop/src/main/controllers/BrowserWindowsCtr.ts
|
||||
import type { OpenSettingsWindowOptions } from '@lobechat/electron-client-ipc';
|
||||
|
||||
import { ControllerModule, IpcMethod } from '@/controllers';
|
||||
|
||||
export default class BrowserWindowsCtr extends ControllerModule {
|
||||
|
||||
@@ -10,14 +10,14 @@ This document outlines the conventions and best practices for defining PostgreSQ
|
||||
|
||||
## Configuration
|
||||
|
||||
- Drizzle configuration is managed in [drizzle.config.ts](mdc:drizzle.config.ts)
|
||||
- Drizzle configuration is managed in `drizzle.config.ts`
|
||||
- Schema files are located in the src/database/schemas/ directory
|
||||
- Migration files are output to `src/database/migrations/`
|
||||
- The project uses `postgresql` dialect with `strict: true`
|
||||
|
||||
## Helper Functions
|
||||
|
||||
Commonly used column definitions, especially for timestamps, are centralized in [src/database/schemas/\_helpers.ts](mdc:src/database/schemas/_helpers.ts):
|
||||
Commonly used column definitions, especially for timestamps, are centralized in `src/database/schemas/_helpers.ts`:
|
||||
|
||||
- `timestamptz(name: string)`: Creates a timestamp column with timezone
|
||||
- `createdAt()`, `updatedAt()`, `accessedAt()`: Helper functions for standard timestamp columns
|
||||
@@ -46,7 +46,7 @@ Commonly used column definitions, especially for timestamps, are centralized in
|
||||
|
||||
### Timestamps
|
||||
|
||||
- Consistently use the `...timestamps` spread from [\_helpers.ts](mdc:src/database/schemas/_helpers.ts) for `created_at`, `updated_at`, and `accessed_at` columns
|
||||
- Consistently use the `...timestamps` spread from `_helpers.ts` for `created_at`, `updated_at`, and `accessed_at` columns
|
||||
|
||||
### Default Values
|
||||
|
||||
@@ -74,12 +74,12 @@ Commonly used column definitions, especially for timestamps, are centralized in
|
||||
|
||||
## Relations
|
||||
|
||||
- Table relationships are defined centrally in [src/database/schemas/relations.ts](mdc:src/database/schemas/relations.ts) using the `relations()` utility from `drizzle-orm`
|
||||
- Table relationships are defined centrally in `src/database/schemas/relations.ts` using the `relations()` utility from `drizzle-orm`
|
||||
|
||||
## Code Style & Structure
|
||||
|
||||
- **File Organization**: Each main database entity typically has its own schema file (e.g., [user.ts](mdc:src/database/schemas/user.ts), [agent.ts](mdc:src/database/schemas/agent.ts))
|
||||
- All schemas are re-exported from [src/database/schemas/index.ts](mdc:src/database/schemas/index.ts)
|
||||
- **File Organization**: Each main database entity typically has its own schema file (e.g., `user.ts`, `agent.ts`)
|
||||
- All schemas are re-exported from `src/database/schemas/index.ts`
|
||||
- **ESLint**: Files often start with `/* eslint-disable sort-keys-fix/sort-keys-fix */`
|
||||
- **Comments**: Use JSDoc-style comments to explain the purpose of tables and complex columns, fields that are self-explanatory do not require jsdoc explanations, such as id, user_id, etc.
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# 如何添加新的快捷键:开发者指南
|
||||
|
||||
本指南将带您一步步地向 LobeChat 添加一个新的快捷键功能。我们将通过一个完整示例,演示从定义到实现的整个过程。
|
||||
|
||||
@@ -37,6 +37,7 @@ export default {
|
||||
- `sync.status.ready` - Feature + group + status
|
||||
|
||||
**Parameters:** Use `{{variableName}}` syntax
|
||||
|
||||
```typescript
|
||||
'alert.cloud.desc': '我们提供 {{credit}} 额度积分',
|
||||
```
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
---
|
||||
description: Project directory structure overview
|
||||
alwaysApply: false
|
||||
alwaysApply: true
|
||||
---
|
||||
|
||||
# LobeChat Project Structure
|
||||
@@ -27,6 +26,11 @@ lobe-chat/
|
||||
│ ├── agent-runtime/
|
||||
│ ├── builtin-agents/
|
||||
│ ├── builtin-tool-*/ # builtin tool packages
|
||||
│ ├── business/ # cloud-only business logic packages
|
||||
│ │ ├── config/
|
||||
│ │ ├── const/
|
||||
│ │ └── model-runtime/
|
||||
│ ├── config/
|
||||
│ ├── const/
|
||||
│ ├── context-engine/
|
||||
│ ├── conversation-flow/
|
||||
@@ -36,6 +40,8 @@ lobe-chat/
|
||||
│ │ ├── schemas/
|
||||
│ │ └── repositories/
|
||||
│ ├── desktop-bridge/
|
||||
│ ├── edge-config/
|
||||
│ ├── editor-runtime/
|
||||
│ ├── electron-client-ipc/
|
||||
│ ├── electron-server-ipc/
|
||||
│ ├── fetch-sse/
|
||||
@@ -46,7 +52,7 @@ lobe-chat/
|
||||
│ │ └── src/
|
||||
│ │ ├── core/
|
||||
│ │ └── providers/
|
||||
│ ├── obervability-otel/
|
||||
│ ├── observability-otel/
|
||||
│ ├── prompts/
|
||||
│ ├── python-interpreter/
|
||||
│ ├── ssrf-safe-fetch/
|
||||
@@ -72,6 +78,10 @@ lobe-chat/
|
||||
│ │ │ ├── onboarding/
|
||||
│ │ │ └── router/
|
||||
│ │ └── desktop/
|
||||
│ ├── business/ # cloud-only business logic (client/server)
|
||||
│ │ ├── client/
|
||||
│ │ ├── locales/
|
||||
│ │ └── server/
|
||||
│ ├── components/
|
||||
│ ├── config/
|
||||
│ ├── const/
|
||||
@@ -130,6 +140,9 @@ lobe-chat/
|
||||
- Repository (bff-queries): `packages/database/src/repositories`
|
||||
- Third-party Integrations: `src/libs` — analytics, oidc etc.
|
||||
- Builtin Tools: `src/tools`, `packages/builtin-tool-*`
|
||||
- Business (cloud-only): Code specific to LobeHub cloud service, only expose empty interfaces for opens-source version.
|
||||
- `src/business/*`
|
||||
- `packages/business/*`
|
||||
|
||||
## Data Flow Architecture
|
||||
|
||||
|
||||
+17
-5
@@ -107,7 +107,7 @@ This project uses a **hybrid routing architecture**: Next.js App Router for stat
|
||||
| Router | oauth, reset-password, etc.) | src/app/[variants]/(auth)/ |
|
||||
+------------------+--------------------------------+--------------------------------+
|
||||
| React Router | Main SPA features | BrowserRouter + Routes |
|
||||
| DOM | (chat, discover, settings) | desktopRouter.config.tsx |
|
||||
| DOM | (chat, community, settings) | desktopRouter.config.tsx |
|
||||
| | | mobileRouter.config.tsx |
|
||||
+------------------+--------------------------------+--------------------------------+
|
||||
```
|
||||
@@ -122,16 +122,16 @@ This project uses a **hybrid routing architecture**: Next.js App Router for stat
|
||||
### Router Utilities
|
||||
|
||||
```tsx
|
||||
import { dynamicElement, redirectElement, ErrorBoundary, RouteConfig } from '@/utils/router';
|
||||
import { ErrorBoundary, RouteConfig, dynamicElement, redirectElement } from '@/utils/router';
|
||||
|
||||
// Lazy load a page component
|
||||
element: dynamicElement(() => import('./chat'), 'Desktop > Chat')
|
||||
element: dynamicElement(() => import('./chat'), 'Desktop > Chat');
|
||||
|
||||
// Create a redirect
|
||||
element: redirectElement('/settings/profile')
|
||||
element: redirectElement('/settings/profile');
|
||||
|
||||
// Error boundary for route
|
||||
errorElement: <ErrorBoundary resetPath="/chat" />
|
||||
errorElement: <ErrorBoundary resetPath="/chat" />;
|
||||
```
|
||||
|
||||
### Adding New Routes
|
||||
@@ -142,6 +142,18 @@ errorElement: <ErrorBoundary resetPath="/chat" />
|
||||
|
||||
### Navigation
|
||||
|
||||
**Important**: For SPA pages (React Router DOM routes), use `Link` from `react-router-dom`, NOT from `next/link`.
|
||||
|
||||
```tsx
|
||||
// ❌ Wrong - next/link in SPA pages
|
||||
import Link from 'next/link';
|
||||
<Link href="/">Home</Link>
|
||||
|
||||
// ✅ Correct - react-router-dom Link in SPA pages
|
||||
import { Link } from 'react-router-dom';
|
||||
<Link to="/">Home</Link>
|
||||
```
|
||||
|
||||
```tsx
|
||||
// In components - use react-router-dom hooks
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
|
||||
@@ -42,7 +42,7 @@ const Component = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
{recentTopics.map(topic => (
|
||||
{recentTopics.map((topic) => (
|
||||
<div key={topic.id}>{topic.title}</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -81,6 +81,7 @@ const isInit = useSessionStore(recentSelectors.isRecentTopicsInit);
|
||||
```
|
||||
|
||||
**RecentTopic 类型:**
|
||||
|
||||
```typescript
|
||||
interface RecentTopic {
|
||||
agent: {
|
||||
|
||||
@@ -3,173 +3,199 @@ globs: *.test.ts,*.test.tsx
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# 测试指南 - LobeChat Testing Guide
|
||||
# LobeChat Testing Guide
|
||||
|
||||
## 测试环境概览
|
||||
## Test Overview
|
||||
|
||||
LobeChat 项目使用 Vitest 测试库,配置了两种不同的测试环境:
|
||||
LobeChat testing consists of **E2E tests** and **Unit tests**. This guide focuses on **Unit tests**.
|
||||
|
||||
### 客户端数据库测试环境 (DOM Environment)
|
||||
Unit tests are organized into three main categories:
|
||||
|
||||
- **配置文件**: [vitest.config.ts](mdc:vitest.config.ts)
|
||||
- **环境**: Happy DOM (浏览器环境模拟)
|
||||
- **数据库**: PGLite (浏览器环境的 PostgreSQL)
|
||||
- **用途**: 测试前端组件、客户端逻辑、React 组件等
|
||||
- **设置文件**: [tests/setup.ts](mdc:tests/setup.ts)
|
||||
```plaintext
|
||||
+---------------------+---------------------------+-----------------------------+
|
||||
| Category | Location | Config File |
|
||||
+---------------------+---------------------------+-----------------------------+
|
||||
| Next.js Webapp | src/**/*.test.ts(x) | vitest.config.ts |
|
||||
| Packages | packages/*/**/*.test.ts | packages/*/vitest.config.ts |
|
||||
| Desktop App | apps/desktop/**/*.test.ts | apps/desktop/vitest.config.ts |
|
||||
+---------------------+---------------------------+-----------------------------+
|
||||
```
|
||||
|
||||
### 服务端数据库测试环境 (Node Environment)
|
||||
### Next.js Webapp Tests
|
||||
|
||||
目前只有 `packages/database` 下的测试可以通过配置 `TEST_SERVER_DB=1` 环境变量来使用服务端数据库测试
|
||||
- **Config File**: `vitest.config.ts`
|
||||
- **Environment**: Happy DOM (browser environment simulation)
|
||||
- **Database**: PGLite (PostgreSQL for browser environments)
|
||||
- **Setup File**: `tests/setup.ts`
|
||||
- **Purpose**: Testing React components, hooks, stores, utilities, and client-side logic
|
||||
|
||||
- **配置文件**: [packages/database/vitest.config.mts](mdc:packages/database/vitest.config.mts) 并且设置环境变量 `TEST_SERVER_DB=1`
|
||||
- **环境**: Node.js
|
||||
- **数据库**: 真实的 PostgreSQL 数据库
|
||||
- **并发限制**: 单线程运行 (`singleFork: true`)
|
||||
- **用途**: 测试数据库模型、服务端逻辑、API 端点等
|
||||
- **设置文件**: [packages/database/tests/setup-db.ts](mdc:packages/database/tests/setup-db.ts)
|
||||
### Packages Tests
|
||||
|
||||
## 测试运行命令
|
||||
Most packages use standard Vitest configuration. However, the `database` package is special:
|
||||
|
||||
** 性能警告**: 项目包含 3000+ 测试用例,完整运行需要约 10 分钟。务必使用文件过滤或测试名称过滤。
|
||||
#### Database Package (Special Case)
|
||||
|
||||
### 正确的命令格式
|
||||
The database package supports **dual-environment testing**:
|
||||
|
||||
| Environment | Database | Config | Use Case |
|
||||
|------------------|-----------------|---------------------------------------|-----------------------------------|
|
||||
| Client (Default) | PGLite | `packages/database/vitest.config.mts` | Fast local development |
|
||||
| Server | Real PostgreSQL | Set `TEST_SERVER_DB=1` | CI/CD, compatibility verification |
|
||||
|
||||
Server environment details:
|
||||
|
||||
- **Concurrency**: Single-threaded (`singleFork: true`)
|
||||
- **Setup File**: `packages/database/tests/setup-db.ts`
|
||||
- **Requirement**: `DATABASE_TEST_URL` environment variable must be set
|
||||
|
||||
### Desktop App Tests
|
||||
|
||||
- **Config File**: `apps/desktop/vitest.config.ts`
|
||||
- **Environment**: Node.js
|
||||
- **Purpose**: Testing Electron main process controllers, IPC handlers, and desktop-specific logic
|
||||
|
||||
## Test Commands
|
||||
|
||||
**Performance Warning**: The project contains 3000+ test cases. A full run takes approximately 10 minutes. Always use file filtering or test name filtering.
|
||||
|
||||
### Recommended Command Format
|
||||
|
||||
```bash
|
||||
# 运行所有客户端/服务端测试
|
||||
bunx vitest run --silent='passed-only' # 客户端测试
|
||||
cd packages/database && TEST_SERVER_DB=1 bunx vitest run --silent='passed-only' # 服务端测试
|
||||
# Run all client/server tests
|
||||
bunx vitest run --silent='passed-only' # Client tests
|
||||
cd packages/database && TEST_SERVER_DB=1 bunx vitest run --silent='passed-only' # Server tests
|
||||
|
||||
# 运行特定测试文件 (支持模糊匹配)
|
||||
# Run specific test file (supports fuzzy matching)
|
||||
bunx vitest run --silent='passed-only' user.test.ts
|
||||
|
||||
# 运行特定测试用例名称 (使用 -t 参数)
|
||||
# Run specific test case by name (using -t flag)
|
||||
bunx vitest run --silent='passed-only' -t "test case name"
|
||||
|
||||
# 组合使用文件和测试名称过滤
|
||||
# Combine file and test name filtering
|
||||
bunx vitest run --silent='passed-only' filename.test.ts -t "specific test"
|
||||
|
||||
# 生成覆盖率报告 (使用 --coverage 参数)
|
||||
# Generate coverage report (using --coverage flag)
|
||||
bunx vitest run --silent='passed-only' --coverage
|
||||
```
|
||||
|
||||
### 避免的命令格式
|
||||
### Commands to Avoid
|
||||
|
||||
```bash
|
||||
# 这些命令会运行所有 3000+ 测试用例,耗时约 10 分钟!
|
||||
# ❌ These commands run all 3000+ test cases, taking ~10 minutes!
|
||||
npm test
|
||||
npm test some-file.test.ts
|
||||
|
||||
# 不要使用裸 vitest (会进入 watch 模式)
|
||||
# ❌ Don't use bare vitest (enters watch mode)
|
||||
vitest test-file.test.ts
|
||||
```
|
||||
|
||||
## 测试修复原则
|
||||
## Test Fixing Principles
|
||||
|
||||
### 核心原则
|
||||
### Core Principles
|
||||
|
||||
1. **收集足够的上下文**
|
||||
在修复测试之前,务必做到:
|
||||
- 完整理解测试的意图和实现
|
||||
- 强烈建议阅读当前的 git diff 和 PR diff
|
||||
1. **Gather Sufficient Context**
|
||||
Before fixing tests, ensure you:
|
||||
- Fully understand the test's intent and implementation
|
||||
- Strongly recommended: review the current git diff and PR diff
|
||||
|
||||
2. **测试优先修复**
|
||||
如果是测试本身写错了,应优先修改测试,而不是实现代码。
|
||||
2. **Prioritize Test Fixes**
|
||||
If the test itself is incorrect, fix the test first rather than the implementation code.
|
||||
|
||||
3. **专注单一问题**
|
||||
只修复指定的测试,不要顺带添加额外测试。
|
||||
3. **Focus on a Single Issue**
|
||||
Only fix the specified test; don't add extra tests along the way.
|
||||
|
||||
4. **不自作主张**
|
||||
发现其他问题时,不要直接修改,需先提出并讨论。
|
||||
4. **Don't Act Unilaterally**
|
||||
When discovering other issues, don't modify them directly—raise and discuss first.
|
||||
|
||||
### 测试协作最佳实践
|
||||
### Testing Collaboration Best Practices
|
||||
|
||||
基于实际开发经验总结的重要协作原则:
|
||||
Important collaboration principles based on real development experience:
|
||||
|
||||
#### 1. 失败处理策略
|
||||
#### 1. Failure Handling Strategy
|
||||
|
||||
**核心原则**: 避免盲目重试,快速识别问题并寻求帮助。
|
||||
**Core Principle**: Avoid blind retries; quickly identify problems and seek help.
|
||||
|
||||
- **失败阈值**: 当连续尝试修复测试 1-2 次都失败后,应立即停止继续尝试
|
||||
- **问题总结**: 分析失败原因,整理已尝试的解决方案及其失败原因
|
||||
- **寻求帮助**: 带着清晰的问题摘要和尝试记录向团队寻求帮助
|
||||
- **避免陷阱**: 不要陷入"不断尝试相同或类似方法"的循环
|
||||
- **Failure Threshold**: After 1-2 consecutive failed fix attempts, stop immediately
|
||||
- **Problem Summary**: Analyze failure reasons and document attempted solutions with their failure causes
|
||||
- **Seek Help**: Approach the team with a clear problem summary and attempt history
|
||||
- **Avoid the Trap**: Don't fall into the loop of repeatedly trying the same or similar approaches
|
||||
|
||||
```typescript
|
||||
// 错误做法:连续失败后继续盲目尝试
|
||||
// 第3次、第4次仍在用相似的方法修复同一个问题
|
||||
// ❌ Wrong approach: Keep blindly trying after consecutive failures
|
||||
// 3rd, 4th attempts still using similar methods to fix the same problem
|
||||
|
||||
// 正确做法:失败1-2次后总结问题
|
||||
// ✅ Correct approach: Summarize after 1-2 failures
|
||||
/*
|
||||
问题总结:
|
||||
1. 尝试过的方法:修改 mock 数据结构
|
||||
2. 失败原因:仍然提示类型不匹配
|
||||
3. 具体错误:Expected 'UserData' but received 'UserProfile'
|
||||
4. 需要帮助:不确定最新的 UserData 接口定义
|
||||
Problem Summary:
|
||||
1. Attempted method: Modified mock data structure
|
||||
2. Failure reason: Still getting type mismatch error
|
||||
3. Specific error: Expected 'UserData' but received 'UserProfile'
|
||||
4. Help needed: Unsure about the latest UserData interface definition
|
||||
*/
|
||||
```
|
||||
|
||||
#### 2. 测试用例命名规范
|
||||
#### 2. Test Case Naming Conventions
|
||||
|
||||
**核心原则**: 测试应该关注"行为",而不是"实现细节"。
|
||||
**Core Principle**: Tests should focus on "behavior," not "implementation details."
|
||||
|
||||
- **描述业务场景**: `describe` 和 `it` 的标题应该描述具体的业务场景和预期行为
|
||||
- **避免实现绑定**: 不要在测试名称中提及具体的代码行号、覆盖率目标或实现细节
|
||||
- **保持稳定性**: 测试名称应该在代码重构后仍然有意义
|
||||
- **Describe Business Scenarios**: `describe` and `it` titles should describe specific business scenarios and expected behaviors
|
||||
- **Avoid Implementation Binding**: Don't mention specific line numbers, coverage goals, or implementation details in test names
|
||||
- **Maintain Stability**: Test names should remain meaningful after code refactoring
|
||||
|
||||
```typescript
|
||||
// 错误的测试命名
|
||||
// ❌ Poor test naming
|
||||
describe('User component coverage', () => {
|
||||
it('covers line 45-50 in getUserData', () => {
|
||||
// 为了覆盖第45-50行而写的测试
|
||||
// Test written just to cover lines 45-50
|
||||
});
|
||||
|
||||
it('tests the else branch', () => {
|
||||
// 仅为了测试某个分支而存在
|
||||
// Exists only to test a specific branch
|
||||
});
|
||||
});
|
||||
|
||||
// 正确的测试命名
|
||||
// ✅ Good test naming
|
||||
describe('<UserAvatar />', () => {
|
||||
it('should render fallback icon when image url is not provided', () => {
|
||||
// 测试具体的业务场景,自然会覆盖相关代码分支
|
||||
// Tests a specific business scenario, naturally covering relevant code branches
|
||||
});
|
||||
|
||||
it('should display user initials when avatar image fails to load', () => {
|
||||
// 描述用户行为和预期结果
|
||||
// Describes user behavior and expected outcome
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**覆盖率提升的正确思路**:
|
||||
**The Right Approach to Improving Coverage**:
|
||||
|
||||
- 通过设计各种业务场景(正常流程、边缘情况、错误处理)来自然提升覆盖率
|
||||
- 不要为了达到覆盖率数字而写测试,更不要在测试中注释"为了覆盖 xxx 行"
|
||||
- Naturally improve coverage by designing various business scenarios (happy paths, edge cases, error handling)
|
||||
- Don't write tests just to hit coverage numbers, and never comment "to cover line xxx" in tests
|
||||
|
||||
#### 3. 测试组织结构
|
||||
#### 3. Test Organization Structure
|
||||
|
||||
**核心原则**: 维护清晰的测试层次结构,避免冗余的顶级测试块。
|
||||
**Core Principle**: Maintain a clear test hierarchy; avoid redundant top-level test blocks.
|
||||
|
||||
- **复用现有结构**: 添加新测试时,优先在现有的 `describe` 块中寻找合适的位置
|
||||
- **逻辑分组**: 相关的测试用例应该组织在同一个 `describe` 块内
|
||||
- **避免碎片化**: 不要为了单个测试用例就创建新的顶级 `describe` 块
|
||||
- **Reuse Existing Structure**: When adding new tests, first look for an appropriate place in existing `describe` blocks
|
||||
- **Logical Grouping**: Related test cases should be organized within the same `describe` block
|
||||
- **Avoid Fragmentation**: Don't create a new top-level `describe` block for a single test case
|
||||
|
||||
```typescript
|
||||
// 错误的组织方式:创建过多顶级块
|
||||
// ❌ Poor organization: Too many top-level blocks
|
||||
describe('<UserProfile />', () => {
|
||||
it('should render user name', () => {});
|
||||
});
|
||||
|
||||
describe('UserProfile new prop test', () => {
|
||||
// 不必要的新块
|
||||
// Unnecessary new block
|
||||
it('should handle email display', () => {});
|
||||
});
|
||||
|
||||
describe('UserProfile edge cases', () => {
|
||||
// 不必要的新块
|
||||
// Unnecessary new block
|
||||
it('should handle missing avatar', () => {});
|
||||
});
|
||||
|
||||
// 正确的组织方式:合并相关测试
|
||||
// ✅ Good organization: Merge related tests
|
||||
describe('<UserProfile />', () => {
|
||||
it('should render user name', () => {});
|
||||
|
||||
@@ -178,78 +204,78 @@ describe('<UserProfile />', () => {
|
||||
it('should handle missing avatar', () => {});
|
||||
|
||||
describe('when user data is incomplete', () => {
|
||||
// 只有在有多个相关子场景时才创建子组
|
||||
// Only create sub-groups when there are multiple related sub-scenarios
|
||||
it('should show placeholder for missing name', () => {});
|
||||
it('should hide email section when email is undefined', () => {});
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**组织决策流程**:
|
||||
**Organization Decision Flow**:
|
||||
|
||||
1. 是否存在逻辑相关的现有 `describe` 块? → 如果有,添加到其中
|
||||
2. 是否有多个(3个以上)相关的测试用例? → 如果有,可以考虑创建新的子 `describe`
|
||||
3. 是否是独立的、无关联的功能模块? → 如果是,才考虑创建新的顶级 `describe`
|
||||
1. Is there a logically related existing `describe` block? → If yes, add to it
|
||||
2. Are there multiple (3+) related test cases? → If yes, consider creating a new sub-`describe`
|
||||
3. Is it an independent, unrelated feature module? → Only then consider creating a new top-level `describe`
|
||||
|
||||
### 测试修复流程
|
||||
### Test Fixing Workflow
|
||||
|
||||
1. **复现问题**: 定位并运行失败的测试,确认能在本地复现
|
||||
2. **分析原因**: 阅读测试代码、错误日志和相关文件的 Git 修改历史
|
||||
3. **建立假设**: 判断问题出在测试逻辑、实现代码还是环境配置
|
||||
4. **修复验证**: 根据假设进行修复,重新运行测试确认通过
|
||||
5. **扩大验证**: 运行当前文件内所有测试,确保没有引入新问题
|
||||
6. **撰写总结**: 说明错误原因和修复方法
|
||||
1. **Reproduce the Issue**: Locate and run the failing test; confirm it can be reproduced locally
|
||||
2. **Analyze the Cause**: Read test code, error logs, and Git history of related files
|
||||
3. **Form a Hypothesis**: Determine if the problem is in test logic, implementation code, or environment configuration
|
||||
4. **Fix and Verify**: Apply the fix based on your hypothesis; rerun the test to confirm it passes
|
||||
5. **Expand Verification**: Run all tests in the current file to ensure no new issues were introduced
|
||||
6. **Write a Summary**: Document the error cause and fix method
|
||||
|
||||
### 修复完成后的总结
|
||||
### Post-Fix Summary
|
||||
|
||||
测试修复完成后,应该提供简要说明,包括:
|
||||
After completing a test fix, provide a brief explanation including:
|
||||
|
||||
1. **错误原因分析**: 说明测试失败的根本原因
|
||||
- 测试逻辑错误
|
||||
- 实现代码bug
|
||||
- 环境配置问题
|
||||
- 依赖变更导致的问题
|
||||
1. **Root Cause Analysis**: Explain the fundamental reason for the test failure
|
||||
- Test logic error
|
||||
- Implementation bug
|
||||
- Environment configuration issue
|
||||
- Dependency change
|
||||
|
||||
2. **修复方法说明**: 简述采用的修复方式
|
||||
- 修改了哪些文件
|
||||
- 采用了什么解决方案
|
||||
- 为什么选择这种修复方式
|
||||
2. **Fix Description**: Briefly describe the fix approach
|
||||
- Which files were modified
|
||||
- What solution was applied
|
||||
- Why this fix approach was chosen
|
||||
|
||||
**示例格式**:
|
||||
**Example Format**:
|
||||
|
||||
```markdown
|
||||
## 测试修复总结
|
||||
## Test Fix Summary
|
||||
|
||||
**错误原因**: 测试中的 mock 数据格式与实际 API 返回格式不匹配,导致断言失败。
|
||||
**Root Cause**: The mock data format in the test didn't match the actual API response format, causing assertion failures.
|
||||
|
||||
**修复方法**: 更新了测试文件中的 mock 数据结构,使其与最新的 API 响应格式保持一致。具体修改了 `user.test.ts` 中的 `mockUserData` 对象结构。
|
||||
**Fix**: Updated the mock data structure in the test file to match the latest API response format. Specifically modified the `mockUserData` object structure in `user.test.ts`.
|
||||
```
|
||||
|
||||
## 测试编写最佳实践
|
||||
## Test Writing Best Practices
|
||||
|
||||
### Mock 数据策略:追求"低成本的真实性"
|
||||
### Mock Data Strategy: Aim for "Low-Cost Authenticity"
|
||||
|
||||
**核心原则**: 测试数据应默认追求真实性,只有在引入"高昂的测试成本"时才进行简化。
|
||||
**Core Principle**: Test data should default to authenticity; only simplify when it introduces "high testing costs."
|
||||
|
||||
#### 什么是"高昂的测试成本"?
|
||||
#### What Are "High Testing Costs"?
|
||||
|
||||
"高成本"指的是测试中引入了外部依赖,使测试变慢、不稳定或复杂:
|
||||
"High cost" refers to introducing external dependencies in tests that make them slow, unstable, or complex:
|
||||
|
||||
- **文件 I/O 操作**:读写硬盘文件
|
||||
- **网络请求**:HTTP 调用、数据库连接
|
||||
- **系统调用**:获取系统时间、环境变量等
|
||||
- **File I/O Operations**: Reading/writing disk files
|
||||
- **Network Requests**: HTTP calls, database connections
|
||||
- **System Calls**: Getting system time, environment variables, etc.
|
||||
|
||||
#### 推荐做法:Mock 依赖,保留真实数据
|
||||
#### Recommended Approach: Mock Dependencies, Keep Real Data
|
||||
|
||||
```typescript
|
||||
// 好的做法:Mock I/O 操作,但使用真实的文件内容格式
|
||||
// ✅ Good approach: Mock I/O operations but use real file content formats
|
||||
describe('parseContentType', () => {
|
||||
beforeEach(() => {
|
||||
// Mock 文件读取操作(避免真实 I/O)
|
||||
// Mock file read operation (avoid real I/O)
|
||||
vi.spyOn(fs, 'readFileSync').mockImplementation((path) => {
|
||||
// 但返回真实的文件内容格式
|
||||
if (path.includes('.pdf')) return '%PDF-1.4\n%âãÏÓ'; // 真实 PDF 文件头
|
||||
if (path.includes('.png')) return '\x89PNG\r\n\x1a\n'; // 真实 PNG 文件头
|
||||
// But return real file content formats
|
||||
if (path.includes('.pdf')) return '%PDF-1.4\n%âãÏÓ'; // Real PDF header
|
||||
if (path.includes('.png')) return '\x89PNG\r\n\x1a\n'; // Real PNG header
|
||||
return '';
|
||||
});
|
||||
});
|
||||
@@ -260,40 +286,38 @@ describe('parseContentType', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// 过度简化:使用不真实的数据
|
||||
// ❌ Over-simplified: Using unrealistic data
|
||||
describe('parseContentType', () => {
|
||||
it('should detect PDF content type correctly', () => {
|
||||
// 这种简化数据没有测试价值
|
||||
// This simplified data has no test value
|
||||
const result = parseContentType('fake-pdf-content');
|
||||
expect(result).toBe('application/pdf');
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
#### 真实标识符的价值
|
||||
#### The Value of Real Identifiers
|
||||
|
||||
```typescript
|
||||
// ✅ 使用真实标识符
|
||||
// ✅ Use real identifiers
|
||||
const result = parseModelString('openai', '+gpt-4,+gpt-3.5-turbo');
|
||||
|
||||
// ❌ 使用占位符(价值较低)
|
||||
// ❌ Use placeholders (lower value)
|
||||
const result = parseModelString('test-provider', '+model1,+model2');
|
||||
```
|
||||
|
||||
### 现代化Mock技巧:环境设置与Mock方法
|
||||
### Modern Mocking Techniques: Environment Setup and Mock Methods
|
||||
|
||||
**环境设置 + Mock方法结合使用**
|
||||
|
||||
客户端代码测试时,推荐使用环境注释配合现代化Mock方法:
|
||||
When testing client-side code, use environment annotations with modern mock methods:
|
||||
|
||||
```typescript
|
||||
/**
|
||||
* @vitest-environment happy-dom // 提供浏览器API
|
||||
* @vitest-environment happy-dom // Provides browser APIs
|
||||
*/
|
||||
import { beforeEach, vi } from 'vitest';
|
||||
|
||||
beforeEach(() => {
|
||||
// 现代方法1:使用vi.stubGlobal替代global.xxx = ...
|
||||
// Modern method 1: Use vi.stubGlobal instead of global.xxx = ...
|
||||
const mockImage = vi.fn().mockImplementation(() => ({
|
||||
addEventListener: vi.fn(),
|
||||
naturalHeight: 600,
|
||||
@@ -301,72 +325,72 @@ beforeEach(() => {
|
||||
}));
|
||||
vi.stubGlobal('Image', mockImage);
|
||||
|
||||
// 现代方法2:使用vi.spyOn保留原功能,只mock特定方法
|
||||
// Modern method 2: Use vi.spyOn to preserve original functionality, only mock specific methods
|
||||
vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:mock-url');
|
||||
vi.spyOn(URL, 'revokeObjectURL').mockImplementation(() => {});
|
||||
});
|
||||
```
|
||||
|
||||
**环境选择优先级**
|
||||
#### Environment Selection Priority
|
||||
|
||||
1. **@vitest-environment happy-dom** (推荐) - 轻量、快速,项目已安装
|
||||
2. **@vitest-environment jsdom** - 功能完整,但需要额外安装jsdom包
|
||||
3. **不设置环境** - Node.js环境,需要手动mock所有浏览器API
|
||||
1. **@vitest-environment happy-dom** (Recommended) - Lightweight, fast, already installed in the project
|
||||
2. **@vitest-environment jsdom** - Full-featured, but requires additional jsdom package installation
|
||||
3. **No environment set** - Node.js environment, requires manually mocking all browser APIs
|
||||
|
||||
**Mock方法对比**
|
||||
#### Mock Method Comparison
|
||||
|
||||
```typescript
|
||||
// ❌ 旧方法:直接操作global对象(类型问题)
|
||||
// ❌ Old method: Directly manipulating global object (type issues)
|
||||
global.Image = mockImage;
|
||||
global.URL = { ...global.URL, createObjectURL: mockFn };
|
||||
|
||||
// ✅ 现代方法:类型安全的vi API
|
||||
vi.stubGlobal('Image', mockImage); // 完全替换全局对象
|
||||
vi.spyOn(URL, 'createObjectURL'); // 部分mock,保留其他功能
|
||||
// ✅ Modern method: Type-safe vi API
|
||||
vi.stubGlobal('Image', mockImage); // Completely replace global object
|
||||
vi.spyOn(URL, 'createObjectURL'); // Partial mock, preserve other functionality
|
||||
```
|
||||
|
||||
### 测试覆盖率原则:代码分支优于用例数量
|
||||
### Test Coverage Principles: Code Branches Over Test Quantity
|
||||
|
||||
**核心原则**: 优先覆盖所有代码分支,而非编写大量重复用例
|
||||
**Core Principle**: Prioritize covering all code branches rather than writing many repetitive test cases.
|
||||
|
||||
```typescript
|
||||
// ❌ 过度测试:29个测试用例都验证相同分支
|
||||
// ❌ Over-testing: 29 test cases all validating the same branch
|
||||
describe('getImageDimensions', () => {
|
||||
it('should reject .txt files');
|
||||
it('should reject .pdf files');
|
||||
// ... 25个类似测试,都走相同的验证分支
|
||||
// ... 25 similar tests, all hitting the same validation branch
|
||||
});
|
||||
|
||||
// ✅ 精简测试:4个核心用例覆盖所有分支
|
||||
// ✅ Lean testing: 4 core cases covering all branches
|
||||
describe('getImageDimensions', () => {
|
||||
it('should return dimensions for valid File object'); // 成功路径 - File
|
||||
it('should return dimensions for valid data URI'); // 成功路径 - String
|
||||
it('should return undefined for invalid inputs'); // 输入验证分支
|
||||
it('should return undefined when image fails to load'); // 错误处理分支
|
||||
it('should return dimensions for valid File object'); // Success path - File
|
||||
it('should return dimensions for valid data URI'); // Success path - String
|
||||
it('should return undefined for invalid inputs'); // Input validation branch
|
||||
it('should return undefined when image fails to load'); // Error handling branch
|
||||
});
|
||||
```
|
||||
|
||||
**分支覆盖策略**
|
||||
#### Branch Coverage Strategy
|
||||
|
||||
1. **成功路径** - 每种输入类型1个测试即可
|
||||
2. **边界条件** - 合并类似场景到单个测试
|
||||
3. **错误处理** - 测试代表性错误即可
|
||||
4. **业务逻辑** - 覆盖所有if/else分支
|
||||
1. **Success Paths** - One test per input type is sufficient
|
||||
2. **Boundary Conditions** - Consolidate similar scenarios into a single test
|
||||
3. **Error Handling** - Test representative errors only
|
||||
4. **Business Logic** - Cover all if/else branches
|
||||
|
||||
**合理测试数量**
|
||||
#### Reasonable Test Counts
|
||||
|
||||
- 简单工具函数:2-5个测试
|
||||
- 复杂业务逻辑:5-10个测试
|
||||
- 核心安全功能:适当增加,但避免重复路径
|
||||
- Simple utility functions: 2-5 tests
|
||||
- Complex business logic: 5-10 tests
|
||||
- Core security features: Add more as needed, but avoid duplicate paths
|
||||
|
||||
### 错误处理测试:测试"行为"而非"文本"
|
||||
### Error Handling Tests: Test "Behavior" Not "Text"
|
||||
|
||||
**核心原则**: 测试应该验证程序在错误发生时的行为是可预测的,而不是验证易变的错误信息文本。
|
||||
**Core Principle**: Tests should verify that program behavior is predictable when errors occur, not verify error message text that may change.
|
||||
|
||||
#### 推荐的错误测试方式
|
||||
#### Recommended Error Testing Approach
|
||||
|
||||
```typescript
|
||||
// ✅ 测试错误类型和属性
|
||||
// ✅ Test error types and properties
|
||||
expect(() => validateUser({})).toThrow(ValidationError);
|
||||
expect(() => processPayment({})).toThrow(
|
||||
expect.objectContaining({
|
||||
@@ -375,136 +399,136 @@ expect(() => processPayment({})).toThrow(
|
||||
}),
|
||||
);
|
||||
|
||||
// ❌ 避免测试具体错误文本
|
||||
expect(() => processUser({})).toThrow('用户数据不能为空,请检查输入参数');
|
||||
// ❌ Avoid testing specific error text
|
||||
expect(() => processUser({})).toThrow('User data cannot be empty, please check input parameters');
|
||||
```
|
||||
|
||||
### 疑难解答:警惕模块污染
|
||||
### Troubleshooting: Beware of Module Pollution
|
||||
|
||||
**识别信号**: 当你的测试出现以下"灵异"现象时,优先怀疑模块污染:
|
||||
**Warning Signs**: When your tests exhibit these "mysterious" behaviors, suspect module pollution first:
|
||||
|
||||
- 单独运行某个测试通过,但和其他测试一起运行就失败
|
||||
- 测试的执行顺序影响结果
|
||||
- Mock 设置看起来正确,但实际使用的是旧的 Mock 版本
|
||||
- A test passes when run alone but fails when run with other tests
|
||||
- Test execution order affects results
|
||||
- Mock setup appears correct but actually uses an old mock version
|
||||
|
||||
#### 典型场景:动态 Mock 同一模块
|
||||
#### Typical Scenario: Dynamic Mocking of the Same Module
|
||||
|
||||
```typescript
|
||||
// ❌ 问题:动态Mock同一模块
|
||||
// ❌ Problem: Dynamic mocking of the same module
|
||||
it('dev mode', async () => {
|
||||
vi.doMock('./config', () => ({ isDev: true }));
|
||||
const { getSettings } = await import('./service'); // 可能使用缓存
|
||||
const { getSettings } = await import('./service'); // May use cache
|
||||
});
|
||||
|
||||
// ✅ 解决:清除模块缓存
|
||||
// ✅ Solution: Clear module cache
|
||||
beforeEach(() => {
|
||||
vi.resetModules(); // 确保每个测试都是干净环境
|
||||
vi.resetModules(); // Ensure each test has a clean environment
|
||||
});
|
||||
```
|
||||
|
||||
**记住**: `vi.resetModules()` 是解决测试"灵异"失败的终极武器。
|
||||
**Remember**: `vi.resetModules()` is the ultimate weapon for resolving "mysterious" test failures.
|
||||
|
||||
## 测试文件组织
|
||||
## Test File Organization
|
||||
|
||||
### 文件命名约定
|
||||
### File Naming Convention
|
||||
|
||||
`*.test.ts`, `*.test.tsx` (任意位置)
|
||||
`*.test.ts`, `*.test.tsx` (any location)
|
||||
|
||||
### 测试文件组织风格
|
||||
### Test File Organization Style
|
||||
|
||||
项目采用 **测试文件与源文件同目录** 的组织风格:
|
||||
The project uses a **co-located test files** organization style:
|
||||
|
||||
- 测试文件放在对应源文件的同一目录下
|
||||
- 命名格式:`原文件名.test.ts` 或 `原文件名.test.tsx`
|
||||
- Test files are placed in the same directory as the corresponding source files
|
||||
- Naming format: `originalFileName.test.ts` or `originalFileName.test.tsx`
|
||||
|
||||
例如:
|
||||
Example:
|
||||
|
||||
```plaintext
|
||||
src/components/Button/
|
||||
├── index.tsx # 源文件
|
||||
└── index.test.tsx # 测试文件
|
||||
├── index.tsx # Source file
|
||||
└── index.test.tsx # Test file
|
||||
```
|
||||
|
||||
- 也有少数情况会统一放到 `__tests__` 文件夹, 例如 `packages/database/src/models/__tests__`
|
||||
- 测试使用的辅助文件放到 fixtures 文件夹
|
||||
- In some cases, tests are consolidated in a `__tests__` folder, e.g., `packages/database/src/models/__tests__`
|
||||
- Test helper files are placed in a fixtures folder
|
||||
|
||||
## 测试调试技巧
|
||||
## Test Debugging Tips
|
||||
|
||||
### 测试调试步骤
|
||||
### Test Debugging Steps
|
||||
|
||||
1. **确定测试环境**: 根据文件路径选择正确的配置文件
|
||||
2. **隔离问题**: 使用 `-t` 参数只运行失败的测试用例
|
||||
3. **分析错误**: 仔细阅读错误信息、堆栈跟踪和最近的文件修改记录
|
||||
4. **添加调试**: 在测试中添加 `console.log` 了解执行流程
|
||||
1. **Determine Test Environment**: Select the correct config file based on file path
|
||||
2. **Isolate the Problem**: Use the `-t` flag to run only the failing test case
|
||||
3. **Analyze the Error**: Carefully read error messages, stack traces, and recent file modification history
|
||||
4. **Add Debugging**: Add `console.log` statements in tests to understand execution flow
|
||||
|
||||
### TypeScript 类型处理
|
||||
### TypeScript Type Handling
|
||||
|
||||
在测试中,为了提高编写效率和可读性,可以适当放宽 TypeScript 类型检测:
|
||||
In tests, you can relax TypeScript type checking to improve writing efficiency and readability:
|
||||
|
||||
#### 推荐的类型放宽策略
|
||||
#### Recommended Type Relaxation Strategies
|
||||
|
||||
```typescript
|
||||
// 使用非空断言访问测试中确定存在的属性
|
||||
// Use non-null assertion to access properties you're certain exist in tests
|
||||
const result = await someFunction();
|
||||
expect(result!.data).toBeDefined();
|
||||
expect(result!.status).toBe('success');
|
||||
|
||||
// 使用 any 类型简化复杂的 Mock 设置
|
||||
// Use any type to simplify complex mock setups
|
||||
const mockStream = new ReadableStream() as any;
|
||||
mockStream.toReadableStream = () => mockStream;
|
||||
|
||||
// 访问私有成员
|
||||
await instance['getFromCache']('key'); // 推荐中括号
|
||||
await (instance as any).getFromCache('key'); // 避免as any
|
||||
// Access private members
|
||||
await instance['getFromCache']('key'); // Bracket notation recommended
|
||||
await (instance as any).getFromCache('key'); // Avoid as any
|
||||
```
|
||||
|
||||
#### 适用场景
|
||||
#### Applicable Scenarios
|
||||
|
||||
- **Mock 对象**: 对于测试用的 Mock 数据,使用 `as any` 避免复杂的类型定义
|
||||
- **第三方库**: 处理复杂的第三方库类型时,适当使用 `any` 提高效率
|
||||
- **测试断言**: 在确定对象存在的测试场景中,使用 `!` 非空断言
|
||||
- **私有成员访问**: 优先使用中括号 `instance['privateMethod']()` 而不是 `(instance as any).privateMethod()`
|
||||
- **临时调试**: 快速编写测试时,先用 `any` 保证功能,后续可选择性地优化类型
|
||||
- **Mock Objects**: Use `as any` for test mock data to avoid complex type definitions
|
||||
- **Third-Party Libraries**: Use `any` appropriately when handling complex third-party library types
|
||||
- **Test Assertions**: Use `!` non-null assertion in test scenarios where you're certain the object exists
|
||||
- **Private Member Access**: Prefer bracket notation `instance['privateMethod']()` over `(instance as any).privateMethod()`
|
||||
- **Temporary Debugging**: When quickly writing tests, use `any` first to ensure functionality, then optionally optimize types later
|
||||
|
||||
#### 注意事项
|
||||
#### Important Notes
|
||||
|
||||
- **适度使用**: 不要过度依赖 `any`,核心业务逻辑的类型仍应保持严格
|
||||
- **私有成员访问优先级**: 中括号访问 > `as any` 转换,保持更好的类型安全性
|
||||
- **文档说明**: 对于使用 `any` 的复杂场景,添加注释说明原因
|
||||
- **测试覆盖**: 确保即使使用了 `any`,测试仍能有效验证功能正确性
|
||||
- **Use Moderately**: Don't over-rely on `any`; core business logic types should remain strict
|
||||
- **Private Member Access Priority**: Bracket notation > `as any` casting for better type safety
|
||||
- **Documentation**: Add comments explaining the reason for complex `any` usage scenarios
|
||||
- **Test Coverage**: Ensure tests still effectively verify correctness even when using `any`
|
||||
|
||||
### 检查最近修改记录
|
||||
### Checking Recent Modifications
|
||||
|
||||
**核心原则**:测试突然失败时,优先检查最近的代码修改。
|
||||
**Core Principle**: When tests suddenly fail, first check recent code changes.
|
||||
|
||||
#### 快速检查方法
|
||||
#### Quick Check Methods
|
||||
|
||||
```bash
|
||||
git status # 查看当前修改状态
|
||||
git diff HEAD -- '*.test.*' # 检查测试文件改动
|
||||
git diff main...HEAD # 对比主分支差异
|
||||
gh pr diff # 查看PR中的所有改动
|
||||
git status # View current modification status
|
||||
git diff HEAD -- '*.test.*' # Check test file changes
|
||||
git diff main...HEAD # Compare with main branch
|
||||
gh pr diff # View all changes in the PR
|
||||
```
|
||||
|
||||
#### 常见原因与解决
|
||||
#### Common Causes and Solutions
|
||||
|
||||
- **最新提交引入bug** → 检查并修复实现代码
|
||||
- **分支代码滞后** → `git rebase main` 同步主分支
|
||||
- **Latest commit introduced a bug** → Check and fix the implementation code
|
||||
- **Branch code is outdated** → `git rebase main` to sync with main branch
|
||||
|
||||
## 特殊场景的测试
|
||||
## Special Testing Scenarios
|
||||
|
||||
针对一些特殊场景的测试,需要阅读相关 rules:
|
||||
For special testing scenarios, refer to the related rules:
|
||||
|
||||
- [Electron IPC 接口测试策略](mdc:./electron-ipc-test.mdc)
|
||||
- [数据库 Model 测试指南](mdc:./db-model-test.mdc)
|
||||
- `electron-ipc-test.mdc` - Electron IPC Interface Testing Strategy
|
||||
- `db-model-test.mdc` - Database Model Testing Guide
|
||||
|
||||
## 核心要点
|
||||
## Key Takeaways
|
||||
|
||||
- **命令格式**: 使用 `bunx vitest run --silent='passed-only'` 并指定文件过滤
|
||||
- **修复原则**: 失败1-2次后寻求帮助,测试命名关注行为而非实现细节
|
||||
- **调试流程**: 复现 → 分析 → 假设 → 修复 → 验证 → 总结
|
||||
- **文件组织**: 优先在现有 `describe` 块中添加测试,避免创建冗余顶级块
|
||||
- **数据策略**: 默认追求真实性,只有高成本(I/O、网络等)时才简化
|
||||
- **错误测试**: 测试错误类型和行为,避免依赖具体的错误信息文本
|
||||
- **模块污染**: 测试"灵异"失败时,优先怀疑模块污染,使用 `vi.resetModules()` 解决
|
||||
- **安全要求**: Model 测试必须包含权限检查,并在双环境下验证通过
|
||||
- **Command Format**: Use `bunx vitest run --silent='passed-only'` with file filtering
|
||||
- **Fix Principles**: Seek help after 1-2 failures; focus test naming on behavior, not implementation details
|
||||
- **Debug Workflow**: Reproduce → Analyze → Hypothesize → Fix → Verify → Summarize
|
||||
- **File Organization**: Prefer adding tests to existing `describe` blocks; avoid creating redundant top-level blocks
|
||||
- **Data Strategy**: Default to authenticity; only simplify for high-cost scenarios (I/O, network, etc.)
|
||||
- **Error Testing**: Test error types and behavior; avoid depending on specific error message text
|
||||
- **Module Pollution**: When tests fail "mysteriously," suspect module pollution first; use `vi.resetModules()` to resolve
|
||||
- **Security Requirements**: Model tests must include permission checks and pass in both environments
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
description: Best practices for testing Zustand store actions
|
||||
globs: 'src/store/**/*.test.ts'
|
||||
globs: src/store/**/*.test.ts
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ Main interfaces exposed for UI component consumption:
|
||||
|
||||
- Naming: Verb form (`createTopic`, `sendMessage`, `updateTopicTitle`)
|
||||
- Responsibilities: Parameter validation, flow orchestration, calling internal actions
|
||||
- Example: [src/store/chat/slices/topic/action.ts](mdc:src/store/chat/slices/topic/action.ts)
|
||||
- Example: `src/store/chat/slices/topic/action.ts`
|
||||
|
||||
```typescript
|
||||
// Public Action example
|
||||
|
||||
@@ -105,7 +105,7 @@ export const initialTopicState: ChatTopicState = {
|
||||
};
|
||||
```
|
||||
|
||||
2. `reducer.ts` (复杂状态使用):
|
||||
1. `reducer.ts` (复杂状态使用):
|
||||
- 定义纯函数 reducer,处理同步状态转换
|
||||
- 使用 `immer` 确保不可变更新
|
||||
|
||||
@@ -151,7 +151,7 @@ export const topicReducer = (state: ChatTopic[] = [], payload: ChatTopicDispatch
|
||||
};
|
||||
```
|
||||
|
||||
3. `selectors.ts`:
|
||||
1. `selectors.ts`:
|
||||
- 提供状态查询和计算函数
|
||||
- 供 UI 组件使用的状态订阅接口
|
||||
- 重要: 使用 `export const xxxSelectors` 模式聚合所有 selectors
|
||||
@@ -186,7 +186,7 @@ export const topicSelectors = {
|
||||
|
||||
当 slice 的 actions 过于复杂时,可以拆分到子目录:
|
||||
|
||||
```
|
||||
```plaintext
|
||||
src/store/chat/slices/aiChat/
|
||||
├── actions/
|
||||
│ ├── generateAIChat.ts # AI 对话生成
|
||||
@@ -204,7 +204,7 @@ src/store/chat/slices/aiChat/
|
||||
|
||||
管理多种内置工具的状态:
|
||||
|
||||
```
|
||||
```plaintext
|
||||
src/store/chat/slices/builtinTool/
|
||||
├── actions/
|
||||
│ ├── dalle.ts # DALL-E 图像生成
|
||||
|
||||
Reference in New Issue
Block a user