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.
11 KiB
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.ymlYAML 解析通过,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
五、遗留(未静默扩权)
- E2E 未真正进入 CI(D-23-D):需先决定 CI 中活栈的供给方式(docker-compose / service container / 种子数据与
E2E_PASSWORD注入)。 - pytest fixture 体系缺失(D-23-C):6 个 E2E 文件现为诚实占位;若要恢复执行,需先建
conftest.py与 fixture 工厂。 - INV-011 缺少显式守卫:
LocalEligibilityService全部方法以userId自限定、零 role 分支,佐证无独立服务端读取入口(仅经受权限控制的ReviewController审核队列),结构性满足;但无测试固化,列为待补。 - 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 一环仍缺。