diff --git a/docs/superpowers/plans/2026-04-26-test-coverage-strategy.md b/docs/superpowers/plans/2026-04-26-test-coverage-strategy.md new file mode 100644 index 0000000..6199de2 --- /dev/null +++ b/docs/superpowers/plans/2026-04-26-test-coverage-strategy.md @@ -0,0 +1,544 @@ +# 测试覆盖率提升实施计划 + +> 设计规格: `docs/superpowers/specs/2026-04-26-test-coverage-strategy-design.md` +> 日期: 2026-04-26 | 状态: draft | 总周期: 9 周(+ Phase 0: 2 天) + +--- + +## Phase 0: 测试基础设施搭建(2 天) + +### Task 1: TestApp struct 实现 + +**目标**: 在现有 `TestDb` 基础上封装 `TestApp`,提供完整测试环境。 + +**涉及文件**: +- 修改: `crates/erp-server/tests/integration/test_db.rs` + +**详细步骤**: + +1. 在 `test_db.rs` 中新增 `TestApp` struct: +```rust +pub struct TestApp { + test_db: TestDb, + health_state: HealthState, + tenant_id: Uuid, + operator_id: Uuid, +} +``` + +2. 实现 `TestApp::new()`: + - 调用 `TestDb::new()` 创建隔离数据库 + - 构建 `HealthState { db, event_bus: EventBus::new(100), crypto: PiiCrypto::dev_default() }` + - 生成随机 `tenant_id` 和 `operator_id` + - 复用现有 `make_state` 模式(见 `health_patient_tests.rs:15-21`) + +3. 添加访问方法: + - `db()` → `&DatabaseConnection` + - `health_state()` → `&HealthState` + - `tenant_id()` → `Uuid` + - `operator_id()` → `Uuid` + +**验收标准**: +- `cargo test -p erp-server test_app_new` 通过 +- TestApp Drop 时自动清理临时数据库(复用 TestDb 的 Drop 实现) +- 现有 7 个集成测试不受影响 + +--- + +### Task 2: TestFixture 工厂函数 + +**目标**: 提供预构建测试数据的工厂函数,减少每个测试的 setup 代码。 + +**涉及文件**: +- 新增: `crates/erp-server/tests/integration/test_fixture.rs` +- 修改: `crates/erp-server/tests/integration.rs`(添加模块注册) + +**详细步骤**: + +1. 创建 `TestFixture` 模块,实现以下工厂函数: + +```rust +impl TestApp { + /// 创建患者(含基本字段,可自定义覆盖) + pub async fn create_patient(&self, overrides: PatientOverrides) -> Patient { ... } + + /// 创建医生 + pub async fn create_doctor(&self, overrides: DoctorOverrides) -> DoctorProfile { ... } + + /// 创建排班 + pub async fn create_schedule(&self, doctor_id: Uuid, date: NaiveDate, ...) -> DoctorSchedule { ... } + + /// 创建随访任务 + pub async fn create_follow_up_task(&self, patient_id: Uuid, ...) -> FollowUpTask { ... } + + /// 创建积分账户 + 初始积分 + pub async fn create_points_account(&self, patient_id: Uuid, initial_balance: i32) -> PointsAccount { ... } +} +``` + +2. 使用 `Default` trait + 覆盖模式: +```rust +#[derive(Default)] +pub struct PatientOverrides { + pub name: Option, + pub gender: Option, + pub birth_date: Option, + pub id_number: Option, + // ... +} +``` + +**验收标准**: +- 现有 `health_patient_tests.rs` 可用 fixture 重写(验证工厂函数可用) +- 每个工厂函数 ≤ 30 行代码 +- 创建的数据包含正确的 `tenant_id` 和 `created_by` + +--- + +### Task 3: 前端 MSW v2 初始配置 + +**涉及文件**: +- 新增: `apps/web/src/test/mocks/handlers.ts` +- 新增: `apps/web/src/test/mocks/server.ts` +- 修改: `apps/web/src/test/setup.ts` +- 修改: `apps/web/package.json`(添加 `msw` 依赖) + +**详细步骤**: + +1. 安装 MSW v2: `pnpm add -D msw` +2. 创建 `handlers.ts` — 初始仅包含 auth 基础 handler(登录/刷新): +```typescript +export const handlers = [ + http.post('/api/v1/auth/login', () => HttpResponse.json({ ... })), + http.post('/api/v1/auth/refresh', () => HttpResponse.json({ ... })), +] +``` +3. 创建 `server.ts`: +```typescript +import { setupServer } from 'msw/node' +export const server = setupServer(...handlers) +``` +4. 在 `setup.ts` 中启动/清理 MSW server: +```typescript +beforeAll(() => server.listen({ onUnhandledRequest: 'warn' })) +afterEach(() => server.resetHandlers()) +afterAll(() => server.close()) +``` + +**验收标准**: +- `pnpm test` 无报错 +- MSW server 正确拦截匹配的请求 +- 未匹配的请求产生 warning(不是 error) + +--- + +### Task 4: 前端覆盖率工具配置 + +**涉及文件**: +- 修改: `apps/web/package.json` +- 修改: `apps/web/vitest.config.ts` + +**详细步骤**: + +1. 安装: `pnpm add -D @vitest/coverage-v8` +2. 在 `vitest.config.ts` 添加 coverage 配置: +```typescript +test: { + coverage: { + provider: 'v8', + reporter: ['text', 'lcov'], + include: ['src/**/*.{ts,tsx}'], + exclude: ['src/test/**', 'src/**/*.test.*', 'src/**/*.d.ts'], + }, +} +``` +3. 添加 npm script: `"test:coverage": "vitest run --coverage"` + +**验收标准**: +- `pnpm test:coverage` 生成覆盖率报告 +- 输出包含各文件的行覆盖率百分比 + +--- + +### Task 5: 后端覆盖率工具安装验证 + +**涉及文件**: 无代码变更,仅环境安装 + +**详细步骤**: + +1. 安装 `cargo-llvm-cov`: `cargo install cargo-llvm-cov` +2. 验证基本功能: `cargo llvm-cov --workspace --text` +3. 编写增量检查脚本 `scripts/coverage-diff-check.py`(或 shell): + - 读取 `cargo llvm-cov --json` 输出 + - 通过 `git diff --name-only origin/main` 获取变更文件列表 + - 过滤出变更文件的覆盖率 + - 低于 80% 的文件列表退出码 1 + +**验收标准**: +- `cargo llvm-cov --workspace --text` 输出覆盖率报告 +- 增量检查脚本对模拟变更文件正确判断通过/不通过 + +--- + +### Task 6: CI workflow 初始创建 + +**涉及文件**: +- 新增: `.github/workflows/test.yml` + +**详细步骤**: + +1. 创建基础 CI workflow,包含: + - `backend-test` job: `cargo check` → `cargo test` → `cargo clippy` + - `frontend-test` job: `pnpm install` → `pnpm test:ci` → `pnpm build` + - PostgreSQL service 容器(仅 backend-test) + - 环境变量配置(测试用 JWT secret、数据库 URL) + +2. 配置触发条件: `on: pull_request` + `on: push: main` + +**验收标准**: +- Workflow 文件语法正确(`actionlint` 验证) +- 包含 spec 要求的完整验证链: check → test → clippy → build + +--- + +## Phase 1: 高风险 Service 测试(Week 1-2) + +> 共 6 个 service,约 47 个测试。所有测试使用 TestApp + TestFixture 模式。 + +### Task 7: points_service 测试(12 个) + +**涉及文件**: +- 新增: `crates/erp-server/tests/integration/health_points_tests.rs` +- 修改: `crates/erp-server/tests/integration.rs` + +**测试用例清单**: + +| 测试名 | 场景 | +|--------|------| +| `test_points_earn_sign_in` | 签到积分 — 首次签到成功,重复签到同天拒绝 | +| `test_points_earn_custom` | 自定义积分增加 | +| `test_points_consume_fifo_deduction` | FIFO 消费 — 先消耗最早的积分记录 | +| `test_points_consume_balance_insufficient` | 余额不足时返回错误 | +| `test_points_consume_exact_balance` | 消费金额等于余额 | +| `test_points_consume_partial` | 消费金额小于单条记录,正确拆分 | +| `test_points_account_create_on_first_earn` | 首次获取积分时自动创建账户 | +| `test_points_checkin_streak` | 连续签到奖励 | +| `test_points_order_create` | 积分兑换商品 — 创建订单 | +| `test_points_order_insufficient_cancel` | 积分不足时订单失败 | +| `test_points_transaction_history` | 交易记录查询 | +| `test_points_tenant_isolation` | 租户隔离 — A 的积分 B 看不到 | + +**验收标准**: +- `cargo test -p erp-server health_points` 全部通过 +- FIFO 消费逻辑有明确的事务性验证(回滚场景) + +--- + +### Task 8: dialysis_service 测试(8 个) + +**涉及文件**: +- 新增: `crates/erp-server/tests/integration/health_dialysis_tests.rs` + +**测试用例清单**: + +| 测试名 | 场景 | +|--------|------| +| `test_dialysis_create_basic` | 创建透析记录基本 CRUD | +| `test_dialysis_create_pii_encrypted` | PII 字段(病史、并发症)加密存储 | +| `test_dialysis_update_status_flow` | 状态流转: scheduled → in_progress → completed | +| `test_dialysis_list_by_patient` | 按患者 ID 过滤列表 | +| `test_dialysis_tenant_isolation` | 租户隔离 | +| `test_dialysis_version_conflict` | 乐观锁 — 旧版本更新拒绝 | +| `test_dialysis_soft_delete` | 软删除后不可见 | +| `test_dialysis_create_without_patient_returns_error` | 无效患者 ID 返回错误 | + +**验收标准**: +- `cargo test -p erp-server health_dialysis` 全部通过 +- PII 加密字段在数据库层面不可明文读取(验证密文格式) + +--- + +### Task 9: alert_engine 测试(8 个) + +**涉及文件**: +- 新增: `crates/erp-server/tests/integration/health_alert_tests.rs`(包含 engine + rule + service 测试) + +**测试用例清单**: + +| 测试名 | 场景 | +|--------|------| +| `test_alert_engine_evaluate_threshold_exceeded` | 体征超阈值触发告警 | +| `test_alert_engine_evaluate_threshold_normal` | 体征正常不触发 | +| `test_alert_engine_cooldown_prevents_duplicate` | cooldown 期内不重复触发 | +| `test_alert_engine_multiple_rules` | 多规则并行评估 | +| `test_alert_rule_crud` | 规则 CRUD 完整性 | +| `test_alert_rule_enable_disable` | 规则启用/禁用 | +| `test_alert_acknowledge` | 告警确认状态变更 | +| `test_alert_batch_acknowledge` | 批量确认 | + +**验收标准**: +- `cargo test -p erp-server health_alert` 全部通过 +- cooldown 逻辑精确到秒级验证 + +--- + +### Task 10: device_reading_service 测试(8 个) + +**涉及文件**: +- 新增: `crates/erp-server/tests/integration/health_device_reading_tests.rs` + +**测试用例清单**: + +| 测试名 | 场景 | +|--------|------| +| `test_device_reading_batch_insert` | 批量插入(模拟 50 条数据) | +| `test_device_reading_upsert_hourly` | 降采样聚合 — 同一小时的数据合并 | +| `test_device_reading_query_range` | 时间范围查询 | +| `test_device_reading_query_by_device_type` | 按设备类型过滤 | +| `test_device_reading_tenant_isolation` | 租户隔离 | +| `test_device_reading_invalid_data_rejected` | 无效数据(空值、超范围)拒绝 | +| `test_device_reading_latest_by_patient` | 获取患者最新读数 | +| `test_device_reading_sync_event_published` | 数据同步后事件发布 | + +**验收标准**: +- `cargo test -p erp-server health_device_reading` 全部通过 +- 降采样逻辑的聚合值(avg/min/max)精度正确 + +--- + +### Task 11: 扩展已有患者测试(+3 个) + +**涉及文件**: +- 修改: `crates/erp-server/tests/integration/health_patient_tests.rs` + +**新增测试**: + +| 测试名 | 场景 | +|--------|------| +| `test_patient_update_version_conflict` | 乐观锁冲突 | +| `test_patient_create_with_pii_encrypted` | PII 字段加密存储验证 | +| `test_patient_search_by_name` | 按名称搜索 | + +--- + +### Task 12: 扩展已有预约测试(+3 个) + +**涉及文件**: +- 修改: `crates/erp-server/tests/integration/health_appointment_tests.rs` + +**新增测试**: + +| 测试名 | 场景 | +|--------|------| +| `test_appointment_cas_exceeds_max_returns_error` | 预约超额拒绝 | +| `test_appointment_cancel_releases_slot` | 取消预约释放名额 | +| `test_appointment_status_flow` | 状态完整流转 | + +**Phase 1 验收**: +- 所有新增测试通过: `cargo test -p erp-server` +- 覆盖率提升约 20-25%(从当前基线) + +--- + +## Phase 2: 中风险 Service 测试(Week 3-4) + +> 共 5 个 service + erp-ai,约 47 个测试。 + +### Task 13: patient_service 完整测试(10 个) + +**涉及文件**: +- 修改: `crates/erp-server/tests/integration/health_patient_tests.rs` + +**新增测试**: 家庭成员管理 CRUD(4)、标签管理 CRUD(3)、健康摘要(2)、搜索/过滤(1) + +### Task 14: appointment_service 完整测试(8 个) + +**涉及文件**: +- 修改: `crates/erp-server/tests/integration/health_appointment_tests.rs` + +**新增测试**: 排班管理 CRUD(3)、预约时间冲突(1)、并发安全(2)、多租户隔离(2) + +### Task 15: follow_up_service 测试(8 个) + +**涉及文件**: +- 新增: `crates/erp-server/tests/integration/health_follow_up_tests.rs` + +**新增测试**: 状态机 6 种转换(6)、任务分配(1)、执行记录(1) + +### Task 16: consultation_service 测试(5 个) + +**涉及文件**: +- 新增: `crates/erp-server/tests/integration/health_consultation_tests.rs` + +**新增测试**: 会话 CRUD(3)、消息收发(1)、状态变更(1) + +### Task 17: doctor_service 测试(4 个) + +**涉及文件**: +- 新增: `crates/erp-server/tests/integration/health_doctor_tests.rs` + +**新增测试**: CRUD(3)、排班关联(1) + +### Task 18: erp-ai 基础测试(12 个) + +**涉及文件**: +- 新增: `crates/erp-server/tests/integration/ai_prompt_template_tests.rs` +- 新增: `crates/erp-server/tests/integration/ai_analysis_tests.rs` +- 新增: `crates/erp-server/tests/integration/ai_usage_tests.rs` + +**新增测试**: 提示模板 CRUD(4)、分析记录 CRUD(4)、使用统计(4) + +**Phase 2 验收**: +- `cargo test -p erp-server` 全部通过 +- 后端 service 层覆盖率 ≥ 55% + +--- + +## Phase 3: 低风险 Service + Handler + DTO(Week 5-6) + +> 共 14 个测试文件,约 71 个测试。 + +### Task 19-28: 低风险 Service 测试 + +每个 service 一个测试文件,遵循统一模式(CRUD + 租户隔离 + 软删除 + 乐观锁): + +| Task | Service | 文件 | 测试数 | +|------|---------|------|--------| +| 19 | article_service | health_article_tests.rs | 5 | +| 20 | article_category_service | (合并入 Task 19) | 4 | +| 21 | article_tag_service | (合并入 Task 19) | 3 | +| 22 | offline_event_service | health_offline_event_tests.rs | 4 | +| 23 | consent_service | health_consent_tests.rs | 3 | +| 24 | diagnosis_service | health_diagnosis_tests.rs | 3 | +| 25 | daily_monitoring_service | health_daily_monitoring_tests.rs | 4 | +| 26 | critical_value_threshold_service | (合并入 Task 9 alert 测试文件) | 3 | +| 27 | stats_service | health_stats_tests.rs | 5 | +| 28 | health_data_service | health_data_tests.rs | 8 | + +### Task 29: trend_service 集成测试补充(3 个) + +**涉及文件**: +- 新增: `crates/erp-server/tests/integration/health_trend_tests.rs` + +### Task 30: Handler 层 HTTP 测试(16 个) + +**目标**: 验证 HTTP 层的权限检查、请求解析、响应格式。 + +**涉及文件**: +- 新增: `crates/erp-server/tests/integration/health_handler_tests.rs` + +**测试模式**: 使用 `tower::ServiceExt` 调用 Axum Router,验证 HTTP 状态码和响应体格式。 + +**覆盖**: 每个核心 handler 1 个测试(患者/预约/随访/咨询/医生/文章/积分/体征/告警/透析等 16 个 handler) + +### Task 31: DTO 转换测试(10 个) + +**涉及文件**: +- 新增: `crates/erp-health/src/dto/dto_tests.rs`(内联 `#[cfg(test)]`) + +**测试内容**: 请求 DTO 反序列化(JSON → struct)+ 响应 DTO 序列化(struct → JSON)+ 边界值 + +### Task 32: CI 后端增量门禁上线 + +**涉及文件**: +- 修改: `.github/workflows/test.yml` + +**操作**: 在 backend-test job 中添加增量覆盖率检查步骤 + +**Phase 3 验收**: +- 后端全量测试覆盖率 ≥ 70% +- CI 增量门禁生效 + +--- + +## Phase 4: 前端补测(Week 7-9) + +### Task 33: API 层测试 — client.ts(12 个) + +**涉及文件**: +- 新增: `apps/web/src/api/client.test.ts` + +**测试内容**: token 主动刷新、401 被动刷新、并发请求队列去重、GET 缓存命中/过期、错误映射 + +**工具**: MSW mock `/api/v1/auth/refresh` 等端点 + +### Task 34: API 层测试 — health 模块(19 个) + +**涉及文件**: +- 新增: `apps/web/src/api/health/patients.test.ts` +- 新增: `apps/web/src/api/health/appointments.test.ts` +- 新增: `apps/web/src/api/health/other.test.ts` + +**测试内容**: 每个 API 文件的 CRUD 参数正确性、分页参数、错误映射 + +### Task 35: Store 层测试(17 个) + +**涉及文件**: +- 新增: `apps/web/src/stores/auth.test.ts` +- 新增: `apps/web/src/stores/plugin.test.ts` +- 新增: `apps/web/src/stores/health.test.ts` +- 新增: `apps/web/src/stores/message.test.ts` + +### Task 36: Hooks 测试(12 个) + +**涉及文件**: +- 新增: `apps/web/src/hooks/useApiRequest.test.ts` +- 新增: `apps/web/src/hooks/usePaginatedData.test.ts` +- 新增: `apps/web/src/hooks/usePermission.test.ts` + +### Task 37: 页面组件测试(19 个) + +**涉及文件**: +- 新增 8 个测试文件,对应 8 个核心健康模块页面 + +### Task 38: Playwright E2E — 患者管理(1 spec) + +**涉及文件**: +- 新增: `apps/web/e2e/health-patient.spec.ts` + +### Task 39: Playwright E2E — 预约/随访/咨询(3 spec) + +**涉及文件**: +- 新增: `apps/web/e2e/health-appointment.spec.ts` +- 新增: `apps/web/e2e/health-follow-up.spec.ts` +- 新增: `apps/web/e2e/health-consultation.spec.ts` + +### Task 40: Playwright E2E — 统计仪表板(1 spec) + +**涉及文件**: +- 新增: `apps/web/e2e/health-statistics.spec.ts` + +**Phase 4 验收**: +- 前端全量覆盖率 ≥ 60% +- 健康模块 E2E ≥ 5 个 spec + +--- + +## 验证与 CI 门禁 + +### Task 41: 后端增量门禁上线(Week 6 后) + +**操作**: 在 CI workflow 中启用后端增量覆盖率检查,新增/修改文件覆盖率 < 80% 时 PR 不允许合并。 + +### Task 42: 前端增量门禁上线(Week 9 后) + +**操作**: 同上,启用前端增量覆盖率检查。 + +### Task 43: 全量覆盖率验证 + 复盘(Week 12 后) + +**操作**: +1. 运行 `cargo llvm-cov --workspace --text` 记录后端全量覆盖率 +2. 运行 `pnpm test:coverage` 记录前端全量覆盖率 +3. 对比 spec 目标(后端 80% + 前端 60%),分析差距 +4. 输出复盘文档到 `docs/discussions/2026-05-XX-test-coverage-retrospective.md` +5. 制定后续补测计划(如有必要) + +--- + +## 执行原则 + +1. **每 Task 完成后立即提交** — 不积压,保持可追溯 +2. **先验证基础设施** — Phase 0 的 TestApp/TestFixture 必须先通过才能开始 Phase 1 +3. **测试文件独立于业务代码** — 新增测试文件不修改已有业务逻辑 +4. **cargo check/test 必须通过** — 每个 Task 完成后运行 `cargo test --workspace` 验证 +5. **Phase 间复盘** — 每个 Phase 结束后统计覆盖率,与目标对比,必要时调整