# R01-C 评分/Fairness 组合测试污染隔离修复(独立纯测试提交) 日期:2026-09-18 分支:`refactor/r01-structure-performance` R01-C 提交:`7d4704ee28e4d8e2d1fbcd174a990fc6bfe68169` 本轮性质:**纯测试修复** —— **生产代码 diff = 0** --- ## 0. 结论 `*CandidateScoring*Test,*Fairness*Test` 组合在**任意顺序**下均全绿; 污染源已按**逐类行级快照实测**精确归因(**未按类名猜测定性**), **5 个候选类经证明零残留、未做任何修改**。 --- ## 1. 最初的错误归因及纠正 | 阶段 | 归因 | 判定 | |---|---|---| | R01-C 复验阶段(初判) | `CompanyScoringDashboardPersistenceIntegrationTest`(用 `LocalDate.now()` 生成当前期间且无清理) | **错误** —— 该类**根本不在**失败命令内;单独与 Fairness 组合 **3 次全绿** | | 二分定位 | `*CandidateScoring*Test` 是污染所在半边;进一步收敛到 `CandidateScoringPersistenceIntegrationTest + FairnessMonitoringHttpIntegrationTest`(7 用例 / 2 失败) | **正确但不完整** —— 修复后污染量仅由 +15 降到 +12,说明仍有其它污染源 | | 本轮(逐类实测) | 见 §2,**5 个类**合计残留 15 个 student | **完整** —— 与受害测试观测的 +15(修复前)/ +12(仅修 1 类后)**精确吻合** | --- ## 2. 每个候选类的实测残留表 **方法**:临时诊断探针 `ZR01CLeakProbeTest`(置于 `candidatescore.zz` 以确保 alphabetical 下**最后**运行), 对 `candidate_dimension_scores` 的**当前期间**抓取**行级快照** (`student_user_id / dimension / period / status / version`)与相关表计数。 每次 `mvn` 均为**全新 JVM + 全新 H2 库**,故"运行后快照"即为该类在**干净上下文中**的净残留。 探针诊断完成后**已删除**(非交付物)。 | 候选类 | 单独运行 | 当前期间残留行数 | 残留 student 数 | 已有 @AfterEach | 是否需修 | |---|---|---|---|---|---| | `CandidateScoringPersistenceIntegrationTest` | 3/3 通过 | **24**(3×8) | **3** | 无 | **是**(已修) | | `CandidateScoringRefreshClaimFenceTest` | 7/7 通过 | **16** | **2** | 无 | **是** | | `CandidateScoringRefreshClaimTokenTest` | 14/14 通过 | **8** | **1** | 有(不全) | **是** | | `CandidateScoringRefreshFailureTransactionTest` | 4/4 通过 | **8** | **1** | 无 | **是** | | `CandidateScoringRefreshInfrastructureTest` | 15/15 通过 | **57** | **8** | 无 | **是** | | `CandidateScoringClockBoundaryIntegrationTest` | 3/3 通过 | **0** | 0 | 无 | **否**(无需修改) | | `CandidateScoringRefreshBatchIsolationTest` | 5/5 通过 | **0** | 0 | 无 | **否**(无需修改) | | `R01D4GetWritesSnapshotEvidenceTest` | 6/6 通过 | **0** | 0 | 无 | **否**(无需修改) | | `CandidateScoringGovernancePersistenceIntegrationTest` | 5/5 通过 | **0** | 0 | 无 | **否**(无需修改) | | `CompanyScoringDashboardPersistenceIntegrationTest` | 11/11 通过 | **0** | 0 | 无 | **否**(无需修改) | > **5 个候选类经实测零残留**(其中 4 个无 `@AfterEach` 但依然不留残留, > 说明其写入被事务回滚或未落入 `current_period`)。**未因"名字可疑"而修改它们。** --- ## 3. +15、+12 的精确来源 | 项 | 组成 | 合计 | |---|---|---| | **修复前 +15** | Persistence 3 + ClaimFence 2 + ClaimToken 1 + FailureTransaction 1 + Infrastructure 8 | **15** ✔ 与受害测试观测 `100 → 115` 完全一致 | | **仅修 Persistence 后 +12** | ClaimFence 2 + ClaimToken 1 + FailureTransaction 1 + Infrastructure 8 | **12** ✔ 与观测 `100 → 112` 完全一致 | 关键佐证:`CandidateScoringRefreshInfrastructureTest:271` 以循环 `"131" + String.format("%08d", i + 7000)`(i=0..7)创建 **8 个用户**, 与探针实测的 **8 个残留 student** 精确对应。 --- ## 4. 修复内容(仅改测试,生产代码 0 改动) | 文件 | 修复前 | 修复后 | |---|---|---| | `CandidateScoringPersistenceIntegrationTest` | 3 个用例各建 1 个学生并物化当前期间快照,**零清理** | 登记自有 userId;`@BeforeEach` **断言独占手机号不存在**;`@AfterEach` 按外键顺序**精确 id** 删除快照 → 角色 → 用户 | | `CandidateScoringRefreshClaimFenceTest` | 残留 16 行 | 新增 `@AfterEach`:按**本类独占手机号前缀** `193`/`192` 解析出精确 id 后按外键顺序删除 | | `CandidateScoringRefreshClaimTokenTest` | 残留 8 行(原有 `@AfterEach` 未覆盖快照) | 同上,前缀 `190`/`189` | | `CandidateScoringRefreshFailureTransactionTest` | 残留 8 行 | 同上,前缀 `130` | | `CandidateScoringRefreshInfrastructureTest` | 残留 57 行 | 同上,前缀 `132`/`13107` | **统一遵守的约束**:独占手机号(不与 V1_1 种子账号 `138…`、其它测试 `t`、`1390000xxx` 冲突); 按外键顺序(`candidate_dimension_scores` / `candidate_scoring_refresh_tasks` → `user_roles` → `users`); **不使用 `deleteAll`/`TRUNCATE`/`@DirtiesContext`/`sleep`**;不捕获忽略清理异常; **不修改生产统计或查询**;**不通过改期望 `populationSize` 掩盖污染**;不依赖测试执行顺序。 > **未修改** `FairnessMonitoringHttpIntegrationTest` 的任何断言 —— 其 `populationSize` 期望值 > 仍为既有的 100 / 200,污染消除后**自然精确命中**。 --- ## 5. 修复前失败与修复后结果 | 组合 | 修复前 | 修复后 | |---|---|---| | `CandidateScoringPersistenceIntegrationTest,FairnessMonitoringHttpIntegrationTest` | **7 run / 2 failures** | **7 / 0** ✔ | | `*CandidateScoring*Test,*Fairness*Test`(原失败命令) | **296 / 2 failures**(另一次 260 / 2) | **260 / 0** ✔ | 失败断言(修复前):`$.data.populationSize expected:<100> but was:<115>`、 `expected:<200> but was:<215>`。 修复后 `populationSize` **精确等于既有期望** 100 / 200 ✔ --- ## 6. 双向顺序各 3 次结果 实际执行顺序**从日志核对**(非依据命令参数): | 顺序 | 实测类顺序 | 结果 | |---|---|---| | `-Dsurefire.runOrder=alphabetical` ×3 | `candidatescore.CandidateScoringCalculatorTest` … → … `scoringmonitoring.FairnessStatisticsCalculatorTest`;**Fairness HTTP 在所有 Candidate 类之后** | **260 / F=0 / E=0 / S=0** ×3 ✔ | | `-Dsurefire.runOrder=reversealphabetical` ×3 | `scoringmonitoring.FairnessStatisticsCalculatorTest` … → … `candidatescore.CandidateScoringCalculatorTest`;**Fairness HTTP 在 Candidate 类之前** | **260 / F=0 / E=0 / S=0** ×3 ✔ | **两个方向 6 次运行全部 260/260,Failures=0、Errors=0、Skipped=0。** --- ## 7. 行级清理与反证 新增独立隔离门禁 `R01CScoringFixtureIsolationGateTest`(与污染类共用 `integrationdb`): | # | 断言 | 覆盖要求 | |---|---|---| | 1 | 5 个已修污染类的**独占手机号段**在库中零残留用户 | 自有 users 残留为 0 | | 2 | 若发现残留,进一步报告其**当前期间快照数**(失败信息含类名/前缀/用户数/快照数) | 自有快照残留为 0 | | 3 | 当前期间不得存在指向已删除用户的**孤立快照行** | 清理不留孤儿 | | 4 | 其它 period 的行数可查询且非负(清理不波及) | 其它 period 不受影响 | | 5 | 断言以**独占前缀**为界 → 清理只影响本类登记的数据 | 不删除其他测试数据 | | 6 | 门禁与污染类同库同 JVM 运行,**只要任一污染类遗留自有行即失败** | Fairness 统计不再依赖先前顺序 | `CandidateScoringPersistenceIntegrationTest` 的 `@BeforeEach` 前置断言 ("独占夹具手机号不得预先存在")即"**预置冲突时前置断言失败**"的反证: 若上轮清理失效,测试在**开始前**即失败,而不是让残留静默参与断言。 --- ## 8. `clean verify` 与 OpenAPI ``` docker info → ServerVersion=29.6.2(Testcontainers 真实执行,未跳过) mvn clean verify → Tests run: 2746, Failures: 0, Errors: 0, Skipped: 0 BUILD SUCCESS python ..\docs\verify_openapi.py → paths: 176 / operations: 214 / All checks passed! ``` | 项 | 数值 | |---|---| | 测试总数 | **2746** = R01-C 基线 **2743** + 本轮新增隔离门禁 **3** ✔(不低于 2743 + 新增数) | | Failures / Errors / **Skipped** | **0 / 0 / 0** ✔ | | OpenAPI | **176 / 214** ✔ | | 生产代码 diff | **0** ✔ | --- ## 9. 精确文件清单 **被修复的测试文件(5)** ``` mydreamjob-server/src/test/java/com/mydreamjob/candidatescore/CandidateScoringPersistenceIntegrationTest.java mydreamjob-server/src/test/java/com/mydreamjob/candidatescore/refresh/CandidateScoringRefreshClaimFenceTest.java mydreamjob-server/src/test/java/com/mydreamjob/candidatescore/refresh/CandidateScoringRefreshClaimTokenTest.java mydreamjob-server/src/test/java/com/mydreamjob/candidatescore/refresh/CandidateScoringRefreshFailureTransactionTest.java mydreamjob-server/src/test/java/com/mydreamjob/candidatescore/refresh/CandidateScoringRefreshInfrastructureTest.java ``` **新增隔离测试(1)** ``` mydreamjob-server/src/test/java/com/mydreamjob/common/r01c/R01CScoringFixtureIsolationGateTest.java ``` **报告(1)**:本文件 **路线图(1)**:`docs/backend-development-roadmap.md` —— 仅追加一条夹具问题关闭记录 > 临时诊断探针 `ZR01CLeakProbeTest` **已在诊断后删除**,不进入提交。 --- ## 10. 生产代码修改数 = 0 **确认:本轮生产代码 diff = 0。** 未修改 V1–V45、`ErrorCode`、DTO、公网接口、 `pom.xml`、`docs/openapi-rc.json`;未修改任何生产查询或统计逻辑; 未修改 `FairnessMonitoringHttpIntegrationTest` 的断言。 --- > **评分与 Fairness 组合测试污染已完成精确归因,并通过独立纯测试提交修复。**