Files
danyang-match/docs/Phase-23-V11-Recovery-Completion-Report-2026-09-18.md
T

165 lines
11 KiB
Markdown
Raw Normal View History

# Phase 23 — 可信系统测试重建(REFACTOR)KEEP-VERIFY 复验报告
- **日期**:2026-09-18
- **执行方式**:KEEP-VERIFY(历史 ✅ 不可信,逐条核对运行时路径与真实约束)
- **结果**:**部分关闭(PARTIAL CLOSED)** —— 核心假覆盖已清除并补真实守卫;CI 串联 E2E 一项因需基础设施决策,列为遗留
- **回归**:**655 tests / 0 failures / 0 errors / 3 skipped / BUILD SUCCESS**(起点 657)
---
## 一、Gate 要求的测试报告
计划 Phase 23 Gate:「**不允许 skip/pass 空壳;报告列出总数、通过、失败、跳过及原因,失败为零才进入 UAT。**」
| 项 | 数值 |
|---|---|
| 总数(后端 `mvn -o test`) | **655** |
| 通过 | **652** |
| 失败 | **0** |
| 错误 | **0** |
| 跳过 | **3** |
### 跳过明细及原因(均有显式声明,非空壳)
| 测试类 | 跳过数 | 原因(代码内原文) |
|---|---|---|
| `concurrent.ConcurrentTests` | 2 | `@EnabledIfEnvironmentVariable(named = "CI", matches = "true", disabledReason = "本地并发测试跳过")` —— 仅在 CI 环境执行的并发竞态用例 |
| `core.CoreE2ETest` | 1 | `@Disabled("需要完整数据库连接,仅验证 Spring Context 可加载")` |
> 数量说明:起点 657 → 655,净 **−2**。
> 原因:`V11InvariantsTest` 原有 **14 个**空转用例被重写合并为 **12 个**(INV-014 状态机 / INV-014 意向 / INV-015 三合一)。
> 合并后 10 个不变量(INV-011~020)**全部仍有对应用例**,其中 **4 个为真实服务行为验证**。
---
## 二、真实缺陷清单
### D-23-A(Critical):`V11InvariantsTest` 全部 14 个用例只断言常量字符串,且不变量归属错误
计划 Gate 明文禁止:「INV-001~020 必须测试真实服务行为,不能只断言常量字符串存在」。
实测原文件问题:
| 用例 | 声称验证 | 实际断言 | 判定 |
|---|---|---|---|
| INV-016「Banner 目标类型验证」 | Banner `target_type` 白名单 | `ARTICLE_STATUS_PUBLISHED/DRAFT`(**文章状态**) | ❌ 完全跑错域 |
| INV-017「首页 8 核心入口编码固定」 | 首页入口顺序 | 声明局部数组后 `assertEquals(4, firstRow.length)` | ❌ 对自己刚写的字面量做断言(重言式),**从未调用首页服务** |
| INV-018「服务站数据范围」 | 服务站数据范围 | `APPOINTMENT_STATUS_*`(预约状态) | ❌ 无权限/范围判定 |
| INV-019「内容编辑员零用户数据」 | 零用户数据权限 | `ARTICLE_STATUS_*`(文章状态) | ❌ 无权限判定 |
| INV-020「组织关系不授权」 | 组织关系权限 | `ACCOUNT_STATUS_*`(账号状态) | ❌ 无权限判定 |
| 其余 9 个 | 各自不变量 | 仅 `assertEquals(常量, 字面量)` / `assertNotNull(常量)` | ❌ 不触碰任何服务行为 |
**修复**:重写为 12 个用例 ——
- **真实服务行为(4 个)**
- `inv013MatchmakerCannotReadCounselingBody`:真实 `CounselingService.getDetail`;断言 Matchmaker 读到的 `userRequestSummary` 为 `null`,**并以 assigned Counselor 能读到正文作反证**(防止「全局脱敏」造成的假阳性),并断言无关角色返回 `null`。
- `inv016BannerTargetTypeWhitelistExcludesExternal`:反射读取生产 `HomeBannerOperatorController.ALLOWED_TARGET_TYPES`,断言含 8 个站内类型且**不含任何 external/http/url/web 外链类型**。
- `inv017QuickEntriesFixedOrder`:真实 `HomeApplicationService.getHome(userId)`,断言 8 个入口编码与顺序完全等于 AGENTS.md 规则 15 的顺序。
- `inv019ContentEditorHasNoUserDataPermission`:真实 `getDetail` 断言 `content_editor` 读到 `null`。
- **诚实常量口径(8 个)**:沿用 `CoreInvariantsTest` 既有模式,标题标注「常量口径 … 行为验证见 XxxTest」,并新增 `assertBehaviorTestExists(fqcn)` —— **用 `Class.forName` 断言被引用的真实测试类确实存在**,把文档指针变成可执行契约,防指针腐烂。
### D-23-B(High):`tests/e2e/` 下 44 个 `pass` 空壳(Gate 明令禁止)
| 文件 | 空壳数 / 总测试数 | 说明 |
|---|---|---|
| `local_eligibility_test.py` | 11 / 11 | 100% 空壳 |
| `counseling_permission_test.py` | 10 / 10 | 100% 空壳 |
| `activity_mutual_consent_test.py` | 8 / 8 | 100% 空壳 |
| `service_appointment_test.py` | 7 / 7 | 100% 空壳 |
| `home_v11_test.py` | 7 / 9 | 混合(另 2 个亦不可执行,见 D-23-C) |
| `counseling_v11_test.py` | 1 / — | 注释自述「在当前测试框架下简化验证」,函数体 `pass` |
**修复**:6 个文件全部改写为**不含任何测试函数的诚实占位**(pytest 收集 0 用例),并在文件头完整保留原场景清单(需求不丢失)+ 说明「后端已有哪个真实测试覆盖该不变量」,避免把「删除空壳」误读为「删除需求」。
### D-23-C(High):pytest E2E 套件整体不可执行
- 全仓库**无 `conftest.py`**,`api_client` / `verified_user` / `banner_factory` / `counselor_client` / `user_a` 等约 15 个 fixture **根本不存在** → 依赖它们的用例连收集都会报错。
- 唯一真实可执行的 E2E 是 `tests/e2e/run_v11_tests.py`(16 个用例,真实 HTTP 打 `localhost:8085`,需活栈 + `E2E_PASSWORD`)。
- 因此 STATUS 宣称的「10 个 E2E 文件、~80+ 测试用例」**从未产生过一次真实执行结果**。
### D-23-D(High,遗留未修):CI 未串联 E2E
计划要求「CI 串联迁移、backend test、admin type-check/build、miniapp type-check 和 **E2E**,任何失败阻断发布」。
实测 `.github/workflows/ci.yml` 只有 backend / admin-web / miniapp / migration 四个 job,**完全没有 E2E**。
**未在本轮修复**,原因:E2E 需要可用的活栈(后端 + PostgreSQL + 种子数据 + `E2E_PASSWORD`),在 CI 中如何供给属基础设施决策,且本地无法验证其正确性 —— 按「不静默扩权 / 不写无法验证的测试」纪律,列为遗留,交由决策后再实施。
本轮已改为接入**可验证**的 `test-shell-guard` job(见下)。
### D-23-E(Medium):CI 的 migration job 是名不副实的假校验
原 job 注释写「验证 migration SQL 语法(通过检查文件是否存在且非空)」,实际代码只 `if [ ! -s "$f" ]` 判非空 —— **没有任何语法校验**。
而真正的语法 / 命名 / 基线表校验在后端测试 `MigrationValidationTest` 中,随 backend job 的 `mvn verify` 已运行。
**修复**:改为该测试未覆盖的**序列校验**(命名合规 / 非空 / 版本号唯一 / 无缺号),并如实改写注释说明职责边界。
---
## 三、本轮新增的可验证产物
| 产物 | 作用 | 验证方式 |
|---|---|---|
| `backend/.../invariants/V11InvariantsTest.java`(重写) | INV-011~020 真实行为 + 指针契约 | 12 用例全绿;负向注入 3 处 → 精准 3 红 |
| `scripts/check-no-empty-tests.py` | AST 扫描,机械阻断 `pass` 空壳与无条件 `skip` | 真实目录 0 处;注入后精准报 2 处并 EXIT=1 |
| `.github/workflows/ci.yml` | 新增 `test-shell-guard` job;`migration` job 改为真实序列校验 | YAML 解析通过(5 job);内联校验脚本抽取实跑通过 |
---
## 四、验证证据
### 1. 负向验证(证明新守卫非假绿)
同时注入 3 处生产缺陷:
| 注入点 | 注入内容 | 预期变红用例 | 实际 |
|---|---|---|---|
| `HomeBannerOperatorController.ALLOWED_TARGET_TYPES` | 增加 `"external_url"` | `inv016` | ✅ 变红:`发现: [external_url]` |
| `HomeApplicationService.buildQuickEntries` | 交换 `classroom` / `premarital` 顺序 | `inv017` | ✅ 变红:实得 `[..., premarital, classroom, ...]` |
| `CounselingService.getDetail` 的 `isMatchmaker` 分支 | 改为返回全文 `attachFollowups(full)` | `inv013` | ✅ 变红:`expected: <null> but was: <不愿被红娘看到的求助正文>` |
结果:**12 用例中恰好 3 个变红,其余 9 个通过,零无关失败**;`md5sum -c` 确认 3 份生产文件还原一致。
### 2. 空壳守卫双向验证
- 扫描真实 `tests/`(14 个 py 文件):`OK: 未发现 skip/pass 空壳` EXIT=0
- 注入 `docstring + pass` 与 `@pytest.mark.skip` 各一:精准报出 2 处并 EXIT=1
### 3. CI 校验
- `ci.yml` YAML 解析通过,job 为 `backend / admin-web / miniapp / migration / test-shell-guard`
- 从 `ci.yml` 中**原样抽取**内联迁移校验脚本并执行:`OK: 43 个迁移文件,版本 1~43 连续且唯一`
### 4. 全量回归
`655 tests, Failures: 0, Errors: 0, Skipped: 3` — **BUILD SUCCESS**
---
## 五、遗留(未静默扩权)
1. **E2E 未真正进入 CI**(D-23-D):需先决定 CI 中活栈的供给方式(docker-compose / service container / 种子数据与 `E2E_PASSWORD` 注入)。
2. **pytest fixture 体系缺失**(D-23-C):6 个 E2E 文件现为诚实占位;若要恢复执行,需先建 `conftest.py` 与 fixture 工厂。
3. **INV-011 缺少显式守卫**:`LocalEligibilityService` 全部方法以 `userId` 自限定、零 role 分支,佐证无独立服务端读取入口(仅经受权限控制的 `ReviewController` 审核队列),**结构性满足**;但无测试固化,列为待补。
4. **E2E 场景待真实实现**:场景清单已完整保留在各占位文件头部(本地资格 11 项、互选 8 项、权限 10 项、预约 7 项、首页 11 项)。
---
## 六、环境事故记录(本机特有,务必注意)
在 `tests/e2e/` 下**删除任意一个文件**,会触发本机常驻监听进程**级联清空整个 `tests/` 目录**(不止 `tests/e2e`,`tests/` 根及其子目录一并消失)。
AGENTS.md 已记载该进程会清理 `refs/heads/` 下嵌套目录;本次实测表明**其作用范围不止于此**。
- 已发生 2 次,均通过 `git reset -q HEAD tests/ && git checkout -- tests/` 完整恢复(21 个文件全部还原,`git status --porcelain tests/` 为 0)。
- 结论:**本仓库禁止用 `rm` / `git rm` 删除 `tests/` 下文件**;需要移除内容时改用原地重写(Write/Edit)。本轮最终即采用该方式。
---
## 七、结论
Phase 23 的核心价值不在于新增了多少用例,而在于**清除假覆盖**:
- STATUS 曾宣称「10 个 E2E 文件 ~80+ 用例」「INV-011~020 ✅」;
- 实测:44 个 `pass` 空壳 + 14 个常量字符串断言 + 整个 pytest 套件不可执行 + CI 无 E2E + 一个假迁移校验;
- 现:假覆盖归零,INV-013/016/017/019 有经负向验证的真实行为守卫,并新增机械化的空壳阻断闸门。
**建议**:本 Phase 判定为**部分关闭**;在遗留项 1(E2E 进入 CI)决策完成前,**不宜据此宣布「失败为零可进入 UAT」** —— 因为 UAT 前的 Gate 要求的是端到端链路被真实执行,而目前 E2E 一环仍缺。