Files
danyang-match/docs/Phase-23-V11-Recovery-Completion-Report-2026-09-18.md
WorkBuddy ae1bd2b734 test(phase23): KEEP-VERIFY — remove 44 pass shells + fake INV tests, add shell guard
Phase 23 "可信系统测试重建 (REFACTOR)" re-audit. STATUS claimed "10 E2E
files / ~80+ cases" and "INV-011~020 ✅"; measurement showed the opposite.

D-23-A (Critical): V11InvariantsTest's 14 cases asserted only Constants
strings. INV-016 "Banner target types" actually asserted ARTICLE_STATUS_*;
INV-017 "home 8 entries" declared local arrays then asserted
firstRow.length == 4 (tautology, never called the home service);
INV-018/019/020 used appointment/article/account status constants as
stand-ins for permission and data-scope checks. Rewritten to 12 cases:
4 real service-behavior guards (INV-013 via real CounselingService.getDetail
— Matchmaker sees null userRequestSummary, with an assigned-Counselor
counter-proof so global sanitization cannot fake a pass; INV-016 reads the
production ALLOWED_TARGET_TYPES whitelist and asserts no external/http/url/
web entry; INV-017 calls real getHome and pins all 8 codes + order;
INV-019 asserts content_editor reads null), plus assertBehaviorTestExists
turning "行为验证见 XxxTest" pointers into executable Class.forName contracts.

D-23-B (High): 44 `pass` shells across 6 files in tests/e2e (4 files were
100% shells). Gate forbids skip/pass shells. Rewritten in place as honest
placeholders with the full scenario lists preserved.

D-23-C (High, residual): the pytest E2E suite is not executable at all —
no conftest.py anywhere, ~15 fixtures (api_client/verified_user/
banner_factory/...) do not exist. Only run_v11_tests.py really runs
(16 cases, needs a live stack + E2E_PASSWORD).

D-23-D (High, residual, NOT fixed): CI does not chain E2E as the plan
requires. Needs an infrastructure decision on how CI provisions a live
stack; not implemented rather than written unverified.

D-23-E (Medium): the CI migration job claimed to "验证 migration SQL 语法"
but only checked file non-emptiness, while the real syntax/baseline checks
live in MigrationValidationTest (already run by the backend job). Replaced
with a real sequence check (naming / non-empty / unique versions / no
gaps); verified by extracting the exact inline script and running it.

New gate: scripts/check-no-empty-tests.py (AST scan, blocks pass-only and
unconditionally skipped tests) + a CI test-shell-guard job. Verified both
ways: 0 findings on the real tree, 2 precise findings on injected shells.

Negative validation: 3 injected production defects (external_url in the
banner whitelist, swapped home entry order, isMatchmaker returning the full
record) turned exactly inv016/inv017/inv013 red, 9 others green, zero
unrelated failures; md5sum -c confirmed byte-identical restore.

Full regression: 655 tests / 652 passed / 0 failures / 0 errors / 3 skipped
(baseline 657; -2 because 14 vacuous cases were merged into 12). The 3
skips carry explicit reasons (2x @EnabledIfEnvironmentVariable(CI=true),
1x @Disabled "需要完整数据库连接") — none is a shell.

Phase 23 is PARTIALLY closed: fake coverage is gone, but the UAT gate
requires the end-to-end chain to actually execute, so "失败为零可进入 UAT"
is NOT claimed.
2026-09-18 11:49:18 +08:00

11 KiB
Raw Permalink Blame 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-011020 ✅」;
  • 实测:44 个 pass 空壳 + 14 个常量字符串断言 + 整个 pytest 套件不可执行 + CI 无 E2E + 一个假迁移校验;
  • 现:假覆盖归零,INV-013/016/017/019 有经负向验证的真实行为守卫,并新增机械化的空壳阻断闸门。

建议:本 Phase 判定为部分关闭;在遗留项 1(E2E 进入 CI)决策完成前,不宜据此宣布「失败为零可进入 UAT」 —— 因为 UAT 前的 Gate 要求的是端到端链路被真实执行,而目前 E2E 一环仍缺。