style: 完善代码规范和测试覆盖

- 新增多个模块的单元测试文件,提升测试覆盖率
- 完善AI-Reading文档系统,包含7步代码检查流程
- 新增集成测试和属性测试框架
- 优化项目结构和配置文件
- 清理过时的规范文档,统一使用新的检查标准
This commit is contained in:
moyin
2026-01-12 20:09:03 +08:00
parent 59128ea9a6
commit 5af44f95d5
22 changed files with 2595 additions and 2096 deletions

5
.gitignore vendored
View File

@@ -45,4 +45,7 @@ coverage/
# Redis数据文件本地开发用 # Redis数据文件本地开发用
redis-data/ redis-data/
.kiro/ .kiro/
config/
docs/merge-requests

View File

@@ -1,346 +0,0 @@
# AI代码检查规范简洁版- Whale Town 游戏服务器专用
## 执行原则
- **分步执行**:每次只执行一个步骤,完成后等待用户确认
- **用户信息收集**:开始前必须收集用户当前日期和名称
- **修改验证**:每次修改后必须重新检查该步骤
- **项目特性适配**针对NestJS游戏服务器的双模式架构和实时通信特点优化
## 检查步骤
### 步骤1命名规范检查
- **文件/文件夹**snake_case下划线分隔保持项目一致性
- **变量/函数**camelCase
- **类/接口**PascalCase
- **常量**SCREAMING_SNAKE_CASE
- **路由**kebab-case
- **文件夹优化**:删除单文件文件夹,扁平化结构
- **Core层命名**业务支撑模块用_core后缀通用工具模块不用
- **游戏服务器特殊规范**
- WebSocket Gateway文件`*.gateway.ts`
- 实时通信相关:`websocket_*`, `realtime_*`
- 双模式服务:`*_memory.service.ts`, `*_database.service.ts`
- 属性测试:`*.property.spec.ts`
- 集成测试:`*.integration.spec.ts`
- E2E测试`*.e2e.spec.ts`
#### 文件夹结构检查要求
**必须使用listDirectory工具详细检查每个文件夹的内容**
1. 使用`listDirectory(path, depth=2)`获取完整文件夹结构
2. 统计每个文件夹内的文件数量
3. 识别只有1个文件的文件夹单文件文件夹
4. 将单文件文件夹中的文件移动到上级目录
5. 更新所有相关的import路径引用
**检查标准:**
- 不超过3个文件的文件夹必须扁平化处理
- 4个以上文件通常保持独立文件夹
- 完整功能模块:即使文件较少也可以保持独立(需特殊说明)
- **测试文件位置**测试文件必须与对应源文件放在同一目录不允许单独的tests文件夹
**测试文件位置规范(重要):**
-**正确位置**:测试文件必须与对应源文件放在同一目录
-**错误位置**测试文件放在单独的tests/、test/、spec/、__tests__/等文件夹中
- **游戏服务器测试分类**
- 单元测试:`*.spec.ts` - 基础功能测试
- 集成测试:`*.integration.spec.ts` - 模块间交互测试
- 属性测试:`*.property.spec.ts` - 基于属性的随机测试(适用于管理员模块)
- E2E测试`*.e2e.spec.ts` - 端到端业务流程测试
- 性能测试:`*.perf.spec.ts` - WebSocket和实时通信性能测试
**常见错误:**
- 只看文件夹名称,不检查内容
- 凭印象判断,不使用工具获取准确数据
- 遗漏3个文件以下文件夹的识别
- **忽略测试文件夹**认为tests文件夹是"标准结构"而不进行扁平化检查
### 步骤2注释规范检查
- **文件头注释**:功能描述、职责分离、修改记录、@author@version@since@lastModified
- **类注释**:职责、主要方法、使用场景
- **方法注释**:业务逻辑步骤、@param@returns@throws@example
- **修改记录**:使用用户提供的日期和名称,格式"日期: 类型 - 内容 (修改者: 名称)"
- **@author处理规范**
- **保留原则**:人名必须保留,不得随意修改
- **AI标识替换**只有AI标识kiro、ChatGPT、Claude、AI等才可替换为用户名称
- **判断示例**`@author kiro` → 可替换,`@author 张三` → 必须保留
- **版本号递增**:规范优化/Bug修复→修订版本+1功能变更→次版本+1重构→主版本+1
- **时间更新规则**
- **仅检查不修改**:如果只是进行代码检查而没有实际修改文件内容,不更新@lastModified字段
- **实际修改才更新**:只有真正修改了文件内容(功能代码、注释内容、结构调整等)时才更新@lastModified字段
- **检查规范强调**:注释规范检查本身不是修改,除非发现需要修正的问题并进行了实际修改
- **Git变更检测**通过git status和git diff检查文件是否有实际变更只有git显示文件被修改时才需要添加修改记录和更新时间戳
### 步骤3代码质量检查
- **清理未使用**:导入、变量、方法
- **常量定义**使用SCREAMING_SNAKE_CASE
- **方法长度**建议不超过50行
- **代码重复**:识别并消除重复代码
- **魔法数字**:提取为常量定义
- **工具函数**:抽象重复逻辑为可复用函数
- **TODO项处理**最终文件不能包含TODO项必须真正实现功能或删除未完成代码
### 步骤4架构分层检查
- **检查范围**:仅检查当前执行检查的文件夹,不考虑其他同层功能模块
- **Core层**:专注技术实现,不含业务逻辑
- **Core层命名规则**
- **业务支撑模块**:为特定业务功能提供技术支撑,使用`_core`后缀(如:`location_broadcast_core`
- **通用工具模块**:提供可复用的数据访问或技术服务,不使用后缀(如:`user_profiles``redis_cache`
- **判断方法**:检查模块是否专门为某个业务服务,如果是则使用`_core`后缀,如果是通用服务则不使用
- **Business层**:专注业务逻辑,不含技术实现细节
- **依赖关系**Core层不能导入Business层Business层通过依赖注入使用Core层
- **职责分离**:确保各层职责清晰,边界明确
### 步骤5测试覆盖检查
- **测试文件存在性**每个Service、Controller、Gateway必须有对应测试文件
- **游戏服务器测试要求**
-**Service类**:文件名包含`.service.ts`的业务逻辑类
-**Controller类**:文件名包含`.controller.ts`的控制器类
-**Gateway类**:文件名包含`.gateway.ts`的WebSocket网关类
-**Guard类**:文件名包含`.guard.ts`的守卫类(游戏服务器安全重要)
-**Interceptor类**:文件名包含`.interceptor.ts`的拦截器类(日志监控重要)
-**Middleware类**:文件名包含`.middleware.ts`的中间件类(性能监控重要)
-**DTO类**:数据传输对象不需要测试文件
-**Interface文件**:接口定义不需要测试文件
-**Utils工具类**:简单工具函数不需要测试文件(复杂工具类需要)
- **测试代码检查严格要求**
- **一对一映射**:每个测试文件必须严格对应一个源文件,不允许一个测试文件测试多个源文件
- **测试范围限制**:测试内容必须严格限于对应源文件的功能测试,不允许跨文件测试
- **集成测试分离**所有集成测试、E2E测试、性能测试必须移动到顶层test/目录的对应子文件夹
- **测试文件命名**:测试文件名必须与源文件名完全对应(除.spec.ts后缀外
- **禁止混合测试**单元测试文件中不允许包含集成测试或E2E测试代码
- **顶层test目录结构**
- `test/integration/` - 所有集成测试文件
- `test/e2e/` - 所有端到端测试文件
- `test/performance/` - 所有性能测试文件
- `test/property/` - 所有属性测试文件(管理员模块)
- **实时通信测试**WebSocket Gateway必须有连接、断开、消息处理的完整测试
- **双模式测试**:内存服务和数据库服务都需要完整测试覆盖
- **属性测试应用**管理员模块使用fast-check进行属性测试放在test/property/目录
- **集成测试要求**复杂Service的集成测试放在test/integration/目录
- **E2E测试要求**关键业务流程的端到端测试放在test/e2e/目录
- **测试执行**:必须执行测试命令验证通过
### 步骤6功能文档生成
- **README结构**:模块概述、对外接口、内部依赖、核心特性、潜在风险
- **接口描述**:每个公共方法一句话功能说明
- **API接口列表**如果business模块开放了可访问的API在README中列出每个API并用一句话解释功能
- **WebSocket接口文档**Gateway模块需要详细的WebSocket事件文档
- **双模式说明**Core层模块需要说明数据库模式和内存模式的差异
- **依赖分析**:列出所有项目内部依赖及用途
- **特性识别**:技术特性、功能特性、质量特性
- **风险评估**:技术风险、业务风险、运维风险、安全风险
- **游戏服务器特殊文档**
- 实时通信协议说明
- 性能监控指标
- 双模式切换指南
- 属性测试策略说明
## 关键规则
### 命名规范
```typescript
// 文件命名(保持项目一致性)
user_service.ts, create_user_dto.ts, admin_operation_log_service.ts
user-service.ts, UserService.ts, adminOperationLog.service.ts
// 游戏服务器特殊文件类型
location_broadcast.gateway.ts, websocket_auth.guard.ts
users_memory.service.ts, file_redis.service.ts
admin.property.spec.ts, zulip_integration.e2e.spec.ts
// 变量命名
const userName = 'test';
const UserName = 'test';
// 常量命名
const MAX_RETRY_COUNT = 3;
const maxRetryCount = 3;
```
### 注释规范
```typescript
/**
* 文件功能描述
*
* 功能描述:
* - 功能点1
* - 功能点2
*
* 最近修改:
* - [用户日期]: 修改类型 - 修改内容 (修改者: [用户名称])
*
* @author [处理后的作者名称]
* @version x.x.x
* @since [创建日期]
* @lastModified [用户日期]
*/
```
**@author字段处理规则**
- **保留人名**:如果@author是人名,必须保留不变
- **替换AI标识**只有AI标识kiro、ChatGPT、Claude、AI等才可替换
- **示例**
- `@author kiro` → 可替换为 `@author [用户名称]`
- `@author 张三` → 必须保留为 `@author 张三`
### 架构分层
```typescript
// Core层 - 业务支撑模块使用_core后缀
@Injectable()
export class LocationBroadcastCoreService {
async broadcastPosition(data: PositionData): Promise<void> {
// 为位置广播业务提供技术支撑
}
}
// Core层 - 通用工具模块(不使用后缀)
@Injectable()
export class UserProfilesService {
async findByUserId(userId: bigint): Promise<UserProfile> {
// 通用的用户档案数据访问服务
}
}
// Business层 - 业务逻辑
@Injectable()
export class LocationBroadcastService {
constructor(
private readonly locationBroadcastCore: LocationBroadcastCoreService,
private readonly userProfiles: UserProfilesService
) {}
async updateUserLocation(userId: string, position: Position): Promise<void> {
// 业务逻辑验证、调用Core层、返回结果
}
}
```
**Core层命名判断标准**
- **业务支撑模块**:专门为某个业务功能提供技术支撑 → 使用`_core`后缀
- **通用工具模块**:提供可复用的数据访问或基础服务 → 不使用后缀
### 测试覆盖
```typescript
// 游戏服务器测试示例 - 严格一对一映射
describe('LocationBroadcastGateway', () => {
// 只测试LocationBroadcastGateway的功能不测试其他类
describe('handleConnection', () => {
it('should accept valid WebSocket connection', () => {}); // 正常情况
it('should reject unauthorized connection', () => {}); // 异常情况
it('should handle connection limit exceeded', () => {}); // 边界情况
});
describe('handlePositionUpdate', () => {
it('should broadcast position to room members', () => {}); // 实时通信测试
it('should validate position data format', () => {}); // 数据验证测试
});
});
// ❌ 错误:在单元测试中包含集成测试代码
describe('LocationBroadcastGateway', () => {
it('should integrate with database and redis', () => {}); // 应该移到test/integration/
});
// ✅ 正确集成测试放在顶层test目录
// 文件位置test/integration/location_broadcast_integration.spec.ts
describe('LocationBroadcast Integration', () => {
it('should integrate gateway with core service and database', () => {
// 测试多个模块间的集成
});
});
// ✅ 正确E2E测试放在顶层test目录
// 文件位置test/e2e/location_broadcast_e2e.spec.ts
describe('LocationBroadcast E2E', () => {
it('should handle complete user position update flow', () => {
// 端到端业务流程测试
});
});
// ✅ 正确属性测试放在顶层test目录
// 文件位置test/property/admin_property.spec.ts
describe('AdminService Properties', () => {
it('should handle any valid user status update',
fc.property(fc.integer(), fc.constantFrom(...Object.values(UserStatus)),
(userId, status) => {
// 属性测试逻辑
})
);
});
// ✅ 正确性能测试放在顶层test目录
// 文件位置test/performance/websocket_performance.spec.ts
describe('WebSocket Performance', () => {
it('should handle 1000 concurrent connections', () => {
// 性能测试逻辑
});
});
```
### API文档规范
**business模块如开放API接口README中必须包含**
```markdown
## 对外API接口
### POST /api/auth/login
用户登录接口,支持用户名/邮箱/手机号多种方式登录。
### GET /api/users/profile
获取当前登录用户的详细档案信息。
### PUT /api/users/:id/status
更新指定用户的状态(激活/禁用/待验证)。
## WebSocket事件接口
### 'position_update'
接收客户端位置更新,广播给房间内其他用户。
### 'join_room'
用户加入游戏房间,建立实时通信连接。
### 'chat_message'
处理聊天消息支持Zulip集成和消息过滤。
```
## 执行模板
每步完成后使用此模板报告:
```
## 步骤X[步骤名称]检查报告
### 🔍 检查结果
[发现的问题列表]
### 🛠️ 修正方案
[具体修正建议]
### ✅ 完成状态
- 检查项1 ✓/✗
- 检查项2 ✓/✗
**请确认修正方案,确认后进行下一步骤**
```
## 修改验证流程
修改后必须:
1. 重新执行该步骤检查
2. 提供验证报告
3. 确认问题是否解决
4. 等待用户确认
## 强制要求
- **用户信息**:开始前必须收集用户日期和名称
- **分步执行**:严禁一次执行多步骤
- **等待确认**:每步完成后必须等待用户确认
- **修改验证**:修改后必须重新检查验证
- **测试执行**步骤5必须执行实际测试命令
- **日期使用**:所有日期字段使用用户提供的真实日期
- **作者字段保护**@author字段中的人名不得修改只有AI标识才可替换
- **修改记录强制**:每次修改文件必须添加修改记录和更新@lastModified
- **API文档强制**business模块如开放API接口README中必须列出所有API并用一句话解释功能
- **测试代码严格要求**每个测试文件必须严格对应一个源文件集成测试等必须移动到顶层test/目录统一管理

View File

@@ -16,6 +16,37 @@
## 🔄 执行原则 ## 🔄 执行原则
### 🚨 中间步骤开始规范(重要)
**如果AI从任何中间步骤开始执行非步骤1开始必须首先完成以下准备工作**
#### 📋 强制信息收集
在执行任何中间步骤之前AI必须
1. **收集用户当前日期**:用于修改记录和时间戳更新
2. **收集用户名称**:用于@author字段处理和修改记录
3. **确认项目特性**识别这是NestJS游戏服务器项目的特点
#### 🔍 全局上下文获取
AI必须先了解
- **项目架构**:双模式架构(数据库+内存、分层结构Core+Business
- **技术栈**NestJS、WebSocket、Jest测试、fast-check属性测试
- **文件结构**:当前项目的整体文件组织方式
- **已有规范**:项目中已建立的命名、注释、测试等规范
#### 🎯 执行流程约束
```
中间步骤开始请求
🚨 强制收集用户信息(日期、名称)
🚨 强制识别项目特性和上下文
🚨 强制了解目标步骤的具体要求
开始执行指定步骤
```
**⚠️ 违规处理如果AI跳过信息收集直接执行中间步骤用户应要求AI重新开始并完成准备工作。**
### ⚠️ 强制要求 ### ⚠️ 强制要求
- **分步执行**:每次只执行一个步骤,严禁跳步骤或合并执行 - **分步执行**:每次只执行一个步骤,严禁跳步骤或合并执行
- **等待确认**:每步完成后必须等待用户确认才能进行下一步 - **等待确认**:每步完成后必须等待用户确认才能进行下一步
@@ -203,6 +234,32 @@
- **实际修改才更新**:只有真正修改了文件内容时才更新@lastModified字段和添加修改记录 - **实际修改才更新**:只有真正修改了文件内容时才更新@lastModified字段和添加修改记录
- **Git变更检测**:通过`git status`和`git diff`检查文件是否有实际变更只有git显示文件被修改时才需要添加修改记录和更新时间戳 - **Git变更检测**:通过`git status`和`git diff`检查文件是否有实际变更只有git显示文件被修改时才需要添加修改记录和更新时间戳
#### 🚨 重要强调:纯检查步骤不更新修改记录
**AI在执行代码检查步骤时如果发现代码已经符合规范无需任何修改**
- **禁止添加修改记录**:不要添加类似"AI代码检查步骤XXXX检查和优化"的记录
- **禁止更新时间戳**:不要更新@lastModified字段
- **禁止递增版本号**:不要修改@version字段
- **只有实际修改了代码内容、注释内容、结构等才需要更新修改记录**
**错误示例**
```typescript
// ❌ 错误:仅检查无修改却添加了修改记录
/**
* 最近修改:
* - 2026-01-12: 代码规范优化 - AI代码检查步骤2注释规范检查和优化 (修改者: moyin) // 这是错误的!
* - 2026-01-07: 功能新增 - 添加用户验证功能 (修改者: 张三)
*/
```
**正确示例**
```typescript
// ✅ 正确:检查发现符合规范,不添加修改记录
/**
* 最近修改:
* - 2026-01-07: 功能新增 - 添加用户验证功能 (修改者: 张三) // 保持原有记录不变
*/
```
### @author字段处理规范 ### @author字段处理规范
- **保留原则**:人名必须保留,不得随意修改 - **保留原则**:人名必须保留,不得随意修改
- **AI标识替换**只有AI标识kiro、ChatGPT、Claude、AI等才可替换为用户名称 - **AI标识替换**只有AI标识kiro、ChatGPT、Claude、AI等才可替换为用户名称

View File

@@ -1,5 +1,21 @@
# 步骤1命名规范检查 # 步骤1命名规范检查
## ⚠️ 执行前必读规范
**🔥 重要在执行本步骤之前AI必须先完整阅读同级目录下的 `README.md` 文件!**
该README文件包含
- 🎯 执行前准备和用户信息收集要求
- 🔄 强制执行原则和分步执行流程
- 🔥 修改后立即重新执行当前步骤的强制规则
- 📝 文件修改记录规范和版本号递增规则
- 🧪 测试文件调试规范和测试指令使用规范
- 🚨 全局约束和游戏服务器特殊要求
**不阅读README直接执行步骤将导致执行不规范违反项目要求**
---
## 🎯 检查目标 ## 🎯 检查目标
检查和修正所有命名规范问题,确保项目代码命名一致性。 检查和修正所有命名规范问题,确保项目代码命名一致性。
@@ -164,4 +180,11 @@ src/business/auth/
- ✅ 执行修改 → 🔥 立即重新执行步骤1 → 提供验证报告 → 等待用户确认 - ✅ 执行修改 → 🔥 立即重新执行步骤1 → 提供验证报告 → 等待用户确认
- ❌ 执行修改 → 直接进入步骤2错误做法 - ❌ 执行修改 → 直接进入步骤2错误做法
**🚨 重要强调:纯检查步骤不更新修改记录**
**如果检查发现命名已经符合规范,无需任何修改,则:**
-**禁止添加检查记录**:不要添加"AI代码检查步骤1命名规范检查和优化"
-**禁止更新时间戳**:不要修改@lastModified字段
-**禁止递增版本号**:不要修改@version字段
-**仅提供检查报告**:说明检查结果,确认符合规范
**不能跳过重新检查环节!** **不能跳过重新检查环节!**

View File

@@ -1,5 +1,21 @@
# 步骤2注释规范检查 # 步骤2注释规范检查
## ⚠️ 执行前必读规范
**🔥 重要在执行本步骤之前AI必须先完整阅读同级目录下的 `README.md` 文件!**
该README文件包含
- 🎯 执行前准备和用户信息收集要求
- 🔄 强制执行原则和分步执行流程
- 🔥 修改后立即重新执行当前步骤的强制规则
- 📝 文件修改记录规范和版本号递增规则
- 🧪 测试文件调试规范和测试指令使用规范
- 🚨 全局约束和游戏服务器特殊要求
**不阅读README直接执行步骤将导致执行不规范违反项目要求**
---
## 🎯 检查目标 ## 🎯 检查目标
检查和完善所有注释规范,确保文件头、类、方法注释的完整性和准确性。 检查和完善所有注释规范,确保文件头、类、方法注释的完整性和准确性。
@@ -151,12 +167,36 @@ async methodName(paramName: ParamType): Promise<ReturnType> {
- ✅ 实际修改文件内容时,才更新@lastModified字段 - ✅ 实际修改文件内容时,才更新@lastModified字段
- ✅ 使用Git变更检测确认文件是否真正被修改 - ✅ 使用Git变更检测确认文件是否真正被修改
### 🚨 重要强调:纯检查不更新修改记录
**步骤2注释规范检查时如果发现注释已经符合规范无需任何修改**
#### 禁止的操作
-**禁止添加检查记录**:不要添加"AI代码检查步骤2注释规范检查和优化"
-**禁止更新时间戳**:不要修改@lastModified字段
-**禁止递增版本号**:不要修改@version字段
-**禁止修改任何现有内容**:包括修改记录、作者信息等
#### 正确的做法
-**仅进行检查**:验证注释规范是否符合要求
-**提供检查报告**:说明检查结果和符合情况
-**保持文件不变**:如果符合规范就不修改任何内容
### 实际修改才更新的情况
**只有在以下情况下才需要更新修改记录:**
- 添加了缺失的文件头注释
- 补充了不完整的类注释
- 完善了缺失的方法注释
- 修正了错误的@author字段AI标识替换为用户名
- 修复了格式错误的注释结构
### Git变更检测检查 ### Git变更检测检查
```bash ```bash
git status # 检查是否有文件被修改 git status # 检查是否有文件被修改
git diff [filename] # 检查具体修改内容 git diff [filename] # 检查具体修改内容
``` ```
**只有git显示文件被修改时才需要添加修改记录和更新时间戳**
**注意具体的时间更新规则请参考README.md中的全局约束部分** **注意具体的时间更新规则请参考README.md中的全局约束部分**
## 🎮 游戏服务器特殊注释要求 ## 🎮 游戏服务器特殊注释要求

View File

@@ -1,5 +1,21 @@
# 步骤3代码质量检查 # 步骤3代码质量检查
## ⚠️ 执行前必读规范
**🔥 重要在执行本步骤之前AI必须先完整阅读同级目录下的 `README.md` 文件!**
该README文件包含
- 🎯 执行前准备和用户信息收集要求
- 🔄 强制执行原则和分步执行流程
- 🔥 修改后立即重新执行当前步骤的强制规则
- 📝 文件修改记录规范和版本号递增规则
- 🧪 测试文件调试规范和测试指令使用规范
- 🚨 全局约束和游戏服务器特殊要求
**不阅读README直接执行步骤将导致执行不规范违反项目要求**
---
## 🎯 检查目标 ## 🎯 检查目标
清理和优化代码质量消除未使用代码、规范常量定义、处理TODO项。 清理和优化代码质量消除未使用代码、规范常量定义、处理TODO项。
@@ -324,4 +340,11 @@ describe('AdminService Properties', () => {
- ✅ 执行修改 → 🔥 立即重新执行步骤3 → 提供验证报告 → 等待用户确认 - ✅ 执行修改 → 🔥 立即重新执行步骤3 → 提供验证报告 → 等待用户确认
- ❌ 执行修改 → 直接进入步骤4错误做法 - ❌ 执行修改 → 直接进入步骤4错误做法
**🚨 重要强调:纯检查步骤不更新修改记录**
**如果检查发现代码质量已经符合规范,无需任何修改,则:**
-**禁止添加检查记录**:不要添加"AI代码检查步骤3代码质量检查和优化"
-**禁止更新时间戳**:不要修改@lastModified字段
-**禁止递增版本号**:不要修改@version字段
-**仅提供检查报告**:说明检查结果,确认符合规范
**不能跳过重新检查环节!** **不能跳过重新检查环节!**

View File

@@ -1,5 +1,21 @@
# 步骤4架构分层检查 # 步骤4架构分层检查
## ⚠️ 执行前必读规范
**🔥 重要在执行本步骤之前AI必须先完整阅读同级目录下的 `README.md` 文件!**
该README文件包含
- 🎯 执行前准备和用户信息收集要求
- 🔄 强制执行原则和分步执行流程
- 🔥 修改后立即重新执行当前步骤的强制规则
- 📝 文件修改记录规范和版本号递增规则
- 🧪 测试文件调试规范和测试指令使用规范
- 🚨 全局约束和游戏服务器特殊要求
**不阅读README直接执行步骤将导致执行不规范违反项目要求**
---
## 🎯 检查目标 ## 🎯 检查目标
检查架构分层的合规性确保Core层和Business层职责清晰、依赖关系正确。 检查架构分层的合规性确保Core层和Business层职责清晰、依赖关系正确。
@@ -307,4 +323,11 @@ export class UsersBusinessService {
- ✅ 执行修改 → 🔥 立即重新执行步骤4 → 提供验证报告 → 等待用户确认 - ✅ 执行修改 → 🔥 立即重新执行步骤4 → 提供验证报告 → 等待用户确认
- ❌ 执行修改 → 直接进入步骤5错误做法 - ❌ 执行修改 → 直接进入步骤5错误做法
**🚨 重要强调:纯检查步骤不更新修改记录**
**如果检查发现架构分层已经符合规范,无需任何修改,则:**
-**禁止添加检查记录**:不要添加"AI代码检查步骤4架构分层检查和优化"
-**禁止更新时间戳**:不要修改@lastModified字段
-**禁止递增版本号**:不要修改@version字段
-**仅提供检查报告**:说明检查结果,确认符合规范
**不能跳过重新检查环节!** **不能跳过重新检查环节!**

View File

@@ -1,5 +1,21 @@
# 步骤5测试覆盖检查 # 步骤5测试覆盖检查
## ⚠️ 执行前必读规范
**🔥 重要在执行本步骤之前AI必须先完整阅读同级目录下的 `README.md` 文件!**
该README文件包含
- 🎯 执行前准备和用户信息收集要求
- 🔄 强制执行原则和分步执行流程
- 🔥 修改后立即重新执行当前步骤的强制规则
- 📝 文件修改记录规范和版本号递增规则
- 🧪 测试文件调试规范和测试指令使用规范
- 🚨 全局约束和游戏服务器特殊要求
**不阅读README直接执行步骤将导致执行不规范违反项目要求**
---
## 🎯 检查目标 ## 🎯 检查目标
检查测试文件的完整性和覆盖率,确保严格的一对一测试映射和测试分离。 检查测试文件的完整性和覆盖率,确保严格的一对一测试映射和测试分离。
@@ -297,6 +313,77 @@ npm run test:property
# 性能测试统一在test/performance/目录执行) # 性能测试统一在test/performance/目录执行)
npm run test:performance npm run test:performance
# 等价于: jest test/performance/ # 等价于: jest test/performance/
# 🔥 特定文件或目录测试步骤5专用指令
pnpm test (文件夹或者文件的相对地址)
# 示例:
pnpm test src/core/zulip_core # 测试整个zulip_core模块
pnpm test src/core/zulip_core/services # 测试services目录
pnpm test src/core/zulip_core/services/config_manager.service.spec.ts # 测试单个文件
pnpm test test/integration/zulip_integration.spec.ts # 测试集成测试文件
```
### 🔥 强制测试执行要求(重要)
**步骤5完成前必须确保所有检查范围内的测试通过**
#### 测试执行验证流程
1. **识别检查范围**:确定当前检查涉及的所有模块和文件
2. **执行范围内测试**:运行所有相关的单元测试、集成测试
3. **修复测试失败**:解决所有测试失败问题(类型错误、逻辑错误等)
4. **验证测试通过**:确保所有测试都能成功执行
5. **提供测试报告**:展示测试执行结果和覆盖率
#### 测试失败处理原则
```bash
# 🔥 如果发现测试失败必须修复后才能完成步骤5
# 1. 运行特定模块测试推荐使用pnpm test指令
pnpm test src/core/zulip_core # 测试整个模块
pnpm test src/core/zulip_core/services # 测试services目录
pnpm test src/core/zulip_core/services/config_manager.service.spec.ts # 测试单个文件
# 2. 分析失败原因
# - 类型错误修正TypeScript类型定义
# - 接口不匹配更新接口或Mock对象
# - 逻辑错误:修正业务逻辑实现
# - 依赖问题更新依赖注入或Mock配置
# 3. 修复后重新运行测试
pnpm test src/core/zulip_core # 重新测试修复后的模块
# 4. 确保所有测试通过后才完成步骤5
```
#### 测试执行成功标准
-**零失败测试**所有相关测试必须通过0 failed
-**零错误测试**所有测试套件必须成功运行0 error
-**完整覆盖**:所有检查范围内的文件都有测试执行
-**类型安全**无TypeScript编译错误
-**依赖正确**所有Mock和依赖注入正确配置
#### 测试执行报告模板
```
## 测试执行验证报告
### 🧪 测试执行结果
- 执行命令pnpm test src/core/zulip_core
- 测试套件X passed, 0 failed
- 测试用例X passed, 0 failed
- 覆盖率X% statements, X% branches, X% functions, X% lines
### 🔧 修复的问题
- 类型错误修复:[具体修复内容]
- 接口更新:[具体更新内容]
- Mock配置[具体配置内容]
### ✅ 验证状态
- 所有测试通过 ✓
- 无编译错误 ✓
- 依赖注入正确 ✓
- Mock配置完整 ✓
**测试执行验证完成,可以进行下一步骤**
``` ```
### 测试执行顺序 ### 测试执行顺序
@@ -305,6 +392,13 @@ npm run test:performance
3. **第三阶段**E2E测试业务流程 3. **第三阶段**E2E测试业务流程
4. **第四阶段**:性能测试(系统性能) 4. **第四阶段**:性能测试(系统性能)
### 🚨 测试执行失败处理
如果在测试执行过程中发现失败,必须:
1. **立即停止步骤5进程**
2. **分析并修复所有测试失败**
3. **重新执行完整的步骤5检查**
4. **确保所有测试通过后才能进入步骤6**
## 🔍 检查执行步骤 ## 🔍 检查执行步骤
1. **扫描需要测试的文件类型** 1. **扫描需要测试的文件类型**
@@ -329,21 +423,284 @@ npm run test:performance
- 将性能测试移动到test/performance/ - 将性能测试移动到test/performance/
- 将属性测试移动到test/property/ - 将属性测试移动到test/property/
6. **执行测试验证** 6. **游戏服务器特殊检查**
- 运行单元测试命令验证通过
- 确保测试覆盖率达标
- 验证测试质量和有效性
7. **游戏服务器特殊检查**
- WebSocket Gateway的完整测试覆盖 - WebSocket Gateway的完整测试覆盖
- 双模式服务的一致性测试 - 双模式服务的一致性测试
- 属性测试的正确实现 - 属性测试的正确实现
7. **🔥 强制执行测试验证(关键步骤)**
- 运行检查范围内的所有相关测试
- 修复所有测试失败问题
- 确保测试覆盖率达标
- 验证测试质量和有效性
- **只有所有测试通过才能完成步骤5**
## 🔥 重要提醒 ## 🔥 重要提醒
**如果在本步骤中执行了任何修改操作创建测试文件、移动测试文件、修正测试内容等必须立即重新执行步骤5的完整检查** **如果在本步骤中执行了任何修改操作(创建测试文件、移动测试文件、修正测试内容、修复测试失败必须立即重新执行步骤5的完整检查**
- ✅ 执行修改 → 🔥 立即重新执行步骤5 → 提供验证报告 → 等待用户确认 - ✅ 执行修改 → 🔥 立即重新执行步骤5 → 🧪 强制执行测试验证 → 提供验证报告 → 等待用户确认
- ❌ 执行修改 → 直接进入步骤6错误做法 - ❌ 执行修改 → 直接进入步骤6错误做法
**不能跳过重新检查环节!** **🚨 重要强调:纯检查步骤不更新修改记录**
**如果检查发现测试覆盖已经符合规范,无需任何修改,则:**
-**禁止添加检查记录**:不要添加"AI代码检查步骤5测试覆盖检查和优化"
-**禁止更新时间戳**:不要修改@lastModified字段
-**禁止递增版本号**:不要修改@version字段
-**仅提供检查报告**:说明检查结果,确认符合规范
**🚨 步骤5完成的强制条件**
1. **测试文件完整性检查通过**
2. **测试映射关系检查通过**
3. **测试分离架构检查通过**
4. **🔥 所有检查范围内的测试必须执行成功(零失败)**
**不能跳过测试执行验证环节如果测试失败必须修复后重新执行整个步骤5**
---
## ✅ zulip_core模块步骤5检查完成报告
### 📋 检查范围
- **模块**src/core/zulip_core
- **检查日期**2026-01-12
- **检查人员**moyin
### 🧪 测试执行验证结果
#### 执行命令
```bash
npx jest src/core/zulip_core --testTimeout=15000
```
#### 测试结果统计
- **测试套件**11 passed, 0 failed
- **测试用例**367 passed, 0 failed
- **执行时间**11.841s
- **覆盖状态**:✅ 完整覆盖
#### 修复的关键问题
1. **DynamicConfigManagerService测试失败修复**
- 修正了Zulip凭据初始化顺序问题
- 修复了Mock配置的fs.existsSync行为
- 解决了环境变量设置时机问题
- 修正了测试用例的预期错误消息
2. **测试文件完整性验证**
- 确认所有service文件都有对应的.spec.ts测试文件
- 验证了严格的一对一测试映射关系
- 检查了测试文件位置的正确性
### 📊 测试覆盖详情
#### 通过的测试套件
1. ✅ api_key_security.service.spec.ts (53 tests)
2. ✅ config_manager.service.spec.ts (45 tests)
3. ✅ dynamic_config_manager.service.spec.ts (32 tests)
4. ✅ monitoring.service.spec.ts (15 tests)
5. ✅ stream_initializer.service.spec.ts (11 tests)
6. ✅ user_management.service.spec.ts (16 tests)
7. ✅ user_registration.service.spec.ts (9 tests)
8. ✅ zulip_account.service.spec.ts (26 tests)
9. ✅ zulip_client.service.spec.ts (19 tests)
10. ✅ zulip_client_pool.service.spec.ts (23 tests)
11. ✅ zulip_core.module.spec.ts (118 tests)
#### 测试质量验证
- **单元测试隔离**:✅ 所有测试使用Mock隔离外部依赖
- **测试范围限制**:✅ 每个测试文件严格测试对应的单个服务
- **错误处理覆盖**:✅ 包含完整的异常情况测试
- **边界条件测试**:✅ 覆盖各种边界和异常场景
### 🔧 修改记录
#### 文件修改详情
- **修改文件**src/core/zulip_core/services/dynamic_config_manager.service.spec.ts
- **修改时间**2026-01-12
- **修改人员**moyin
- **修改内容**
- 修正了beforeEach中环境变量设置顺序
- 修复了无凭据测试的服务实例创建
- 修正了fs.existsSync的Mock行为
- 更新了错误消息的预期值
### ✅ 验证状态确认
- **测试文件完整性**:✅ 通过
- **一对一测试映射**:✅ 通过
- **测试分离架构**:✅ 通过
- **测试执行验证**:✅ 通过0失败367通过
- **类型安全检查**:✅ 通过
- **依赖注入配置**:✅ 通过
### 🎯 步骤5完成确认
**zulip_core模块的步骤5测试覆盖检查已完成所有强制条件均已满足**
1. ✅ 测试文件完整性检查通过
2. ✅ 测试映射关系检查通过
3. ✅ 测试分离架构检查通过
4. ✅ 所有测试执行成功(零失败)
**可以进入下一步骤的开发工作。**
---
## ✅ Zulip模块完整步骤5检查完成报告
### 📋 检查范围
- **模块**Zulip相关所有模块
- src/core/zulip_core (12个源文件)
- src/core/db/zulip_accounts (5个源文件)
- src/business/zulip (13个源文件)
- **检查日期**2026-01-12
- **检查人员**moyin
### 🧪 测试执行验证结果
#### 最终测试状态
- **总测试套件**30个
- **通过测试套件**30个 ✅
- **失败测试套件**0个 ✅
- **总测试用例**907个
- **通过测试用例**907个 ✅
- **失败测试用例**0个 ✅
#### 执行的测试命令
```bash
# 核心模块测试
pnpm test src/core/zulip_core
# 结果12个测试套件通过394个测试通过
# 数据库模块测试
pnpm test src/core/db/zulip_accounts
# 结果5个测试套件通过156个测试通过
# 业务模块测试
pnpm test src/business/zulip
# 结果13个测试套件通过357个测试通过
```
### 🔧 修复的测试问题
#### 1. chat.controller.spec.ts
- **问题**错误处理测试期望HttpException但收到Error
- **修复**修改mock实现抛出HttpException而不是Error
- **状态**:✅ 已修复
- **修改记录**:已更新文件头部修改记录
#### 2. zulip.service.spec.ts
- **问题**消息内容断言失败实际内容包含额外的游戏消息ID
- **修复**使用expect.stringContaining()匹配包含原始内容的字符串
- **状态**:✅ 已修复
- **修改记录**:已更新文件头部修改记录
#### 3. zulip_accounts.controller.spec.ts
- **问题**:日志记录测试中多次调用的参数期望不匹配
- **修复**使用toHaveBeenNthCalledWith()精确匹配特定调用的参数
- **状态**:✅ 已修复
- **修改记录**:已更新文件头部修改记录
### 📊 测试覆盖详情
#### 核心模块 (src/core/zulip_core)
**完整覆盖** - 所有12个源文件都有对应的测试文件
- api_key_security.service.spec.ts
- config_manager.service.spec.ts
- dynamic_config_manager.service.spec.ts
- monitoring.service.spec.ts
- stream_initializer.service.spec.ts
- user_management.service.spec.ts
- user_registration.service.spec.ts
- zulip_account.service.spec.ts
- zulip_client.service.spec.ts
- zulip_client_pool.service.spec.ts
- zulip_core.module.spec.ts
- zulip_event_queue.service.spec.ts
#### 数据库模块 (src/core/db/zulip_accounts)
**完整覆盖** - 所有5个源文件都有对应的测试文件
- zulip_accounts.repository.spec.ts
- zulip_accounts_memory.repository.spec.ts
- zulip_accounts.entity.spec.ts
- zulip_accounts.module.spec.ts
- zulip_accounts.service.spec.ts
#### 业务模块 (src/business/zulip)
**完整覆盖** - 所有13个源文件都有对应的测试文件
- chat.controller.spec.ts
- clean_websocket.gateway.spec.ts
- dynamic_config.controller.spec.ts
- websocket_docs.controller.spec.ts
- websocket_openapi.controller.spec.ts
- websocket_test.controller.spec.ts
- zulip.service.spec.ts
- zulip_accounts.controller.spec.ts
- services/message_filter.service.spec.ts
- services/session_cleanup.service.spec.ts
- services/session_manager.service.spec.ts
- services/zulip_accounts_business.service.spec.ts
- services/zulip_event_processor.service.spec.ts
### 🎯 测试质量验证
#### 功能覆盖率
- **登录流程**: ✅ 完整覆盖(包括属性测试)
- **消息发送**: ✅ 完整覆盖(包括属性测试)
- **位置更新**: ✅ 完整覆盖(包括属性测试)
- **会话管理**: ✅ 完整覆盖
- **配置管理**: ✅ 完整覆盖
- **错误处理**: ✅ 完整覆盖
- **WebSocket集成**: ✅ 完整覆盖
- **数据库操作**: ✅ 完整覆盖
#### 属性测试覆盖
- **Property 1**: 玩家登录流程完整性 ✅
- **Property 3**: 消息发送流程完整性 ✅
- **Property 6**: 位置更新和上下文注入 ✅
- **Property 7**: 内容安全和频率控制 ✅
#### 测试架构验证
- **单元测试隔离**: ✅ 所有测试使用Mock隔离外部依赖
- **一对一测试映射**: ✅ 每个测试文件严格对应一个源文件
- **测试范围限制**: ✅ 测试内容严格限于对应源文件功能
- **错误处理覆盖**: ✅ 包含完整的异常情况测试
- **边界条件测试**: ✅ 覆盖各种边界和异常场景
### 🔧 修改文件记录
#### 修改的测试文件
1. **src/business/zulip/chat.controller.spec.ts**
- 修改时间2026-01-12
- 修改人员moyin
- 修改内容:修复错误处理测试中的异常类型期望
2. **src/business/zulip/zulip.service.spec.ts**
- 修改时间2026-01-12
- 修改人员moyin
- 修改内容修复消息内容断言使用stringContaining匹配
3. **src/business/zulip/zulip_accounts.controller.spec.ts**
- 修改时间2026-01-12
- 修改人员moyin
- 修改内容:修复日志记录测试的参数期望
### ✅ 最终验证状态确认
- **测试文件完整性**:✅ 通过30/30文件有测试
- **一对一测试映射**:✅ 通过(严格对应关系)
- **测试分离架构**:✅ 通过(单元测试在源文件同目录)
- **测试执行验证**:✅ 通过907个测试全部通过0失败
- **类型安全检查**:✅ 通过无TypeScript编译错误
- **依赖注入配置**:✅ 通过Mock配置正确
### 🎯 步骤5完成确认
**Zulip模块的步骤5测试覆盖检查已完成所有强制条件均已满足**
1. ✅ 测试文件完整性检查通过100%覆盖率)
2. ✅ 测试映射关系检查通过(严格一对一映射)
3. ✅ 测试分离架构检查通过(单元测试正确位置)
4. ✅ 所有测试执行成功907个测试通过0失败
**🎉 Zulip模块具备完整的测试覆盖率和高质量的测试代码可以进入下一步骤的开发工作。**

View File

@@ -1,5 +1,21 @@
# 步骤6功能文档生成 # 步骤6功能文档生成
## ⚠️ 执行前必读规范
**🔥 重要在执行本步骤之前AI必须先完整阅读同级目录下的 `README.md` 文件!**
该README文件包含
- 🎯 执行前准备和用户信息收集要求
- 🔄 强制执行原则和分步执行流程
- 🔥 修改后立即重新执行当前步骤的强制规则
- 📝 文件修改记录规范和版本号递增规则
- 🧪 测试文件调试规范和测试指令使用规范
- 🚨 全局约束和游戏服务器特殊要求
**不阅读README直接执行步骤将导致执行不规范违反项目要求**
---
## 🎯 检查目标 ## 🎯 检查目标
生成和维护功能模块的README文档确保文档内容完整、准确、实用。 生成和维护功能模块的README文档确保文档内容完整、准确、实用。
@@ -324,4 +340,11 @@ Gateway模块需要详细的WebSocket事件文档
- ✅ 执行修改 → 🔥 立即重新执行步骤6 → 提供验证报告 → 等待用户确认 - ✅ 执行修改 → 🔥 立即重新执行步骤6 → 提供验证报告 → 等待用户确认
- ❌ 执行修改 → 直接结束检查(错误做法) - ❌ 执行修改 → 直接结束检查(错误做法)
**🚨 重要强调:纯检查步骤不更新修改记录**
**如果检查发现功能文档已经符合规范,无需任何修改,则:**
-**禁止添加检查记录**:不要添加"AI代码检查步骤6功能文档检查和优化"
-**禁止更新时间戳**:不要修改@lastModified字段
-**禁止递增版本号**:不要修改@version字段
-**仅提供检查报告**:说明检查结果,确认符合规范
**不能跳过重新检查环节!** **不能跳过重新检查环节!**

View File

@@ -1,5 +1,21 @@
# 步骤7代码提交 # 步骤7代码提交
## ⚠️ 执行前必读规范
**🔥 重要在执行本步骤之前AI必须先完整阅读同级目录下的 `README.md` 文件!**
该README文件包含
- 🎯 执行前准备和用户信息收集要求
- 🔄 强制执行原则和分步执行流程
- 🔥 修改后立即重新执行当前步骤的强制规则
- 📝 文件修改记录规范和版本号递增规则
- 🧪 测试文件调试规范和测试指令使用规范
- 🚨 全局约束和游戏服务器特殊要求
**不阅读README直接执行步骤将导致执行不规范违反项目要求**
---
## 🎯 检查目标 ## 🎯 检查目标
完成代码修改后的规范化提交流程,确保代码变更记录清晰、分支管理规范、提交信息符合项目标准。 完成代码修改后的规范化提交流程,确保代码变更记录清晰、分支管理规范、提交信息符合项目标准。
@@ -8,6 +24,37 @@
- 所有修改的文件已更新修改记录和版本信息 - 所有修改的文件已更新修改记录和版本信息
- 代码能够正常运行且通过测试 - 代码能够正常运行且通过测试
## 🚨 协作规范和范围控制
### 绝对禁止的操作
**以下操作严格禁止违反将影响其他AI的工作**
1. **禁止暂存范围外代码**
```bash
# ❌ 绝对禁止
git stash push [范围外文件]
git stash push -m "消息" [范围外文件]
```
2. **禁止重置范围外代码**
```bash
# ❌ 绝对禁止
git reset HEAD [范围外文件]
git checkout -- [范围外文件]
```
3. **禁止移动或隐藏范围外代码**
```bash
# ❌ 绝对禁止
git mv [范围外文件] [其他位置]
git rm [范围外文件]
```
### 协作原则
- **范围外代码必须保持原状**其他AI需要处理这些代码
- **只处理自己的范围**:严格按照检查任务的文件夹范围执行
- **不影响其他工作流**任何操作都不能影响其他AI的检查任务
## 🔍 Git变更检查与校验 ## 🔍 Git变更检查与校验
### 1. 检查Git状态和变更内容 ### 1. 检查Git状态和变更内容
@@ -59,47 +106,113 @@ git diff --cached
## 🌿 分支管理规范 ## 🌿 分支管理规范
### 🔥 重要原则:严格范围限制
**🚨 绝对禁止:不得暂存、提交或以任何方式处理检查范围外的代码!**
- ✅ **正确做法**:只提交当前检查任务涉及的文件和文件夹
- ❌ **严格禁止**:提交其他模块、其他开发者负责的文件
- ❌ **严格禁止**使用git stash暂存其他范围的代码
- ❌ **严格禁止**:以任何方式移动、隐藏或处理范围外的代码
- ⚠️ **检查要求**:提交前必须确认所有变更文件都在当前检查范围内
- 🔥 **协作原则**其他范围的代码必须保持原状供其他AI处理
### 分支命名规范 ### 分支命名规范
根据修改类型创建对应的分支: 根据修改类型和检查范围创建对应的分支:
```bash ```bash
# 代码规范优化分支 # 代码规范优化分支(指定检查范围)
feature/code-standard-optimization-[日期] feature/code-standard-[模块名称]-[日期]
# 示例feature/code-standard-optimization-20240112 # 示例feature/code-standard-auth-20240112
# 示例feature/code-standard-zulip-20240112
# Bug修复分支 # Bug修复分支(指定模块)
fix/[具体问题描述] fix/[模块名称]-[具体问题描述]
# 示例fix/room-concurrency-issue # 示例fix/auth-login-validation-issue
# 示例fix/zulip-message-handling-bug
# 功能新增分支 # 功能新增分支(指定模块)
feature/[功能名称] feature/[模块名称]-[功能名称]
# 示例feature/user-authentication # 示例feature/auth-multi-factor-authentication
# 示例feature/zulip-message-encryption
# 重构分支 # 重构分支(指定模块)
refactor/[模块名称] refactor/[模块名称]-[重构内容]
# 示例refactor/room-management # 示例refactor/auth-service-architecture
# 示例refactor/zulip-websocket-handler
# 性能优化分支 # 性能优化分支(指定模块)
perf/[优化内容] perf/[模块名称]-[优化内容]
# 示例perf/database-query-optimization # 示例perf/auth-token-validation
# 示例perf/zulip-message-processing
# 文档更新分支 # 文档更新分支(指定范围)
docs/[文档类型] docs/[模块名称]-[文档类型]
# 示例docs/api-documentation # 示例docs/auth-api-documentation
# 示例docs/zulip-integration-guide
``` ```
### 创建和切换分支 ### 创建和切换分支
```bash ```bash
# 确保在主分支 # 🔥 重要:在当前分支基础上创建新分支(不切换到主分支
git checkout main # 查看当前分支状态
# 或者 git status
git checkout develop git branch
# 拉取最新代码 # 直接在当前分支基础上创建并切换到新分支(包含检查范围标识)
git pull origin main git checkout -b feature/code-standard-[模块名称]-[日期]
# 创建并切换到新分支 # 示例如果当前检查auth模块
git checkout -b feature/code-standard-optimization-20240112 git checkout -b feature/code-standard-auth-20240112
# 示例如果当前检查zulip模块
git checkout -b feature/code-standard-zulip-20240112
```
### 🔍 提交前范围检查
在执行任何git操作前必须进行范围检查
```bash
# 1. 查看当前变更的文件
git status
# 2. 检查变更文件是否都在检查范围内
git diff --name-only
# 3. 🚨 重要:如果发现范围外的文件,绝对不能暂存或提交!
# 正确做法:只添加范围内的文件,忽略范围外的文件
git add [范围内的具体文件路径]
# 4. ❌ 错误做法:不要使用以下命令处理范围外文件
# git stash push [范围外文件] # 禁止会影响其他AI
# git reset HEAD [范围外文件] # 禁止会影响其他AI
# git add -i # 谨慎使用,容易误选范围外文件
```
### 📂 检查范围示例
#### 正确的范围控制
```bash
# 如果检查任务是 "auth 模块代码规范优化"
# ✅ 应该包含的文件:
src/business/auth/
src/core/auth/
test/business/auth/
test/core/auth/
docs/auth/
# ❌ 不应该包含的文件:
src/business/zulip/ # 其他模块
src/business/user-mgmt/ # 其他模块
client/ # 前端代码
config/ # 配置文件(除非明确要求)
```
#### 范围检查命令
```bash
# 检查当前变更是否超出范围
git diff --name-only | grep -v "^src/business/auth/" | grep -v "^test/.*auth" | grep -v "^docs/.*auth"
# 如果上述命令有输出,说明存在范围外的文件,需要排除
``` ```
## 📝 提交信息规范 ## 📝 提交信息规范
@@ -125,8 +238,9 @@ git checkout -b feature/code-standard-optimization-20240112
### 提交信息格式 ### 提交信息格式
```bash ```bash
<类型><简短描述> <类型>(<范围>)<简短描述>
范围:<具体的文件/文件夹范围>
[可选的详细描述] [可选的详细描述]
[可选的关联信息] [可选的关联信息]
@@ -134,34 +248,38 @@ git checkout -b feature/code-standard-optimization-20240112
### 提交信息示例 ### 提交信息示例
#### 单一类型修改 #### 单一类型修改(明确范围)
```bash ```bash
# 代码规范优化 # 代码规范优化
git commit -m "style统一命名规范和注释格式 git commit -m "style(auth):统一命名规范和注释格式
范围src/business/auth/, src/core/auth/
- 调整文件和变量命名符合项目规范 - 调整文件和变量命名符合项目规范
- 优化注释格式和内容完整性 - 优化注释格式和内容完整性
- 清理代码格式和缩进问题" - 清理代码格式和缩进问题"
# Bug修复 # Bug修复
git commit -m "fix:修复用户注册时的邮箱验证问题 git commit -m "fix(zulip):修复消息处理时的并发问题
- 修复邮箱格式验证逻辑错误 范围src/business/zulip/services/
- 添加重复邮箱检查机制 - 修复消息队列处理逻辑错误
- 添加并发控制机制
- 优化错误提示信息" - 优化错误提示信息"
# 功能新增 # 功能新增
git commit -m "feat:实现用户权限管理系统 git commit -m "feat(auth):实现多因素认证系统
- 添加角色和权限定义 范围src/business/auth/, src/core/auth/
- 实现权限验证中间件 - 添加TOTP验证支持
- 支持动态权限分配" - 实现短信验证功能
- 支持备用验证码"
``` ```
#### 多文件相关修改 #### 多文件相关修改(明确范围)
```bash ```bash
git commit -m "refactor重构用户管理模块架构 git commit -m "refactor(user-mgmt):重构用户管理模块架构
范围src/business/user-mgmt/, src/core/db/users/
涉及文件: 涉及文件:
- src/business/user-mgmt/user.service.ts - src/business/user-mgmt/user.service.ts
- src/business/user-mgmt/user.controller.ts - src/business/user-mgmt/user.controller.ts
@@ -175,61 +293,118 @@ git commit -m "refactor重构用户管理模块架构
## 🔄 提交执行流程 ## 🔄 提交执行流程
### 1. 分阶段提交(推荐) ### 🔥 范围控制原则
**🚨 在执行任何提交操作前,必须确保所有变更文件都在当前检查任务的范围内!**
**🚨 绝对禁止暂存、重置或以任何方式处理范围外的代码!**
### 1. 范围检查与文件筛选
```bash
# 第一步:查看所有变更文件
git status
git diff --name-only
# 第二步:识别范围内和范围外的文件
# 假设当前检查任务是 "auth 模块优化"
# 范围内文件示例:
# - src/business/auth/
# - src/core/auth/
# - test/business/auth/
# - test/core/auth/
# - docs/auth/
# 第三步:🚨 重要 - 只添加范围内的文件,绝对不处理范围外文件
git add src/business/auth/
git add src/core/auth/
git add test/business/auth/
git add test/core/auth/
git add docs/auth/
# ❌ 禁止使用交互式添加(容易误选范围外文件)
# git add -i # 不推荐,风险太高
```
### 2. 分阶段提交(推荐)
将不同类型的修改分别提交,保持提交历史清晰: 将不同类型的修改分别提交,保持提交历史清晰:
```bash ```bash
# 第一步:提交代码规范优化 # 第一步:提交代码规范优化(仅限检查范围内)
git add src/business/auth/ git add src/business/auth/ src/core/auth/
git commit -m "style优化auth模块代码规范 git commit -m "style(auth)优化auth模块代码规范
范围src/business/auth/, src/core/auth/
- 统一命名规范和注释格式 - 统一命名规范和注释格式
- 清理未使用的导入 - 清理未使用的导入
- 调整代码结构和缩进" - 调整代码结构和缩进"
# 第二步:提交功能改进(如果有) # 第二步:提交功能改进(如果有,仅限范围内
git add src/business/user-mgmt/ git add src/business/auth/enhanced-features/
git commit -m "feat添加用户状态管理功能 git commit -m "feat(auth):添加用户状态管理功能
范围src/business/auth/
- 实现用户激活/禁用功能 - 实现用户激活/禁用功能
- 添加状态变更日志记录 - 添加状态变更日志记录
- 支持批量状态操作" - 支持批量状态操作"
# 第三步:提交测试相关(如果有) # 第三步:提交测试相关(仅限范围内)
git add test/ git add test/business/auth/ test/core/auth/
git commit -m "test:完善用户管理模块测试覆盖 git commit -m "test(auth)完善auth模块测试覆盖
范围test/business/auth/, test/core/auth/
- 添加缺失的单元测试 - 添加缺失的单元测试
- 补充集成测试用例 - 补充集成测试用例
- 提升测试覆盖率到95%以上" - 提升测试覆盖率到95%以上"
# 第四步:提交文档更新(如果有) # 第四步:提交文档更新(仅限范围内)
git add docs/ src/**/README.md git add docs/auth/ src/business/auth/README.md src/core/auth/README.md
git commit -m "docs:更新用户管理模块文档 git commit -m "docs(auth)更新auth模块文档
范围docs/auth/, auth模块README文件
- 完善API接口文档 - 完善API接口文档
- 更新功能模块README - 更新功能模块README
- 添加使用示例和注意事项" - 添加使用示例和注意事项"
``` ```
### 2. 使用交互式暂存(精确控制) ### 3. 使用交互式暂存(精确控制)
```bash ```bash
# 交互式选择要提交的代码块 # 交互式选择要提交的代码块(仅限范围内文件)
git add -p src/business/auth/login.service.ts git add -p src/business/auth/login.service.ts
# 选择代码规范相关的修改 # 选择代码规范相关的修改
# 提交第一部分 # 提交第一部分
git commit -m "style优化login.service代码规范" git commit -m "style(auth)优化login.service代码规范"
# 暂存剩余的功能修改 # 暂存剩余的功能修改
git add src/business/auth/login.service.ts git add src/business/auth/login.service.ts
git commit -m "feat添加多因素认证支持" git commit -m "feat(auth):添加多因素认证支持"
``` ```
### 3. 提交前最终检查 ### 4. 范围外文件处理
🚨 **重要:绝对不能处理范围外的文件!**
```bash ```bash
# 检查暂存区内容 # ✅ 正确做法:查看范围外的文件,但不做任何处理
git diff --cached git status | findstr /v "auth" # 假设检查范围是auth模块查看非auth文件
# ✅ 正确做法:只添加范围内的文件
git add src/business/auth/
git add src/core/auth/
git add test/business/auth/
# ❌ 错误做法:不要重置、暂存或移动范围外文件
# git checkout -- src/business/zulip/some-file.ts # 禁止!
# git stash push src/business/zulip/ # 禁止会影响其他AI
# git reset HEAD src/business/user-mgmt/ # 禁止会影响其他AI
# 🔥 协作原则范围外文件必须保持原状供其他AI处理
```
### 5. 提交前最终检查
```bash
# 检查暂存区内容(确保只有范围内文件)
git diff --cached --name-only
# 确认所有文件都在检查范围内
git diff --cached --name-only | grep -E "^(src|test|docs)/(business|core)/auth/"
# 确认提交信息准确性 # 确认提交信息准确性
git commit --dry-run git commit --dry-run
@@ -240,8 +415,30 @@ git commit -m "提交信息"
## 📄 合并文档生成 ## 📄 合并文档生成
### 🔥 重要规范:独立合并文档生成
**在完成代码提交后必须在docs目录中生成一个独立的合并md文档方便最后统一完成合并操作。**
#### 合并文档命名规范
```
docs/merge-requests/[模块名称]-code-standard-[日期].md
```
#### 合并文档存放位置
- **目录路径**`docs/merge-requests/`
- **文件命名**`[模块名称]-code-standard-[日期].md`
- **示例文件名**
- `auth-code-standard-20240112.md`
- `zulip-code-standard-20240112.md`
- `user-mgmt-code-standard-20240112.md`
#### 创建合并文档目录
如果`docs/merge-requests/`目录不存在,需要先创建:
```bash
mkdir -p docs/merge-requests
```
### 合并请求文档模板 ### 合并请求文档模板
完成所有提交后,生成合并文档: 完成所有提交后,在`docs/merge-requests/`目录中生成独立的合并文档:
```markdown ```markdown
# 代码规范优化合并请求 # 代码规范优化合并请求
@@ -308,12 +505,89 @@ git commit -m "提交信息"
- **监控要点**:关注 [具体的监控指标] - **监控要点**:关注 [具体的监控指标]
``` ```
### 📝 独立合并文档创建示例
#### 1. 创建合并文档目录(如果不存在)
```bash
mkdir -p docs/merge-requests
```
#### 2. 生成具体的合并文档
假设当前检查的是auth模块日期是2024-01-12则创建文件
`docs/merge-requests/auth-code-standard-20240112.md`
#### 3. 合并文档内容示例
```markdown
# Auth模块代码规范优化合并请求
## 📋 变更概述
本次合并请求包含对Auth模块的代码规范优化和质量提升涉及登录、注册、权限验证等核心功能。
## 🔍 主要变更内容
### 代码规范优化
- **命名规范**统一service、controller、entity文件命名
- **注释规范**完善JSDoc注释添加参数和返回值说明
- **代码清理**:移除未使用的导入和死代码
- **格式统一**统一TypeScript代码缩进和换行
### 功能改进
- **错误处理**:完善异常捕获和错误提示
- **类型安全**添加缺失的TypeScript类型定义
- **性能优化**:优化数据库查询和缓存策略
### 测试完善
- **测试覆盖**:补充登录服务和注册控制器的单元测试
- **集成测试**添加JWT认证流程的集成测试
- **E2E测试**:完善用户注册登录的端到端测试
## 📊 影响范围
- **修改文件数量**15个文件
- **涉及模块**src/business/auth/, src/core/auth/, test/business/auth/
- **新增代码行数**+245行
- **删除代码行数**-89行
- **测试覆盖率**从78%提升到95%
## 🧪 测试验证
- [x] 所有单元测试通过 (npm run test:auth:unit)
- [x] 集成测试通过 (npm run test:auth:integration)
- [x] E2E测试通过 (npm run test:auth:e2e)
- [x] 手动功能验证通过
## 🔗 相关信息
- **分支名称**feature/code-standard-auth-20240112
- **远程仓库**origin
- **检查日期**2024-01-12
- **检查人员**[用户名称]
## 📝 合并后操作
1. 验证生产环境功能正常
2. 监控登录注册成功率
3. 关注系统性能指标
4. 更新相关文档链接
---
**文档生成时间**2024-01-12
**对应分支**feature/code-standard-auth-20240112
**合并状态**:待合并
```
#### 4. 在PR中引用合并文档
创建Pull Request时在描述中添加
```markdown
## 📄 详细合并文档
请查看独立合并文档:`docs/merge-requests/auth-code-standard-20240112.md`
该文档包含完整的变更说明、测试验证结果和合并后操作指南。
```
## 🔧 执行步骤总结 ## 🔧 执行步骤总结
### 完整执行流程 ### 完整执行流程
1. **Git变更检查** 1. **Git变更检查**
- 执行 `git status` 和 `git diff` 查看变更 - 执行 `git status` 和 `git diff` 查看变更
- 确认所有修改文件都在预期范围内 - 确认所有修改文件都在当前检查任务的范围内
- 排除或暂存范围外的文件
2. **修改记录校验** 2. **修改记录校验**
- 逐个检查修改文件的头部注释 - 逐个检查修改文件的头部注释
@@ -321,29 +595,99 @@ git commit -m "提交信息"
- 如有不一致,立即修正 - 如有不一致,立即修正
3. **创建功能分支** 3. **创建功能分支**
- 根据修改类型创建合适的分支 - 🔥 **在当前分支基础上**创建新分支(不切换到主分支)
- 使用规范的分支命名格式 - 根据修改类型和检查范围创建合适的分支
- 使用规范的分支命名格式(包含模块标识)
4. **分类提交代码** 4. **分类提交代码**
- 按修改类型分别提交style、feat、fix、docs等 - 按修改类型分别提交style、feat、fix、docs等
- 使用规范的提交信息格式 - 使用规范的提交信息格式(包含范围标识)
- 每次提交保持原子性(一次提交只做一件事) - 每次提交保持原子性(一次提交只做一件事)
- 确保每次提交只包含检查范围内的文件
5. **生成合并文档** 5. **推送到指定远程仓库**
- 创建详细的合并请求文档 - 询问用户要推送到哪个远程仓库
- 说明变更内容、影响范围、测试情况 - 使用 `git push [远程仓库名] [分支名]` 推送到指定远程仓库
- 提供审查要点和部署说明 - 验证推送结果和分支状态
6. **推送和创建PR** 6. **生成独立合并文档**
- 推送分支到远程仓库 - 在 `docs/merge-requests/` 目录中创建独立的合并md文档
- 创建Pull Request并关联合并文档 - 使用规范的文件命名:`[模块名称]-code-standard-[日期].md`
- 包含完整的变更概述、影响范围、测试验证等信息
- 方便后续统一进行合并操作管理
7. **创建PR和关联文档**
- 在指定的远程仓库创建Pull Request
- 在PR描述中引用独立合并文档的路径
- 明确标注检查范围和变更内容
## 🚀 推送到远程仓库
### 📋 执行前询问
**在推送前AI必须询问用户以下信息**
1. **目标远程仓库名称**要推送到哪个远程仓库origin、whale-town-end、upstream等
2. **确认分支名称**:确认要推送的分支名称是否正确
### 推送新分支到指定远程仓库
完成所有提交后,将分支推送到用户指定的远程仓库:
```bash
# 推送新分支到指定远程仓库([远程仓库名]由用户提供)
git push [远程仓库名] feature/code-standard-[模块名称]-[日期]
# 示例推送到origin远程仓库
git push origin feature/code-standard-auth-20240112
# 示例推送到whale-town-end远程仓库
git push whale-town-end feature/code-standard-auth-20240112
# 示例推送到upstream远程仓库
git push upstream feature/code-standard-zulip-20240112
# 如果是首次推送该分支,设置上游跟踪
git push -u [远程仓库名] feature/code-standard-auth-20240112
```
### 验证推送结果
```bash
# 查看远程分支状态
git branch -r
# 确认分支已成功推送到指定远程仓库
git ls-remote [远程仓库名] | grep feature/code-standard-[模块名称]-[日期]
# 查看指定远程仓库的所有分支
git ls-remote [远程仓库名]
```
### 远程仓库配置检查
如果推送时遇到问题,可以检查远程仓库配置:
```bash
# 查看当前配置的所有远程仓库
git remote -v
# 如果没有指定的远程仓库,需要添加
git remote add [远程仓库名] [仓库URL]
# 验证指定远程仓库连接
git remote show [远程仓库名]
```
### 🔍 常见远程仓库名称
- **origin**:通常是默认的远程仓库
- **upstream**:通常指向原始项目仓库
- **whale-town-end**:项目特定的远程仓库名
- **fork**个人fork的仓库
- **dev**:开发环境仓库
## ⚠️ 重要注意事项 ## ⚠️ 重要注意事项
### 提交原则 ### 提交原则
- **范围限制**:只提交当前检查任务范围内的文件,不涉及其他模块
- **原子性**:每次提交只包含一个逻辑改动 - **原子性**:每次提交只包含一个逻辑改动
- **完整性**:每次提交的代码都应该能正常运行 - **完整性**:每次提交的代码都应该能正常运行
- **描述性**:提交信息要清晰描述改动内容和原因 - **描述性**:提交信息要清晰描述改动内容、范围和原因
- **一致性**:文件修改记录必须与实际修改内容一致 - **一致性**:文件修改记录必须与实际修改内容一致
### 质量保证 ### 质量保证
@@ -354,6 +698,7 @@ git commit -m "提交信息"
### 协作规范 ### 协作规范
- 遵循项目的分支管理策略 - 遵循项目的分支管理策略
- 推送前询问并确认目标远程仓库
- 提供清晰的合并请求说明 - 提供清晰的合并请求说明
- 及时响应代码审查意见 - 及时响应代码审查意见
- 保持提交历史的清晰和可追溯性 - 保持提交历史的清晰和可追溯性
@@ -365,4 +710,33 @@ git commit -m "提交信息"
- ✅ 执行修改 → 🔥 立即重新执行步骤7 → 提供验证报告 → 等待用户确认 - ✅ 执行修改 → 🔥 立即重新执行步骤7 → 提供验证报告 → 等待用户确认
- ❌ 执行修改 → 直接结束检查(错误做法) - ❌ 执行修改 → 直接结束检查(错误做法)
**不能跳过重新检查环节!** **🚨 重要强调:纯检查步骤不更新修改记录**
**如果检查发现代码提交已经符合规范,无需任何修改,则:**
-**禁止添加检查记录**:不要添加"AI代码检查步骤7代码提交检查和优化"
-**禁止更新时间戳**:不要修改@lastModified字段
-**禁止递增版本号**:不要修改@version字段
-**仅提供检查报告**:说明检查结果,确认符合规范
**不能跳过重新检查环节!**
### 🔥 合并文档生成强制要求
**每次完成代码提交后必须在docs/merge-requests/目录中生成独立的合并md文档**
- ✅ 完成提交 → 生成独立合并文档 → 在PR中引用文档路径
- ❌ 完成提交 → 直接创建PR缺少独立文档
**独立合并文档是统一管理合并操作的重要依据,不能省略!**
## 📋 执行前必须询问的信息
**在执行推送操作前AI必须询问用户**
1. **目标远程仓库名称**
- 问题:请问要推送到哪个远程仓库?
- 示例回答origin / whale-town-end / upstream / 其他
2. **确认分支名称**
- 问题确认要推送的分支名称是feature/code-standard-[模块名称]-[日期] 吗?
- 等待用户确认或提供正确的分支名称
**只有获得用户明确回答后,才能执行推送操作!**

View File

@@ -82,7 +82,7 @@ C. 接收消息 (Downstream: Zulip -> Node -> Godot)
``` ```
用户注册 (POST /auth/register) 用户注册 (POST /auth/register)
1. 创建游戏账号 (LoginService.register) 1. 创建游戏账号 (RegisterService.register)
2. 初始化 Zulip 管理员客户端 2. 初始化 Zulip 管理员客户端

View File

@@ -24,4 +24,6 @@ module.exports = {
transformIgnorePatterns: [ transformIgnorePatterns: [
'node_modules/(?!(@faker-js/faker)/)', 'node_modules/(?!(@faker-js/faker)/)',
], ],
// 设置测试环境变量
setupFilesAfterEnv: ['<rootDir>/test-setup.js'],
}; };

View File

@@ -15,20 +15,7 @@
"test:unit": "jest --testPathPattern=spec.ts --testPathIgnorePatterns=e2e.spec.ts", "test:unit": "jest --testPathPattern=spec.ts --testPathIgnorePatterns=e2e.spec.ts",
"test:integration": "jest --testPathPattern=integration.spec.ts --runInBand", "test:integration": "jest --testPathPattern=integration.spec.ts --runInBand",
"test:property": "jest --testPathPattern=property.spec.ts", "test:property": "jest --testPathPattern=property.spec.ts",
"test:all": "cross-env RUN_E2E_TESTS=true jest --runInBand", "test:all": "cross-env RUN_E2E_TESTS=true jest --runInBand"
"test:isolated": "jest --runInBand --forceExit --detectOpenHandles",
"test:debug": "jest --runInBand --detectOpenHandles --verbose",
"test:zulip": "jest --testPathPattern=zulip.*spec.ts --runInBand",
"test:zulip:unit": "jest --testPathPattern=zulip.*spec.ts --testPathIgnorePatterns=integration --testPathIgnorePatterns=e2e --testPathIgnorePatterns=performance --runInBand",
"test:zulip:integration": "jest test/zulip_integration/integration/ --runInBand",
"test:zulip:e2e": "jest test/zulip_integration/e2e/ --runInBand",
"test:zulip:performance": "jest test/zulip_integration/performance/ --runInBand",
"test:zulip-integration": "node scripts/test-zulip-integration.js",
"test:zulip-real": "jest test/zulip_integration/real_zulip_api.spec.ts --runInBand",
"test:zulip-message": "jest src/core/zulip_core/services/zulip_message_integration.spec.ts",
"zulip:connection-test": "npx ts-node test/zulip_integration/tools/simple_connection_test.ts",
"zulip:list-streams": "npx ts-node test/zulip_integration/tools/list_streams.ts",
"zulip:chat-simulation": "npx ts-node test/zulip_integration/tools/chat_simulation.ts"
}, },
"keywords": [ "keywords": [
"game", "game",
@@ -40,6 +27,7 @@
"author": "", "author": "",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@nestjs/cache-manager": "^3.1.0",
"@nestjs/common": "^11.1.9", "@nestjs/common": "^11.1.9",
"@nestjs/config": "^4.0.2", "@nestjs/config": "^4.0.2",
"@nestjs/core": "^11.1.9", "@nestjs/core": "^11.1.9",
@@ -56,6 +44,7 @@
"archiver": "^7.0.1", "archiver": "^7.0.1",
"axios": "^1.13.2", "axios": "^1.13.2",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"cache-manager": "^7.2.8",
"class-transformer": "^0.5.1", "class-transformer": "^0.5.1",
"class-validator": "^0.14.3", "class-validator": "^0.14.3",
"ioredis": "^5.8.2", "ioredis": "^5.8.2",

View File

@@ -31,8 +31,8 @@ export * from './login.controller';
export * from './register.controller'; export * from './register.controller';
// 服务 // 服务
export * from './login.service'; export { LoginService } from './login.service';
export * from './register.service'; export { RegisterService } from './register.service';
// DTO // DTO
export * from './login.dto'; export * from './login.dto';

View File

@@ -0,0 +1,120 @@
/**
* 邮件模块测试套件
*
* 功能描述:
* - 测试EmailModule的模块配置和依赖注入
* - 验证模块导入、提供者和导出的正确性
* - 确保邮件服务的正确配置
* - 测试模块间的依赖关系
*
* 测试覆盖范围:
* - 模块实例化:模块能够正确创建和初始化
* - 依赖注入:所有服务的正确注入
* - 服务导出EmailService的正确导出
* - 配置验证:邮件配置的正确性
*
* 最近修改:
* - 2026-01-12: 功能新增 - 创建EmailModule测试文件确保模块配置测试覆盖 (修改者: moyin)
*
* @author moyin
* @version 1.0.0
* @since 2026-01-12
* @lastModified 2026-01-12
*/
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigService } from '@nestjs/config';
import { EmailModule } from './email.module';
import { EmailService } from './email.service';
describe('EmailModule', () => {
let module: TestingModule;
let emailService: EmailService;
let configService: ConfigService;
beforeEach(async () => {
const mockConfigService = {
get: jest.fn((key: string, defaultValue?: any) => {
switch (key) {
case 'EMAIL_HOST':
return 'smtp.test.com';
case 'EMAIL_PORT':
return 587;
case 'EMAIL_USER':
return 'test@test.com';
case 'EMAIL_PASS':
return 'test-password';
default:
return defaultValue;
}
}),
};
module = await Test.createTestingModule({
providers: [
EmailService,
{
provide: ConfigService,
useValue: mockConfigService,
},
],
}).compile();
emailService = module.get<EmailService>(EmailService);
configService = module.get<ConfigService>(ConfigService);
});
afterEach(async () => {
if (module) {
await module.close();
}
});
it('should be defined', () => {
expect(module).toBeDefined();
});
describe('Service Providers', () => {
it('should provide EmailService', () => {
expect(emailService).toBeDefined();
expect(emailService).toBeInstanceOf(EmailService);
});
it('should provide ConfigService', () => {
expect(configService).toBeDefined();
expect(configService.get).toBeDefined();
});
});
describe('Module Dependencies', () => {
it('should import required modules', () => {
expect(module).toBeDefined();
expect(emailService).toBeDefined();
});
it('should not have circular dependencies', () => {
expect(module).toBeDefined();
});
});
describe('Module Exports', () => {
it('should export EmailService', () => {
expect(emailService).toBeDefined();
expect(emailService).toBeInstanceOf(EmailService);
});
it('should make EmailService available for injection', () => {
const service = module.get<EmailService>(EmailService);
expect(service).toBe(emailService);
});
});
describe('Configuration Validation', () => {
it('should validate email configuration completeness', () => {
expect(configService.get('EMAIL_HOST')).toBeDefined();
expect(configService.get('EMAIL_PORT')).toBeDefined();
expect(configService.get('EMAIL_USER')).toBeDefined();
expect(configService.get('EMAIL_PASS')).toBeDefined();
});
});
});

27
test-setup.js Normal file
View File

@@ -0,0 +1,27 @@
/**
* Jest测试环境设置
*
* 功能描述:
* - 加载.env文件中的环境变量
* - 为测试环境提供必要的配置
*
* @author moyin
* @version 1.0.0
* @since 2026-01-12
*/
const dotenv = require('dotenv');
const path = require('path');
// 加载.env文件
dotenv.config({ path: path.resolve(__dirname, '.env') });
// 只在需要时输出调试信息
if (process.env.DEBUG_TEST_CONFIG === 'true') {
console.log('🔧 测试环境配置加载:');
console.log(` DB_HOST: ${process.env.DB_HOST ? '已配置' : '未配置'}`);
console.log(` DB_PORT: ${process.env.DB_PORT ? '已配置' : '未配置'}`);
console.log(` DB_USERNAME: ${process.env.DB_USERNAME ? '已配置' : '未配置'}`);
console.log(` DB_PASSWORD: ${process.env.DB_PASSWORD ? '已配置' : '未配置'}`);
console.log(` DB_NAME: ${process.env.DB_NAME ? '已配置' : '未配置'}`);
}

View File

@@ -0,0 +1,230 @@
/**
* Zulip集成功能端到端测试
*
* 功能描述:
* - 测试用户注册时Zulip账号的创建和绑定
* - 验证用户登录时Zulip API Key的验证和更新
* - 确保Zulip账号关联的完整性
* - 测试Zulip集成的完整业务流程
*
* 职责分离:
* - E2E测试测试完整的用户注册和登录流程
* - 集成验证验证Zulip服务与业务逻辑的集成
* - 数据一致性确保Zulip账号关联数据的正确性
*
* 测试策略:
* - 模拟真实用户操作流程进行端到端测试
* - 验证Zulip账号创建和绑定的各种场景
* - 测试异常情况下的错误处理和恢复机制
*
* 使用场景:
* - 验证Zulip集成功能的完整性
* - 确保用户注册登录流程的稳定性
* - 回归测试中验证Zulip相关功能
*
* 最近修改:
* - 2026-01-12: 代码规范优化 - 完善E2E测试文件注释规范添加职责分离和使用场景 (修改者: moyin)
*
* @author moyin
* @version 1.0.1
* @since 2026-01-12
* @lastModified 2026-01-12
*/
import { Test, TestingModule } from '@nestjs/testing';
import { INestApplication } from '@nestjs/common';
import * as request from 'supertest';
import { AppModule } from '../../src/app.module';
describe('Zulip Integration (e2e)', () => {
let app: INestApplication;
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile();
app = moduleFixture.createNestApplication();
await app.init();
});
afterEach(async () => {
await app.close();
});
describe('用户注册时的Zulip集成', () => {
it('应该在用户注册时创建或绑定Zulip账号', async () => {
const timestamp = Date.now();
const username = `zuliptest${timestamp}`;
const email = `zuliptest${timestamp}@example.com`;
const response = await request(app.getHttpServer())
.post('/auth/register')
.send({
username,
password: 'password123',
nickname: 'Zulip测试用户',
email,
email_verification_code: '123456' // 在测试模式下可能需要
})
.expect(201);
expect(response.body.success).toBe(true);
expect(response.body.data.user.username).toBe(username);
expect(response.body.data.access_token).toBeDefined();
// 检查响应消息是否包含Zulip相关信息
const message = response.body.data.message || response.body.message;
console.log('注册响应消息:', message);
});
it('应该处理邮箱已存在的Zulip账号绑定', async () => {
const timestamp = Date.now();
const username1 = `zulipbind1_${timestamp}`;
const username2 = `zulipbind2_${timestamp}`;
const sharedEmail = `shared${timestamp}@example.com`;
// 第一次注册
await request(app.getHttpServer())
.post('/auth/register')
.send({
username: username1,
password: 'password123',
nickname: 'Zulip绑定测试1',
email: sharedEmail,
})
.expect(201);
// 第二次注册使用不同用户名但相同邮箱模拟Zulip账号已存在的情况
const response = await request(app.getHttpServer())
.post('/auth/register')
.send({
username: username2,
password: 'password123',
nickname: 'Zulip绑定测试2',
email: `different${timestamp}@example.com`, // 使用不同邮箱避免冲突
})
.expect(201);
expect(response.body.success).toBe(true);
});
});
describe('用户登录时的Zulip API Key验证', () => {
let testUser: any;
beforeEach(async () => {
// 创建测试用户
const timestamp = Date.now();
const username = `loginzulip${timestamp}`;
const email = `loginzulip${timestamp}@example.com`;
const registerResponse = await request(app.getHttpServer())
.post('/auth/register')
.send({
username,
password: 'password123',
nickname: 'Zulip登录测试用户',
email,
})
.expect(201);
testUser = {
username,
password: 'password123',
email,
userId: registerResponse.body.data.user.id
};
});
it('应该在登录时验证Zulip API Key', async () => {
const response = await request(app.getHttpServer())
.post('/auth/login')
.send({
identifier: testUser.username,
password: testUser.password
})
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data.user.username).toBe(testUser.username);
expect(response.body.data.access_token).toBeDefined();
// 登录成功表示Zulip API Key验证通过或已更新
console.log('登录成功Zulip API Key状态正常');
});
it('应该处理多次登录的API Key验证', async () => {
// 第一次登录
const firstLogin = await request(app.getHttpServer())
.post('/auth/login')
.send({
identifier: testUser.username,
password: testUser.password
})
.expect(200);
expect(firstLogin.body.success).toBe(true);
// 第二次登录测试API Key缓存和验证
const secondLogin = await request(app.getHttpServer())
.post('/auth/login')
.send({
identifier: testUser.username,
password: testUser.password
})
.expect(200);
expect(secondLogin.body.success).toBe(true);
console.log('多次登录API Key验证正常');
});
});
describe('错误处理', () => {
it('应该在Zulip服务不可用时仍能正常注册', async () => {
const timestamp = Date.now();
const username = `errortest${timestamp}`;
// 即使Zulip服务出错用户注册也应该成功
const response = await request(app.getHttpServer())
.post('/auth/register')
.send({
username,
password: 'password123',
nickname: 'Zulip错误测试用户',
// 不提供邮箱跳过Zulip创建
})
.expect(201);
expect(response.body.success).toBe(true);
expect(response.body.data.user.username).toBe(username);
});
it('应该在Zulip API Key验证失败时仍能正常登录', async () => {
// 创建没有邮箱的用户不会创建Zulip账号
const timestamp = Date.now();
const username = `nozulip${timestamp}`;
await request(app.getHttpServer())
.post('/auth/register')
.send({
username,
password: 'password123',
nickname: '无Zulip测试用户',
})
.expect(201);
// 登录应该成功即使没有Zulip账号
const response = await request(app.getHttpServer())
.post('/auth/login')
.send({
identifier: username,
password: 'password123'
})
.expect(200);
expect(response.body.success).toBe(true);
console.log('无Zulip账号用户登录正常');
});
});
});

View File

@@ -0,0 +1,234 @@
/**
* Zulip账号关联服务数据库测试
*
* 功能描述:
* - 专门测试数据库模式下的真实数据库操作
* - 需要配置数据库环境变量才能运行
* - 测试真实的CRUD操作和业务逻辑
*
* 运行条件:
* - 需要设置环境变量DB_HOST, DB_PORT, DB_USERNAME, DB_PASSWORD, DB_NAME
* - 数据库中需要存在 zulip_accounts 表
*
* @author angjustinl
* @version 1.0.0
* @since 2026-01-10
*/
import { Test, TestingModule } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ConfigModule } from '@nestjs/config';
import { CacheModule } from '@nestjs/cache-manager';
import { ZulipAccountsService } from '../../src/core/db/zulip_accounts/zulip_accounts.service';
import { ZulipAccountsRepository } from '../../src/core/db/zulip_accounts/zulip_accounts.repository';
import { ZulipAccounts } from '../../src/core/db/zulip_accounts/zulip_accounts.entity';
import { Users } from '../../src/core/db/users/users.entity';
import { AppLoggerService } from '../../src/core/utils/logger/logger.service';
/**
* 检查是否配置了数据库
*/
function isDatabaseConfigured(): boolean {
const requiredEnvVars = ['DB_HOST', 'DB_PORT', 'DB_USERNAME', 'DB_PASSWORD', 'DB_NAME'];
return requiredEnvVars.every(varName => process.env[varName]);
}
// 只有在配置了数据库时才运行这些测试
const describeDatabase = isDatabaseConfigured() ? describe : describe.skip;
describeDatabase('ZulipAccountsService - Database Mode', () => {
let service: ZulipAccountsService;
let module: TestingModule;
// 只有在数据库配置完整时才输出这些信息
if (isDatabaseConfigured()) {
console.log('🗄️ 运行数据库模式测试');
console.log('📊 使用真实数据库连接进行测试');
}
beforeAll(async () => {
module = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
envFilePath: ['.env.test', '.env'],
isGlobal: true,
}),
TypeOrmModule.forRoot({
type: 'mysql',
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT || '3306'),
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
entities: [ZulipAccounts, Users],
synchronize: false,
logging: false,
}),
TypeOrmModule.forFeature([ZulipAccounts, Users]),
CacheModule.register({
ttl: 300,
max: 1000,
}),
],
providers: [
ZulipAccountsService,
ZulipAccountsRepository,
AppLoggerService,
],
}).compile();
service = module.get<ZulipAccountsService>(ZulipAccountsService);
}, 30000); // 增加超时时间
afterAll(async () => {
if (module) {
await module.close();
}
});
// 生成唯一的测试数据
const generateTestData = (suffix: string = Date.now().toString()) => {
const timestamp = Date.now();
const uniqueId = timestamp + Math.floor(Math.random() * 1000); // 添加随机数避免冲突
return {
gameUserId: uniqueId.toString(), // 使用纯数字字符串
zulipUserId: parseInt(`8${timestamp.toString().slice(-5)}`),
zulipEmail: `test_db_${timestamp}_${suffix}@example.com`,
zulipFullName: `数据库测试用户_${timestamp}_${suffix}`,
zulipApiKeyEncrypted: 'encrypted_api_key_for_db_test',
status: 'active' as const,
};
};
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('Database CRUD Operations', () => {
it('should create and retrieve account from database', async () => {
const testData = generateTestData('crud');
// 创建账号
const created = await service.create(testData);
expect(created).toBeDefined();
expect(created.gameUserId).toBe(testData.gameUserId);
expect(created.zulipEmail).toBe(testData.zulipEmail);
expect(created.status).toBe('active');
// 根据游戏用户ID查找
const found = await service.findByGameUserId(testData.gameUserId);
expect(found).toBeDefined();
expect(found?.id).toBe(created.id);
expect(found?.zulipUserId).toBe(testData.zulipUserId);
// 清理测试数据
await service.deleteByGameUserId(testData.gameUserId);
}, 15000);
it('should handle duplicate creation properly', async () => {
const testData = generateTestData('duplicate');
// 创建第一个账号
const created = await service.create(testData);
expect(created).toBeDefined();
// 尝试创建重复账号,应该抛出异常
await expect(service.create(testData)).rejects.toThrow();
// 清理测试数据
await service.deleteByGameUserId(testData.gameUserId);
}, 15000);
it('should update account in database', async () => {
const testData = generateTestData('update');
// 创建账号
const created = await service.create(testData);
// 更新账号
const updated = await service.update(created.id, {
zulipFullName: '更新后的用户名',
status: 'inactive',
});
expect(updated.zulipFullName).toBe('更新后的用户名');
expect(updated.status).toBe('inactive');
// 清理测试数据
await service.deleteByGameUserId(testData.gameUserId);
}, 15000);
it('should delete account from database', async () => {
const testData = generateTestData('delete');
// 创建账号
const created = await service.create(testData);
// 删除账号
const deleted = await service.delete(created.id);
expect(deleted).toBe(true);
// 验证账号已被删除
const found = await service.findByGameUserId(testData.gameUserId);
expect(found).toBeNull();
}, 15000);
});
describe('Database Business Logic', () => {
it('should check email existence in database', async () => {
const testData = generateTestData('email_check');
// 邮箱不存在时应该返回false
const notExists = await service.existsByEmail(testData.zulipEmail);
expect(notExists).toBe(false);
// 创建账号
await service.create(testData);
// 邮箱存在时应该返回true
const exists = await service.existsByEmail(testData.zulipEmail);
expect(exists).toBe(true);
// 清理测试数据
await service.deleteByGameUserId(testData.gameUserId);
}, 15000);
it('should get status statistics from database', async () => {
const stats = await service.getStatusStatistics();
expect(typeof stats.active).toBe('number');
expect(typeof stats.inactive).toBe('number');
expect(typeof stats.suspended).toBe('number');
expect(typeof stats.error).toBe('number');
expect(typeof stats.total).toBe('number');
expect(stats.total).toBe(stats.active + stats.inactive + stats.suspended + stats.error);
}, 15000);
it('should verify account in database', async () => {
const testData = generateTestData('verify');
// 创建账号
await service.create(testData);
// 验证账号
const result = await service.verifyAccount(testData.gameUserId);
expect(result.success).toBe(true);
expect(result.isValid).toBe(true);
expect(result.verifiedAt).toBeDefined();
// 清理测试数据
await service.deleteByGameUserId(testData.gameUserId);
}, 15000);
});
});
// 如果没有配置数据库,显示跳过信息
if (!isDatabaseConfigured()) {
console.log('⚠️ 数据库测试已跳过:未检测到数据库配置');
console.log('💡 要运行数据库测试,请设置以下环境变量:');
console.log(' - DB_HOST');
console.log(' - DB_PORT');
console.log(' - DB_USERNAME');
console.log(' - DB_PASSWORD');
console.log(' - DB_NAME');
}

View File

@@ -0,0 +1,161 @@
/**
* Zulip账号关联集成测试
*
* 功能描述:
* - 测试数据库和内存模式的切换
* - 测试完整的业务流程
* - 验证模块配置的正确性
*
* @author angjustinl
* @version 1.0.0
* @since 2025-01-07
*/
import { Test, TestingModule } from '@nestjs/testing';
import { ConfigModule } from '@nestjs/config';
import { ZulipAccountsModule } from '../../src/core/db/zulip_accounts/zulip_accounts.module';
import { ZulipAccountsMemoryService } from '../../src/core/db/zulip_accounts/zulip_accounts_memory.service';
import { CreateZulipAccountDto } from '../../src/core/db/zulip_accounts/zulip_accounts.dto';
describe('ZulipAccountsModule Integration', () => {
let memoryModule: TestingModule;
beforeAll(async () => {
// 测试内存模式
memoryModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
envFilePath: ['.env.test', '.env'],
isGlobal: true,
}),
ZulipAccountsModule.forMemory()
],
}).compile();
});
afterAll(async () => {
if (memoryModule) {
await memoryModule.close();
}
});
describe('Memory Mode', () => {
let service: ZulipAccountsMemoryService;
beforeEach(() => {
service = memoryModule.get<ZulipAccountsMemoryService>('ZulipAccountsService');
});
it('should be defined', () => {
expect(service).toBeDefined();
expect(service).toBeInstanceOf(ZulipAccountsMemoryService);
});
it('should create and retrieve account in memory', async () => {
const createDto: CreateZulipAccountDto = {
gameUserId: '77777',
zulipUserId: 88888,
zulipEmail: 'memory@example.com',
zulipFullName: '内存测试用户',
zulipApiKeyEncrypted: 'encrypted_api_key',
status: 'active',
};
// 创建账号关联
const created = await service.create(createDto);
expect(created).toBeDefined();
expect(created.gameUserId).toBe('77777');
expect(created.zulipEmail).toBe('memory@example.com');
// 根据游戏用户ID查找
const found = await service.findByGameUserId('77777');
expect(found).toBeDefined();
expect(found?.id).toBe(created.id);
});
it('should handle batch operations in memory', async () => {
// 创建多个账号
const accounts = [];
for (let i = 1; i <= 3; i++) {
const createDto: CreateZulipAccountDto = {
gameUserId: `${20000 + i}`,
zulipUserId: 30000 + i,
zulipEmail: `batch${i}@example.com`,
zulipFullName: `批量用户${i}`,
zulipApiKeyEncrypted: 'encrypted_api_key',
status: 'active',
};
const account = await service.create(createDto);
accounts.push(account);
}
// 批量更新状态
const ids = accounts.map(a => a.id);
const batchResult = await service.batchUpdateStatus(ids, 'inactive');
expect(batchResult.success).toBe(true);
expect(batchResult.updatedCount).toBe(3);
// 验证状态已更新
for (const account of accounts) {
const updated = await service.findById(account.id);
expect(updated.status).toBe('inactive');
}
});
it('should get statistics in memory', async () => {
// 创建不同状态的账号
const statuses: Array<'active' | 'inactive' | 'suspended' | 'error'> = ['active', 'inactive', 'suspended', 'error'];
for (let i = 0; i < statuses.length; i++) {
const createDto: CreateZulipAccountDto = {
gameUserId: `${40000 + i}`,
zulipUserId: 50000 + i,
zulipEmail: `stats${i}@example.com`,
zulipFullName: `统计用户${i}`,
zulipApiKeyEncrypted: 'encrypted_api_key',
status: statuses[i],
};
await service.create(createDto);
}
// 获取统计信息
const stats = await service.getStatusStatistics();
expect(stats.active).toBeGreaterThanOrEqual(1);
expect(stats.inactive).toBeGreaterThanOrEqual(1);
expect(stats.suspended).toBeGreaterThanOrEqual(1);
expect(stats.error).toBeGreaterThanOrEqual(1);
expect(stats.total).toBeGreaterThanOrEqual(4);
});
});
describe('Cross-Mode Compatibility', () => {
it('should have same interface for both modes', () => {
const memoryService = memoryModule.get<ZulipAccountsMemoryService>('ZulipAccountsService');
// 检查内存服务有所需的方法
const methods = [
'create',
'findByGameUserId',
'findByZulipUserId',
'findByZulipEmail',
'findById',
'update',
'updateByGameUserId',
'delete',
'deleteByGameUserId',
'findMany',
'findAccountsNeedingVerification',
'findErrorAccounts',
'batchUpdateStatus',
'getStatusStatistics',
'verifyAccount',
'existsByEmail',
'existsByZulipUserId',
];
methods.forEach(method => {
expect(typeof memoryService[method]).toBe('function');
});
});
});
});

View File

@@ -0,0 +1,469 @@
/**
* Zulip消息发送集成测试
*
* 功能描述:
* - 测试消息发送到真实Zulip服务器的完整流程
* - 验证HTTP请求、响应处理和错误场景
* - 包含网络异常和API错误的测试
*
* 注意这些测试需要真实的Zulip服务器配置
*
* 最近修改:
* - 2026-01-12: 架构优化 - 从src/core/zulip_core/services/移动到test/integration/,符合测试分离规范 (修改者: moyin)
* - 2026-01-12: 代码规范优化 - 修正注释规范和修改记录格式 (修改者: moyin)
* - 2026-01-10: 测试新增 - 创建Zulip消息发送集成测试 (修改者: moyin)
*
* @author moyin
* @version 1.1.0
* @since 2026-01-10
* @lastModified 2026-01-12
*/
import { Test, TestingModule } from '@nestjs/testing';
import { ZulipClientService, ZulipClientConfig, ZulipClientInstance } from '../../src/core/zulip_core/services/zulip_client.service';
import * as nock from 'nock';
describe('ZulipMessageIntegration', () => {
let service: ZulipClientService;
let mockZulipClient: any;
let clientInstance: ZulipClientInstance;
const testConfig: ZulipClientConfig = {
username: 'test-bot@example.com',
apiKey: 'test-api-key-12345',
realm: 'https://test-zulip.example.com',
};
beforeEach(async () => {
// 清理所有HTTP拦截
nock.cleanAll();
const module: TestingModule = await Test.createTestingModule({
providers: [ZulipClientService],
}).compile();
service = module.get<ZulipClientService>(ZulipClientService);
// 创建模拟的zulip-js客户端
mockZulipClient = {
config: testConfig,
users: {
me: {
getProfile: jest.fn(),
},
},
messages: {
send: jest.fn(),
},
queues: {
register: jest.fn(),
deregister: jest.fn(),
},
events: {
retrieve: jest.fn(),
},
};
// 模拟客户端实例
clientInstance = {
userId: 'test-user-123',
config: testConfig,
client: mockZulipClient,
lastEventId: -1,
createdAt: new Date(),
lastActivity: new Date(),
isValid: true,
};
// Mock zulip-js模块加载
jest.spyOn(service as any, 'loadZulipModule').mockResolvedValue(() => mockZulipClient);
});
afterEach(() => {
nock.cleanAll();
jest.clearAllMocks();
});
describe('消息发送到Zulip服务器', () => {
it('应该成功发送消息到Zulip API', async () => {
// 模拟成功的API响应
mockZulipClient.messages.send.mockResolvedValue({
result: 'success',
id: 12345,
msg: '',
});
const result = await service.sendMessage(
clientInstance,
'test-stream',
'test-topic',
'Hello from integration test!'
);
expect(result.success).toBe(true);
expect(result.messageId).toBe(12345);
expect(mockZulipClient.messages.send).toHaveBeenCalledWith({
type: 'stream',
to: 'test-stream',
subject: 'test-topic',
content: 'Hello from integration test!',
});
});
it('应该处理Zulip API错误响应', async () => {
// 模拟API错误响应
mockZulipClient.messages.send.mockResolvedValue({
result: 'error',
msg: 'Stream does not exist',
code: 'STREAM_NOT_FOUND',
});
const result = await service.sendMessage(
clientInstance,
'nonexistent-stream',
'test-topic',
'This should fail'
);
expect(result.success).toBe(false);
expect(result.error).toBe('Stream does not exist');
});
it('应该处理网络连接异常', async () => {
// 模拟网络异常
mockZulipClient.messages.send.mockRejectedValue(new Error('Network timeout'));
const result = await service.sendMessage(
clientInstance,
'test-stream',
'test-topic',
'This will timeout'
);
expect(result.success).toBe(false);
expect(result.error).toBe('Network timeout');
});
it('应该处理认证失败', async () => {
// 模拟认证失败
mockZulipClient.messages.send.mockResolvedValue({
result: 'error',
msg: 'Invalid API key',
code: 'BAD_REQUEST',
});
const result = await service.sendMessage(
clientInstance,
'test-stream',
'test-topic',
'Authentication test'
);
expect(result.success).toBe(false);
expect(result.error).toBe('Invalid API key');
});
it('应该正确处理特殊字符和长消息', async () => {
const longMessage = 'A'.repeat(1000) + '特殊字符测试: 🎮🎯🚀 @#$%^&*()';
mockZulipClient.messages.send.mockResolvedValue({
result: 'success',
id: 67890,
});
const result = await service.sendMessage(
clientInstance,
'test-stream',
'special-chars-topic',
longMessage
);
expect(result.success).toBe(true);
expect(result.messageId).toBe(67890);
expect(mockZulipClient.messages.send).toHaveBeenCalledWith({
type: 'stream',
to: 'test-stream',
subject: 'special-chars-topic',
content: longMessage,
});
});
it('应该更新客户端最后活动时间', async () => {
const initialTime = new Date('2026-01-01T00:00:00Z');
clientInstance.lastActivity = initialTime;
mockZulipClient.messages.send.mockResolvedValue({
result: 'success',
id: 11111,
});
await service.sendMessage(
clientInstance,
'test-stream',
'test-topic',
'Activity test'
);
expect(clientInstance.lastActivity.getTime()).toBeGreaterThan(initialTime.getTime());
});
});
describe('事件队列与Zulip服务器交互', () => {
it('应该成功注册事件队列', async () => {
mockZulipClient.queues.register.mockResolvedValue({
result: 'success',
queue_id: 'test-queue-123',
last_event_id: 42,
});
const result = await service.registerQueue(clientInstance, ['message', 'typing']);
expect(result.success).toBe(true);
expect(result.queueId).toBe('test-queue-123');
expect(result.lastEventId).toBe(42);
expect(clientInstance.queueId).toBe('test-queue-123');
expect(clientInstance.lastEventId).toBe(42);
});
it('应该处理队列注册失败', async () => {
mockZulipClient.queues.register.mockResolvedValue({
result: 'error',
msg: 'Rate limit exceeded',
});
const result = await service.registerQueue(clientInstance);
expect(result.success).toBe(false);
expect(result.error).toBe('Rate limit exceeded');
});
it('应该成功获取事件', async () => {
clientInstance.queueId = 'test-queue-123';
clientInstance.lastEventId = 10;
const mockEvents = [
{
id: 11,
type: 'message',
message: {
id: 98765,
sender_email: 'user@example.com',
content: 'Test message from Zulip',
stream_id: 1,
subject: 'Test Topic',
},
},
{
id: 12,
type: 'typing',
sender: { user_id: 123 },
},
];
mockZulipClient.events.retrieve.mockResolvedValue({
result: 'success',
events: mockEvents,
});
const result = await service.getEvents(clientInstance, true);
expect(result.success).toBe(true);
expect(result.events).toEqual(mockEvents);
expect(clientInstance.lastEventId).toBe(12); // 更新为最后一个事件的ID
});
it('应该处理空事件队列', async () => {
clientInstance.queueId = 'test-queue-123';
mockZulipClient.events.retrieve.mockResolvedValue({
result: 'success',
events: [],
});
const result = await service.getEvents(clientInstance, true);
expect(result.success).toBe(true);
expect(result.events).toEqual([]);
});
it('应该成功注销事件队列', async () => {
clientInstance.queueId = 'test-queue-123';
mockZulipClient.queues.deregister.mockResolvedValue({
result: 'success',
});
const result = await service.deregisterQueue(clientInstance);
expect(result).toBe(true);
expect(clientInstance.queueId).toBeUndefined();
expect(clientInstance.lastEventId).toBe(-1);
});
it('应该处理队列过期情况', async () => {
clientInstance.queueId = 'expired-queue';
// 模拟队列过期的JSON解析错误
mockZulipClient.queues.deregister.mockRejectedValue(
new Error('invalid json response body at https://zulip.example.com/api/v1/events reason: Unexpected token')
);
const result = await service.deregisterQueue(clientInstance);
expect(result).toBe(true); // 应该返回true因为队列已过期
expect(clientInstance.queueId).toBeUndefined();
expect(clientInstance.lastEventId).toBe(-1);
});
});
describe('API Key验证', () => {
it('应该成功验证有效的API Key', async () => {
mockZulipClient.users.me.getProfile.mockResolvedValue({
result: 'success',
email: 'test-bot@example.com',
full_name: 'Test Bot',
user_id: 123,
});
const isValid = await service.validateApiKey(clientInstance);
expect(isValid).toBe(true);
expect(clientInstance.isValid).toBe(true);
});
it('应该拒绝无效的API Key', async () => {
mockZulipClient.users.me.getProfile.mockResolvedValue({
result: 'error',
msg: 'Invalid API key',
});
const isValid = await service.validateApiKey(clientInstance);
expect(isValid).toBe(false);
expect(clientInstance.isValid).toBe(false);
});
it('应该处理API Key验证网络异常', async () => {
mockZulipClient.users.me.getProfile.mockRejectedValue(new Error('Connection refused'));
const isValid = await service.validateApiKey(clientInstance);
expect(isValid).toBe(false);
expect(clientInstance.isValid).toBe(false);
});
});
describe('错误恢复和重试机制', () => {
it('应该在临时网络错误后恢复', async () => {
// 第一次调用失败,第二次成功
mockZulipClient.messages.send
.mockRejectedValueOnce(new Error('Temporary network error'))
.mockResolvedValueOnce({
result: 'success',
id: 99999,
});
// 第一次调用应该失败
const firstResult = await service.sendMessage(
clientInstance,
'test-stream',
'test-topic',
'First attempt'
);
expect(firstResult.success).toBe(false);
// 第二次调用应该成功
const secondResult = await service.sendMessage(
clientInstance,
'test-stream',
'test-topic',
'Second attempt'
);
expect(secondResult.success).toBe(true);
expect(secondResult.messageId).toBe(99999);
});
it('应该处理服务器5xx错误', async () => {
mockZulipClient.messages.send.mockRejectedValue(new Error('Internal Server Error (500)'));
const result = await service.sendMessage(
clientInstance,
'test-stream',
'test-topic',
'Server error test'
);
expect(result.success).toBe(false);
expect(result.error).toBe('Internal Server Error (500)');
});
});
describe('性能和并发测试', () => {
it('应该处理并发消息发送', async () => {
// 模拟多个并发消息 - 设置一次mock让它返回不同的ID
mockZulipClient.messages.send.mockImplementation(() => {
const id = Math.floor(Math.random() * 10000) + 1000;
return Promise.resolve({
result: 'success',
id: id,
});
});
// 创建并发消息发送的Promise数组
const messagePromises: Promise<any>[] = [];
for (let i = 0; i < 10; i++) {
messagePromises.push(
service.sendMessage(
clientInstance,
'test-stream',
'concurrent-topic',
`Concurrent message ${i}`
)
);
}
const results = await Promise.all(messagePromises);
results.forEach((result) => {
expect(result.success).toBe(true);
expect(result.messageId).toBeGreaterThan(999);
});
});
it('应该在大量消息发送时保持性能', async () => {
const startTime = Date.now();
const messageCount = 100;
mockZulipClient.messages.send.mockImplementation(() =>
Promise.resolve({
result: 'success',
id: Math.floor(Math.random() * 100000),
})
);
const promises = Array.from({ length: messageCount }, (_, i) =>
service.sendMessage(
clientInstance,
'performance-stream',
'performance-topic',
`Performance test message ${i}`
)
);
const results = await Promise.all(promises);
const endTime = Date.now();
const duration = endTime - startTime;
// 验证所有消息都成功发送
results.forEach(result => {
expect(result.success).toBe(true);
});
// 性能检查100条消息应该在合理时间内完成这里设为5秒
expect(duration).toBeLessThan(5000);
console.log(`发送${messageCount}条消息耗时: ${duration}ms`);
}, 10000);
});
});

View File

@@ -0,0 +1,340 @@
/**
* 配置验证属性测试
*
* 功能描述:
* - 使用fast-check进行配置验证的属性测试
* - 验证配置验证逻辑的正确性和完整性
* - 测试各种边界情况和随机输入
*
* 职责分离:
* - 属性测试:验证配置验证的数学属性
* - 随机测试:使用随机生成的数据验证逻辑
* - 边界测试:测试各种边界条件
*
* 最近修改:
* - 2026-01-12: 代码规范优化 - 从单元测试中分离属性测试 (修改者: moyin)
*
* @author moyin
* @version 1.0.1
* @since 2026-01-12
* @lastModified 2026-01-12
*/
import { Test, TestingModule } from '@nestjs/testing';
import * as fc from 'fast-check';
import { ConfigManagerService } from '../../src/core/zulip_core/services/config_manager.service';
import { AppLoggerService } from '../../src/core/utils/logger/logger.service';
import * as fs from 'fs';
// Mock fs module
jest.mock('fs');
describe('ConfigManagerService Property Tests', () => {
let service: ConfigManagerService;
let mockLogger: jest.Mocked<AppLoggerService>;
const mockFs = fs as jest.Mocked<typeof fs>;
// 默认有效配置
const validMapConfig = {
maps: [
{
mapId: 'novice_village',
mapName: '新手村',
zulipStream: 'Novice Village',
interactionObjects: [
{
objectId: 'notice_board',
objectName: '公告板',
zulipTopic: 'Notice Board',
position: { x: 100, y: 150 }
}
]
}
]
};
beforeEach(async () => {
jest.clearAllMocks();
// 设置测试环境变量
process.env.NODE_ENV = 'test';
process.env.ZULIP_SERVER_URL = 'https://test-zulip.com';
process.env.ZULIP_BOT_EMAIL = 'test-bot@test.com';
process.env.ZULIP_BOT_API_KEY = 'test-api-key';
process.env.ZULIP_API_KEY_ENCRYPTION_KEY = '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef';
mockLogger = {
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
debug: jest.fn(),
} as any;
// 默认mock fs行为
mockFs.existsSync.mockReturnValue(true);
mockFs.readFileSync.mockReturnValue(JSON.stringify(validMapConfig));
mockFs.writeFileSync.mockImplementation(() => {});
mockFs.mkdirSync.mockImplementation(() => undefined);
const module: TestingModule = await Test.createTestingModule({
providers: [
ConfigManagerService,
{
provide: AppLoggerService,
useValue: mockLogger,
},
],
}).compile();
service = module.get<ConfigManagerService>(ConfigManagerService);
await service.loadMapConfig();
});
afterEach(() => {
jest.restoreAllMocks();
// 清理环境变量
delete process.env.NODE_ENV;
delete process.env.ZULIP_SERVER_URL;
delete process.env.ZULIP_BOT_EMAIL;
delete process.env.ZULIP_BOT_API_KEY;
delete process.env.ZULIP_API_KEY_ENCRYPTION_KEY;
});
/**
* 属性测试: 配置验证
*
* **Feature: zulip-integration, Property 12: 配置验证**
* **Validates: Requirements 10.5**
*
* 对于任何系统配置,系统应该在启动时验证配置的有效性,
* 并在发现无效配置时报告详细的错误信息
*/
describe('Property 12: 配置验证', () => {
/**
* 属性: 对于任何有效的地图配置验证应该返回valid=true
* 验证需求 10.5: 验证配置时系统应在启动时检查配置的有效性并报告错误
*/
it('对于任何有效的地图配置验证应该返回valid=true', async () => {
await fc.assert(
fc.asyncProperty(
// 生成有效的mapId
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
// 生成有效的mapName
fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
// 生成有效的zulipStream
fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
// 生成有效的交互对象数组
fc.array(
fc.record({
objectId: fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
objectName: fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
zulipTopic: fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
position: fc.record({
x: fc.integer({ min: 0, max: 10000 }),
y: fc.integer({ min: 0, max: 10000 }),
}),
}),
{ minLength: 0, maxLength: 10 }
),
async (mapId, mapName, zulipStream, interactionObjects) => {
const config = {
mapId: mapId.trim(),
mapName: mapName.trim(),
zulipStream: zulipStream.trim(),
interactionObjects: interactionObjects.map(obj => ({
objectId: obj.objectId.trim(),
objectName: obj.objectName.trim(),
zulipTopic: obj.zulipTopic.trim(),
position: obj.position,
})),
};
const result = service.validateMapConfigDetailed(config);
// 有效配置应该通过验证
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
}
),
{ numRuns: 100 }
);
}, 60000);
/**
* 属性: 对于任何缺少必填字段的配置验证应该返回valid=false并包含错误信息
* 验证需求 10.5: 验证配置时系统应在启动时检查配置的有效性并报告错误
*/
it('对于任何缺少mapId的配置验证应该返回valid=false', async () => {
await fc.assert(
fc.asyncProperty(
// 生成有效的mapName
fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
// 生成有效的zulipStream
fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
async (mapName, zulipStream) => {
const config = {
// 缺少mapId
mapName: mapName.trim(),
zulipStream: zulipStream.trim(),
interactionObjects: [] as any[],
};
const result = service.validateMapConfigDetailed(config);
// 缺少mapId应该验证失败
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('mapId'))).toBe(true);
}
),
{ numRuns: 100 }
);
}, 60000);
/**
* 属性: 对于任何缺少mapName的配置验证应该返回valid=false
*/
it('对于任何缺少mapName的配置验证应该返回valid=false', async () => {
await fc.assert(
fc.asyncProperty(
// 生成有效的mapId
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
// 生成有效的zulipStream
fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
async (mapId, zulipStream) => {
const config = {
mapId: mapId.trim(),
// 缺少mapName
zulipStream: zulipStream.trim(),
interactionObjects: [] as any[],
};
const result = service.validateMapConfigDetailed(config);
// 缺少mapName应该验证失败
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('mapName'))).toBe(true);
}
),
{ numRuns: 100 }
);
}, 60000);
/**
* 属性: 对于任何缺少zulipStream的配置验证应该返回valid=false
*/
it('对于任何缺少zulipStream的配置验证应该返回valid=false', async () => {
await fc.assert(
fc.asyncProperty(
// 生成有效的mapId
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
// 生成有效的mapName
fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
async (mapId, mapName) => {
const config = {
mapId: mapId.trim(),
mapName: mapName.trim(),
// 缺少zulipStream
interactionObjects: [] as any[],
};
const result = service.validateMapConfigDetailed(config);
// 缺少zulipStream应该验证失败
expect(result.valid).toBe(false);
expect(result.errors.some(e => e.includes('zulipStream'))).toBe(true);
}
),
{ numRuns: 100 }
);
}, 60000);
/**
* 属性: 验证结果的错误数量应该与实际错误数量一致
*/
it('验证结果的错误数量应该与实际错误数量一致', async () => {
await fc.assert(
fc.asyncProperty(
// 随机决定是否包含各个字段
fc.boolean(),
fc.boolean(),
fc.boolean(),
// 生成字段值
fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
async (includeMapId, includeMapName, includeZulipStream, mapId, mapName, zulipStream) => {
const config: any = {
interactionObjects: [] as any[],
};
let expectedErrors = 0;
if (includeMapId) {
config.mapId = mapId.trim();
} else {
expectedErrors++;
}
if (includeMapName) {
config.mapName = mapName.trim();
} else {
expectedErrors++;
}
if (includeZulipStream) {
config.zulipStream = zulipStream.trim();
} else {
expectedErrors++;
}
const result = service.validateMapConfigDetailed(config);
// 错误数量应该与预期一致
expect(result.errors.length).toBe(expectedErrors);
expect(result.valid).toBe(expectedErrors === 0);
}
),
{ numRuns: 100 }
);
}, 60000);
/**
* 属性: 配置验证的幂等性
*/
it('配置验证应该是幂等的', async () => {
await fc.assert(
fc.asyncProperty(
fc.record({
mapId: fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
mapName: fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
zulipStream: fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
interactionObjects: fc.array(
fc.record({
objectId: fc.string({ minLength: 1, maxLength: 50 }).filter(s => s.trim().length > 0),
objectName: fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
zulipTopic: fc.string({ minLength: 1, maxLength: 100 }).filter(s => s.trim().length > 0),
position: fc.record({
x: fc.integer({ min: 0, max: 10000 }),
y: fc.integer({ min: 0, max: 10000 }),
}),
}),
{ maxLength: 5 }
),
}),
async (config) => {
// 多次验证同一个配置应该返回相同结果
const result1 = service.validateMapConfigDetailed(config);
const result2 = service.validateMapConfigDetailed(config);
const result3 = service.validateMapConfigDetailed(config);
expect(result1.valid).toBe(result2.valid);
expect(result2.valid).toBe(result3.valid);
expect(result1.errors).toEqual(result2.errors);
expect(result2.errors).toEqual(result3.errors);
}
),
{ numRuns: 100 }
);
}, 60000);
});
});

File diff suppressed because it is too large Load Diff