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.
This commit is contained in:
+62
-11
@@ -79,15 +79,66 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Validate Flyway migrations
|
- name: Set up Python
|
||||||
working-directory: backend
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
# Phase 23 KEEP-VERIFY:本步骤原注释称「验证 migration SQL 语法」,实际只检查文件非空,
|
||||||
|
# 属名不副实的假校验(SQL 语法与基线表完整性由后端测试 MigrationValidationTest 覆盖,
|
||||||
|
# 该测试随 backend job 的 `mvn verify` 运行)。现改为该测试未覆盖的**序列**校验。
|
||||||
|
- name: 迁移序列校验(命名 / 非空 / 版本号唯一 / 无缺号)
|
||||||
run: |
|
run: |
|
||||||
# 验证 migration SQL 语法(通过检查文件是否存在且非空)
|
python3 - <<'PY'
|
||||||
find src/main/resources/db/migration -name "V*.sql" -type f | sort | while read f; do
|
import re, sys, pathlib, collections
|
||||||
echo "Validating: $f"
|
|
||||||
if [ ! -s "$f" ]; then
|
d = pathlib.Path("backend/src/main/resources/db/migration")
|
||||||
echo "ERROR: Empty migration file: $f"
|
files = sorted(d.glob("V*.sql"))
|
||||||
exit 1
|
if not files:
|
||||||
fi
|
sys.exit("ERROR: 未找到任何迁移文件")
|
||||||
done
|
|
||||||
echo "All migration files validated."
|
vers, bad_name, empty = [], [], []
|
||||||
|
for f in files:
|
||||||
|
m = re.match(r"V(\d+)__", f.name)
|
||||||
|
if not m:
|
||||||
|
bad_name.append(f.name)
|
||||||
|
continue
|
||||||
|
if f.stat().st_size == 0:
|
||||||
|
empty.append(f.name)
|
||||||
|
vers.append(int(m.group(1)))
|
||||||
|
|
||||||
|
problems = []
|
||||||
|
if bad_name:
|
||||||
|
problems.append(f"迁移文件命名不合规(应为 V<序号>__<描述>.sql): {bad_name}")
|
||||||
|
if empty:
|
||||||
|
problems.append(f"空迁移文件: {empty}")
|
||||||
|
dups = sorted(v for v, c in collections.Counter(vers).items() if c > 1)
|
||||||
|
if dups:
|
||||||
|
problems.append(f"迁移版本号重复: {dups}")
|
||||||
|
gaps = [v for v in range(min(vers), max(vers) + 1) if v not in set(vers)]
|
||||||
|
if gaps:
|
||||||
|
problems.append(f"迁移版本号缺号: {gaps}")
|
||||||
|
|
||||||
|
if problems:
|
||||||
|
for p in problems:
|
||||||
|
print("ERROR: " + p, file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"OK: {len(files)} 个迁移文件,版本 {min(vers)}~{max(vers)} 连续且唯一")
|
||||||
|
PY
|
||||||
|
|
||||||
|
test-shell-guard:
|
||||||
|
name: Test Shell Guard (Phase 23 Gate)
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
# Phase 23 Gate:「不允许 skip/pass 空壳」。
|
||||||
|
# 机械阻断空壳回归 —— 详见 scripts/check-no-empty-tests.py。
|
||||||
|
- name: 禁止 skip/pass 空壳测试
|
||||||
|
run: python3 scripts/check-no-empty-tests.py tests
|
||||||
|
|||||||
+194
-51
@@ -1,29 +1,83 @@
|
|||||||
package com.danyang.match.platform.invariants;
|
package com.danyang.match.platform.invariants;
|
||||||
|
|
||||||
|
import com.danyang.match.platform.banner.controller.HomeBannerOperatorController;
|
||||||
|
import com.danyang.match.platform.counseling.entity.CounselingServiceRecord;
|
||||||
|
import com.danyang.match.platform.counseling.service.CounselingService;
|
||||||
|
import com.danyang.match.platform.home.dto.HomeDTO;
|
||||||
|
import com.danyang.match.platform.home.service.HomeApplicationService;
|
||||||
import com.danyang.match.platform.shared.constant.Constants;
|
import com.danyang.match.platform.shared.constant.Constants;
|
||||||
import org.junit.jupiter.api.*;
|
import com.danyang.match.platform.user.entity.User;
|
||||||
|
import com.danyang.match.platform.user.mapper.UserMapper;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.lang.reflect.Field;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* V1.1 不变量测试 — INV-011 ~ INV-020
|
* V1.1 不变量测试 — INV-011 ~ INV-020
|
||||||
|
*
|
||||||
|
* <p><b>Phase 23 KEEP-VERIFY 重写说明(原文件为假测试)</b></p>
|
||||||
|
* 原文件 10 个测试全部只断言 {@code Constants} 里的字符串存在,不触碰任何服务行为,
|
||||||
|
* 其中三处属于明确错误:
|
||||||
|
* <ul>
|
||||||
|
* <li>INV-016「Banner 目标类型」实际断言的是 {@code ARTICLE_STATUS_*}(文章状态常量),
|
||||||
|
* 与 Banner {@code target_type} 毫无关系;</li>
|
||||||
|
* <li>INV-017「首页 8 入口」声明两个局部数组后断言 {@code firstRow.length == 4},
|
||||||
|
* 是对自己刚写的字面量做断言的重言式,从未调用首页服务;</li>
|
||||||
|
* <li>INV-018/019/020 分别用预约状态、文章状态、账号状态常量充当「数据范围 / 零用户数据 /
|
||||||
|
* 组织关系」的验证,同样不触及任何权限判定。</li>
|
||||||
|
* </ul>
|
||||||
|
* 计划 Phase 23 Gate 明文要求:<b>INV-001~020 必须测试真实服务行为,不能只断言常量字符串存在。</b>
|
||||||
|
*
|
||||||
|
* <p>现按「能在本文件内做真行为验证的就地做,已被其它真实测试类覆盖的给出可校验的指针」重写:
|
||||||
|
* <ul>
|
||||||
|
* <li><b>本文件内真实行为</b>:INV-013({@code CounselingService.getDetail} 脱敏与越权返回 null)、
|
||||||
|
* INV-016(反射读取生产 {@code ALLOWED_TARGET_TYPES} 白名单,断言不含外链类型)、
|
||||||
|
* INV-017(真实 {@code getHome(userId)} 的 8 核心入口与固定顺序);</li>
|
||||||
|
* <li><b>指针 + 常量口径</b>:其余不变量的行为验证在各自真实测试类中,
|
||||||
|
* 本文件用 {@link #assertBehaviorTestExists} 断言该测试类确实存在,防止指针腐烂。</li>
|
||||||
|
* </ul>
|
||||||
*/
|
*/
|
||||||
|
@SpringBootTest
|
||||||
|
@ActiveProfiles("test")
|
||||||
|
@Transactional
|
||||||
class V11InvariantsTest {
|
class V11InvariantsTest {
|
||||||
|
|
||||||
|
@Autowired private HomeApplicationService homeApplicationService;
|
||||||
|
@Autowired private UserMapper userMapper;
|
||||||
|
@Autowired private CounselingService counselingService;
|
||||||
|
|
||||||
|
private static final String USER = "u-inv-v11";
|
||||||
|
private static final String COUNSELOR = "op-inv-counselor";
|
||||||
|
private static final String MATCHMAKER = "op-inv-matchmaker";
|
||||||
|
private static final String CONTENT_EDITOR = "op-inv-content-editor";
|
||||||
|
|
||||||
|
// ==================== INV-011 ====================
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("INV-011: 本地资格佐证访问需要特定权限")
|
@DisplayName("常量口径 INV-011: 本地资格类型取值固定(行为验证见 LocalEligibilityServiceTest / LocalEligibilityStatusVocabularyGuardTest)")
|
||||||
void testLocalEvidenceRequiresPermission() {
|
void inv011LocalEligibilityVocabulary() {
|
||||||
assertEquals("local_eligibility", Constants.NOTIFICATION_TYPE_LOCAL_ELIGIBILITY);
|
assertBehaviorTestExists("com.danyang.match.platform.local_eligibility.LocalEligibilityServiceTest");
|
||||||
assertNotNull(Constants.LOCAL_ELIGIBILITY_TYPE_HUKOU);
|
assertBehaviorTestExists("com.danyang.match.platform.account.LocalEligibilityStatusVocabularyGuardTest");
|
||||||
|
assertEquals("household_registration", Constants.LOCAL_ELIGIBILITY_TYPE_HUKOU);
|
||||||
assertNotNull(Constants.LOCAL_ELIGIBILITY_TYPE_WORK);
|
assertNotNull(Constants.LOCAL_ELIGIBILITY_TYPE_WORK);
|
||||||
assertNotNull(Constants.LOCAL_ELIGIBILITY_TYPE_RESIDENCE);
|
assertNotNull(Constants.LOCAL_ELIGIBILITY_TYPE_RESIDENCE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== INV-012 ====================
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("INV-012: Counselor咨询状态机验证")
|
@DisplayName("常量口径 INV-012: 咨询服务状态机取值固定(行为验证见 CounselingPhase13ATest 的 4 个 INV-012 用例)")
|
||||||
void testCounselorStatusMachine() {
|
void inv012CounselingStatusMachine() {
|
||||||
|
assertBehaviorTestExists("com.danyang.match.platform.counseling.CounselingPhase13ATest");
|
||||||
assertEquals("pending", Constants.COUNSELING_STATUS_PENDING);
|
assertEquals("pending", Constants.COUNSELING_STATUS_PENDING);
|
||||||
assertEquals("assigned", Constants.COUNSELING_STATUS_ASSIGNED);
|
assertEquals("assigned", Constants.COUNSELING_STATUS_ASSIGNED);
|
||||||
assertEquals("scheduled", Constants.COUNSELING_STATUS_SCHEDULED);
|
assertEquals("scheduled", Constants.COUNSELING_STATUS_SCHEDULED);
|
||||||
@@ -31,27 +85,56 @@ class V11InvariantsTest {
|
|||||||
assertEquals("completed", Constants.COUNSELING_STATUS_COMPLETED);
|
assertEquals("completed", Constants.COUNSELING_STATUS_COMPLETED);
|
||||||
assertEquals("cancelled", Constants.COUNSELING_STATUS_CANCELLED);
|
assertEquals("cancelled", Constants.COUNSELING_STATUS_CANCELLED);
|
||||||
assertEquals("closed", Constants.COUNSELING_STATUS_CLOSED);
|
assertEquals("closed", Constants.COUNSELING_STATUS_CLOSED);
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
@DisplayName("INV-012: 咨询类型枚举完整")
|
|
||||||
void testCounselingTypeEnums() {
|
|
||||||
assertEquals("relationship_guidance", Constants.COUNSELING_TYPE_RELATIONSHIP_GUIDANCE);
|
assertEquals("relationship_guidance", Constants.COUNSELING_TYPE_RELATIONSHIP_GUIDANCE);
|
||||||
assertEquals("emotional_support", Constants.COUNSELING_TYPE_EMOTIONAL_SUPPORT);
|
assertEquals("emotional_support", Constants.COUNSELING_TYPE_EMOTIONAL_SUPPORT);
|
||||||
assertEquals("premarital_guidance", Constants.COUNSELING_TYPE_PREMARITAL_GUIDANCE);
|
assertEquals("premarital_guidance", Constants.COUNSELING_TYPE_PREMARITAL_GUIDANCE);
|
||||||
assertEquals("anti_fraud_guidance", Constants.COUNSELING_TYPE_ANTI_FRAUD_GUIDANCE);
|
assertEquals("anti_fraud_guidance", Constants.COUNSELING_TYPE_ANTI_FRAUD_GUIDANCE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
// ==================== INV-013(真实服务行为) ====================
|
||||||
@DisplayName("INV-013: 咨询服务业务边界")
|
|
||||||
void testCounselingBusinessBoundary() {
|
|
||||||
assertEquals("counseling", Constants.NOTIFICATION_TYPE_COUNSELING);
|
|
||||||
assertEquals("premarital", Constants.NOTIFICATION_TYPE_PREMARITAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("INV-014: 活动互选状态机验证")
|
@DisplayName("INV-013: Matchmaker 只读到脱敏版(无 user_request_summary);无关角色读到 null")
|
||||||
void testActivityMutualStateMachine() {
|
void inv013MatchmakerCannotReadCounselingBody() {
|
||||||
|
CounselingServiceRecord svc = counselingService.apply(
|
||||||
|
USER, Constants.COUNSELING_TYPE_RELATIONSHIP_GUIDANCE, "不愿被红娘看到的求助正文",
|
||||||
|
"user_self", "online", "工作日晚间", null, null);
|
||||||
|
assertNotNull(svc, "申请应成功");
|
||||||
|
String serviceId = svc.getId();
|
||||||
|
assertNotNull(serviceId);
|
||||||
|
|
||||||
|
// 申请者本人:完整视图(含正文)
|
||||||
|
CounselingServiceRecord mine = counselingService.getDetail(USER, "user", serviceId);
|
||||||
|
assertNotNull(mine, "申请者本人应能看到自己的服务");
|
||||||
|
assertEquals("不愿被红娘看到的求助正文", mine.getUserRequestSummary(),
|
||||||
|
"申请者本人应能读到自己填写的求助正文");
|
||||||
|
|
||||||
|
// INV-013 核心:Matchmaker / 普通 operator 只能看脱敏版
|
||||||
|
CounselingServiceRecord forMatchmaker =
|
||||||
|
counselingService.getDetail(MATCHMAKER, "matchmaker", serviceId);
|
||||||
|
assertNotNull(forMatchmaker, "Matchmaker 可见服务条目本身(脱敏版)");
|
||||||
|
assertNull(forMatchmaker.getUserRequestSummary(),
|
||||||
|
"INV-013: Matchmaker 不得读取咨询正文 user_request_summary");
|
||||||
|
|
||||||
|
// 兜底:与咨询无关的角色(内容编辑员)一律无权限
|
||||||
|
assertNull(counselingService.getDetail(CONTENT_EDITOR, "content_editor", serviceId),
|
||||||
|
"INV-013: 与咨询无关的角色不得读取咨询服务详情");
|
||||||
|
|
||||||
|
// assigned Counselor 属完整视图,用于确认上面的脱敏不是「全局脱敏」造成的假阳性
|
||||||
|
counselingService.assign(serviceId, COUNSELOR);
|
||||||
|
CounselingServiceRecord forAssigned =
|
||||||
|
counselingService.getDetail(COUNSELOR, "counselor", serviceId);
|
||||||
|
assertNotNull(forAssigned);
|
||||||
|
assertEquals("不愿被红娘看到的求助正文", forAssigned.getUserRequestSummary(),
|
||||||
|
"assigned Counselor 应能读到正文,用于反证 Matchmaker 的脱敏是真实生效的");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== INV-014 / INV-015 ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("常量口径 INV-014/015: 活动互选状态取值固定(行为验证见 ActivityMutualConsentServiceTest 的双确认 Gate 用例)")
|
||||||
|
void inv014015MutualConsentVocabulary() {
|
||||||
|
assertBehaviorTestExists("com.danyang.match.platform.activity_mutual.ActivityMutualConsentServiceTest");
|
||||||
assertEquals("pending", Constants.MUTUAL_CONSENT_STATUS_PENDING);
|
assertEquals("pending", Constants.MUTUAL_CONSENT_STATUS_PENDING);
|
||||||
assertEquals("accepted", Constants.MUTUAL_CONSENT_STATUS_ACCEPTED);
|
assertEquals("accepted", Constants.MUTUAL_CONSENT_STATUS_ACCEPTED);
|
||||||
assertEquals("declined", Constants.MUTUAL_CONSENT_STATUS_DECLINED);
|
assertEquals("declined", Constants.MUTUAL_CONSENT_STATUS_DECLINED);
|
||||||
@@ -59,62 +142,108 @@ class V11InvariantsTest {
|
|||||||
assertEquals("mutual", Constants.ACTIVITY_INTEREST_STATUS_MUTUAL);
|
assertEquals("mutual", Constants.ACTIVITY_INTEREST_STATUS_MUTUAL);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
// ==================== INV-016(真实生产数据校验) ====================
|
||||||
@DisplayName("INV-014: 活动意向不自动创建Match")
|
|
||||||
void testActivityInterestNoAutoMatch() {
|
|
||||||
assertEquals("submitted", Constants.MATCHMAKER_STATUS_SUBMITTED);
|
|
||||||
assertEquals("in_service", Constants.MATCHMAKER_STATUS_IN_SERVICE);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("INV-015: 双方确认后才有沟通权限")
|
@DisplayName("INV-016: Banner 跳转类型白名单不含任何外链类型(读取生产 ALLOWED_TARGET_TYPES)")
|
||||||
void testBothConfirmBeforeChat() {
|
@SuppressWarnings("unchecked")
|
||||||
assertEquals("pending", Constants.MUTUAL_CONSENT_STATUS_PENDING);
|
void inv016BannerTargetTypeWhitelistExcludesExternal() throws Exception {
|
||||||
assertEquals("accepted", Constants.MUTUAL_CONSENT_STATUS_ACCEPTED);
|
Field f = HomeBannerOperatorController.class.getDeclaredField("ALLOWED_TARGET_TYPES");
|
||||||
|
f.setAccessible(true);
|
||||||
|
Set<String> allowed = (Set<String>) f.get(null);
|
||||||
|
|
||||||
|
assertNotNull(allowed, "ALLOWED_TARGET_TYPES 应存在");
|
||||||
|
// AGENTS.md 规则 16:target_type 仅限以下站内类型
|
||||||
|
assertTrue(allowed.containsAll(List.of(
|
||||||
|
"activity", "matchmaker", "counseling", "premarital",
|
||||||
|
"service_location", "article", "internal_page", "none")),
|
||||||
|
"Banner 白名单应包含全部 8 个站内跳转类型,实际为: " + allowed);
|
||||||
|
// 禁外链:任何形如 external / http / url / web 的类型都不得出现
|
||||||
|
List<String> externalish = allowed.stream()
|
||||||
|
.filter(t -> t.contains("external") || t.contains("http")
|
||||||
|
|| t.contains("url") || t.contains("web"))
|
||||||
|
.toList();
|
||||||
|
assertTrue(externalish.isEmpty(),
|
||||||
|
"INV-016: Banner 白名单不得包含外链类型,发现: " + externalish);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
// ==================== INV-017(真实服务行为) ====================
|
||||||
@DisplayName("INV-016: Banner目标类型验证")
|
|
||||||
void testBannerTargetTypes() {
|
|
||||||
assertEquals("published", Constants.ARTICLE_STATUS_PUBLISHED);
|
|
||||||
assertEquals("draft", Constants.ARTICLE_STATUS_DRAFT);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("INV-017: 首页8核心入口编码固定")
|
@DisplayName("INV-017: 首页 8 核心入口固定且顺序不可更改(真实 getHome)")
|
||||||
void testQuickEntryCodesFixed() {
|
void inv017QuickEntriesFixedOrder() {
|
||||||
String[] firstRow = {"find_object", "matchmaker", "activities", "counseling"};
|
User u = new User();
|
||||||
String[] secondRow = {"classroom", "premarital", "anti_fraud", "service_station"};
|
u.setWechatOpenid("inv017_" + System.nanoTime());
|
||||||
assertEquals(4, firstRow.length);
|
u.setAccountStatus(Constants.ACCOUNT_STATUS_NORMAL);
|
||||||
assertEquals(4, secondRow.length);
|
u.setRelationshipStatus(Constants.RELATIONSHIP_STATUS_SEARCHING);
|
||||||
assertEquals(8, firstRow.length + secondRow.length);
|
u.setRecommendationEnabled(true);
|
||||||
|
userMapper.insert(u);
|
||||||
|
|
||||||
|
HomeDTO dto = homeApplicationService.getHome(u.getId());
|
||||||
|
assertNotNull(dto, "首页聚合响应不应为 null");
|
||||||
|
List<HomeDTO.QuickEntry> entries = dto.getQuickEntries();
|
||||||
|
assertNotNull(entries, "quickEntries 不应为 null");
|
||||||
|
|
||||||
|
List<String> codes = entries.stream().map(HomeDTO.QuickEntry::getCode).toList();
|
||||||
|
// AGENTS.md 规则 15:第一排 找对象/公益红娘/公益活动/婚恋咨询;第二排 婚恋课堂/婚前辅导/婚恋反诈/善缘服务站
|
||||||
|
assertEquals(
|
||||||
|
List.of("find_object", "matchmaker", "activities", "counseling",
|
||||||
|
"classroom", "premarital", "anti_fraud", "service_station"),
|
||||||
|
codes,
|
||||||
|
"INV-017: 首页 8 核心入口的编码与顺序必须完全固定(实得 " + codes + ")");
|
||||||
|
assertEquals(8, codes.size(), "INV-017: 核心入口必须恰好 8 个");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== INV-018 ====================
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("INV-018: 服务站运营人员数据范围")
|
@DisplayName("常量口径 INV-018: 服务预约状态取值固定(行为验证见 ServiceAppointmentDataScopeTest / ServiceAppointmentOwnerGuardTest)")
|
||||||
void testStationOperatorDataScope() {
|
void inv018AppointmentStatusVocabulary() {
|
||||||
|
assertBehaviorTestExists("com.danyang.match.platform.service_appointment.ServiceAppointmentDataScopeTest");
|
||||||
|
assertBehaviorTestExists("com.danyang.match.platform.service_appointment.ServiceAppointmentOwnerGuardTest");
|
||||||
assertEquals("submitted", Constants.APPOINTMENT_STATUS_SUBMITTED);
|
assertEquals("submitted", Constants.APPOINTMENT_STATUS_SUBMITTED);
|
||||||
assertEquals("confirmed", Constants.APPOINTMENT_STATUS_CONFIRMED);
|
assertEquals("confirmed", Constants.APPOINTMENT_STATUS_CONFIRMED);
|
||||||
assertEquals("completed", Constants.APPOINTMENT_STATUS_COMPLETED);
|
assertEquals("completed", Constants.APPOINTMENT_STATUS_COMPLETED);
|
||||||
assertEquals("cancelled", Constants.APPOINTMENT_STATUS_CANCELLED);
|
assertEquals("cancelled", Constants.APPOINTMENT_STATUS_CANCELLED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== INV-019 ====================
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("INV-019: 内容编辑员零用户数据权限")
|
@DisplayName("常量口径 INV-019: 内容状态取值固定(行为验证见 V11PermissionMatrixContractTest 的角色基线用例)")
|
||||||
void testContentEditorZeroUserAccess() {
|
void inv019ContentStatusVocabulary() {
|
||||||
|
assertBehaviorTestExists("com.danyang.match.platform.security.V11PermissionMatrixContractTest");
|
||||||
|
assertEquals("draft", Constants.ARTICLE_STATUS_DRAFT);
|
||||||
assertEquals("published", Constants.ARTICLE_STATUS_PUBLISHED);
|
assertEquals("published", Constants.ARTICLE_STATUS_PUBLISHED);
|
||||||
assertEquals("offline", Constants.ARTICLE_STATUS_OFFLINE);
|
assertEquals("offline", Constants.ARTICLE_STATUS_OFFLINE);
|
||||||
assertEquals("scheduled", Constants.ARTICLE_STATUS_SCHEDULED);
|
assertEquals("scheduled", Constants.ARTICLE_STATUS_SCHEDULED);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("INV-020: 组织关系不自动授予数据权限")
|
@DisplayName("INV-019: 内容编辑员角色不得持有任何用户婚恋数据权限(真实角色-权限基线)")
|
||||||
void testOrgRelationshipNoAutoPermission() {
|
void inv019ContentEditorHasNoUserDataPermission() {
|
||||||
|
// 与 INV-013 相互印证:content_editor 既读不到咨询详情,也不在咨询可见角色集合内
|
||||||
|
CounselingServiceRecord svc = counselingService.apply(
|
||||||
|
USER, Constants.COUNSELING_TYPE_EMOTIONAL_SUPPORT, "内容编辑员不应看到的正文",
|
||||||
|
"user_self", "online", "工作日晚间", null, null);
|
||||||
|
assertNull(counselingService.getDetail(CONTENT_EDITOR, "content_editor", svc.getId()),
|
||||||
|
"INV-019: 内容编辑员不得读取用户婚恋咨询数据");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== INV-020 ====================
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("常量口径 INV-020: 账号状态取值固定(行为验证见 AuthServiceAccountClosedGuardTest / V11MethodSecurityMatrixTest)")
|
||||||
|
void inv020AccountStatusVocabulary() {
|
||||||
|
assertBehaviorTestExists("com.danyang.match.platform.auth.AuthServiceAccountClosedGuardTest");
|
||||||
|
assertBehaviorTestExists("com.danyang.match.platform.security.V11MethodSecurityMatrixTest");
|
||||||
assertEquals("normal", Constants.ACCOUNT_STATUS_NORMAL);
|
assertEquals("normal", Constants.ACCOUNT_STATUS_NORMAL);
|
||||||
assertEquals("banned", Constants.ACCOUNT_STATUS_BANNED);
|
assertEquals("banned", Constants.ACCOUNT_STATUS_BANNED);
|
||||||
assertEquals("suspended", Constants.ACCOUNT_STATUS_SUSPENDED);
|
assertEquals("suspended", Constants.ACCOUNT_STATUS_SUSPENDED);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 常量完整性(回归保护) ====================
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@DisplayName("V1.1常量完整性: 所有新增枚举值存在且非空")
|
@DisplayName("V1.1常量完整性: 所有新增枚举值存在且非空")
|
||||||
void testV11ConstantsExist() {
|
void testV11ConstantsExist() {
|
||||||
@@ -160,6 +289,20 @@ class V11InvariantsTest {
|
|||||||
Constants.COUNSELING_STATUS_CLOSED
|
Constants.COUNSELING_STATUS_CLOSED
|
||||||
};
|
};
|
||||||
assertEquals(7, statuses.length);
|
assertEquals(7, statuses.length);
|
||||||
assertEquals(7, (long) Arrays.stream(statuses).distinct().count());
|
assertEquals(7, java.util.Arrays.stream(statuses).distinct().count(),
|
||||||
|
"咨询状态常量不得出现重复值(手工维护易复制粘贴出错)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 辅助 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 断言「行为验证见 XxxTest」指向的测试类真实存在。
|
||||||
|
*
|
||||||
|
* <p>本文件曾用常量断言冒充不变量验证,且未指明真正的验证位置。
|
||||||
|
* 这里把指针变成可执行的契约:一旦被引用的测试类被重命名或删除,本用例立即失败。
|
||||||
|
*/
|
||||||
|
private void assertBehaviorTestExists(String fqcn) {
|
||||||
|
assertDoesNotThrow(() -> Class.forName(fqcn),
|
||||||
|
"被引用的行为验证测试类必须存在: " + fqcn);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -179,7 +179,8 @@
|
|||||||
| Phase 19 统一运营后台 + 用户端公益服务页面复验 | 已完成(2026-09-17) | 见 `Phase-19-V11-Recovery-Completion-Report-2026-09-17.md`;修复 **25 处真实缺陷**(**D20 服务站公告能力整条不可达:Service + V042 表都在、全仓零端点**;**D21 服务站 `create/update` 越权写:类级 `service_location.view` 兜底,`service_location.manage` 是从未使用的死权限**;D22 该控制器未纳入权限覆盖守卫;**D2/D3 Banner 校验 500 + 软删 Banner 仍上首页**;**D5/D6 Content 发布死路 + 分类停用不可恢复**;D10~D14 ServiceProgram 审批死路/草稿泄露/成员数恒 0/退出未实现/撞唯一键;D9/D23 测试库缺 `content_categories` 与 `service_location_announcements` → 零覆盖;D18 预约 IDOR;D24 迁移基线未跟进 V042);补 V042 公告表 + 运营端五端点 + 用户端只读端点 + 后台公告管理 UI + 小程序 U50 公告区块;新增 3 组守卫(24+5+5 用例)并把服务站运营端收入权限覆盖扫描;负向验证 3 注入 → 4 用例精准变红;后端 **602 tests 全绿**;admin-web `vue-tsc`/`vite build` 双绿;小程序 `tsc` EXIT=0;ui-tests 14/14;**Phase 20 尚未获准** |
|
| Phase 19 统一运营后台 + 用户端公益服务页面复验 | 已完成(2026-09-17) | 见 `Phase-19-V11-Recovery-Completion-Report-2026-09-17.md`;修复 **25 处真实缺陷**(**D20 服务站公告能力整条不可达:Service + V042 表都在、全仓零端点**;**D21 服务站 `create/update` 越权写:类级 `service_location.view` 兜底,`service_location.manage` 是从未使用的死权限**;D22 该控制器未纳入权限覆盖守卫;**D2/D3 Banner 校验 500 + 软删 Banner 仍上首页**;**D5/D6 Content 发布死路 + 分类停用不可恢复**;D10~D14 ServiceProgram 审批死路/草稿泄露/成员数恒 0/退出未实现/撞唯一键;D9/D23 测试库缺 `content_categories` 与 `service_location_announcements` → 零覆盖;D18 预约 IDOR;D24 迁移基线未跟进 V042);补 V042 公告表 + 运营端五端点 + 用户端只读端点 + 后台公告管理 UI + 小程序 U50 公告区块;新增 3 组守卫(24+5+5 用例)并把服务站运营端收入权限覆盖扫描;负向验证 3 注入 → 4 用例精准变红;后端 **602 tests 全绿**;admin-web `vue-tsc`/`vite build` 双绿;小程序 `tsc` EXIT=0;ui-tests 14/14;**Phase 20 尚未获准** |
|
||||||
| Phase 20 Analytics 数据统计与管理驾驶舱复验 | 已完成(2026-09-17) | 见 `Phase-20-V11-Recovery-Completion-Report-2026-09-17.md`;修复 **20 处真实缺陷**(**D1 实名人数用 `profile_status != 'rejected'` 冒充 → 把"资料还在填"算成已实名**;**D3 Chat Started 直接数会话行数 → 系统通知计入**;**D4 Match 不分来源 → 10 对互选被当 10 对成功引用**;**D5 活动漏斗全部硬编码 0 + 一行把"用户数"当"活动数"的死代码**;**D6 Banner 曝光/点击硬编码 0**;**D12 系统性软删缺陷:`BaseEntity.deletedAt` 无 `@TableLogic` + 全局无逻辑删除配置 → `selectCount` 把软删数据全部计入且无任何报错**,修复 45 处统计调用(含 2 轮复验补扫出的 5 处次级路径);**D15 三套独立实现** → 抽 `AnalyticsMetricsCalculator` 统一口径;**D13/权限 授予脱节:`data_viewer` 空壳角色 + `platform_admin` 被 `id >= '20000000-…'` 条件漏掉 V1.0 老权限段 → 整个模块除 super_admin 外全员 403**;**D14 `AnalyticsServiceTest` = 5×`assertTrue(true)` 假绿 → 删除**;D20 `getDailySummary` 无边界校验;前端 `AnalyticsView.vue` 未接 `/v11-dashboard`);建立指标字典 `MetricCatalog`(36 指标 × 12 字段)+ `docs/ANALYTICS_METRICS.md`;新增 V043 授权(**刻意排除 `organization_admin`** —— 相关表无组织维度,无范围全量数据即越权);新增守卫 `MetricCatalogGuardTest`(11) / `AnalyticsMetricsPhase20IntegrationTest`(21) / `AnalyticsSoftDeleteGuardTest`(4,结构性) / `AnalyticsPermissionGrantTest`(6);**跨阶段修复 X1~X3**(**注销写入 `user_identities` 状态域的 `'failed'` → 违反 V017 `chk_elig_verify_status` → 真实 PG 上账号注销 100% 不可用**;`checkCanWithdraw` 资格拦截死代码;测试库补两条 CHECK 后暴露 `ActivityPhase14Test` 夹具违规)并新增 `LocalEligibilityStatusVocabularyGuardTest`(9);负向验证两轮 7 注入 → 7 用例精准变红并还原(`md5sum` 逐字节一致);后端 **648 tests 全绿**;admin-web `vue-tsc` EXIT=0;**Phase 21 已复验关闭(2026-09-18,650 tests 全绿)** |
|
| Phase 20 Analytics 数据统计与管理驾驶舱复验 | 已完成(2026-09-17) | 见 `Phase-20-V11-Recovery-Completion-Report-2026-09-17.md`;修复 **20 处真实缺陷**(**D1 实名人数用 `profile_status != 'rejected'` 冒充 → 把"资料还在填"算成已实名**;**D3 Chat Started 直接数会话行数 → 系统通知计入**;**D4 Match 不分来源 → 10 对互选被当 10 对成功引用**;**D5 活动漏斗全部硬编码 0 + 一行把"用户数"当"活动数"的死代码**;**D6 Banner 曝光/点击硬编码 0**;**D12 系统性软删缺陷:`BaseEntity.deletedAt` 无 `@TableLogic` + 全局无逻辑删除配置 → `selectCount` 把软删数据全部计入且无任何报错**,修复 45 处统计调用(含 2 轮复验补扫出的 5 处次级路径);**D15 三套独立实现** → 抽 `AnalyticsMetricsCalculator` 统一口径;**D13/权限 授予脱节:`data_viewer` 空壳角色 + `platform_admin` 被 `id >= '20000000-…'` 条件漏掉 V1.0 老权限段 → 整个模块除 super_admin 外全员 403**;**D14 `AnalyticsServiceTest` = 5×`assertTrue(true)` 假绿 → 删除**;D20 `getDailySummary` 无边界校验;前端 `AnalyticsView.vue` 未接 `/v11-dashboard`);建立指标字典 `MetricCatalog`(36 指标 × 12 字段)+ `docs/ANALYTICS_METRICS.md`;新增 V043 授权(**刻意排除 `organization_admin`** —— 相关表无组织维度,无范围全量数据即越权);新增守卫 `MetricCatalogGuardTest`(11) / `AnalyticsMetricsPhase20IntegrationTest`(21) / `AnalyticsSoftDeleteGuardTest`(4,结构性) / `AnalyticsPermissionGrantTest`(6);**跨阶段修复 X1~X3**(**注销写入 `user_identities` 状态域的 `'failed'` → 违反 V017 `chk_elig_verify_status` → 真实 PG 上账号注销 100% 不可用**;`checkCanWithdraw` 资格拦截死代码;测试库补两条 CHECK 后暴露 `ActivityPhase14Test` 夹具违规)并新增 `LocalEligibilityStatusVocabularyGuardTest`(9);负向验证两轮 7 注入 → 7 用例精准变红并还原(`md5sum` 逐字节一致);后端 **648 tests 全绿**;admin-web `vue-tsc` EXIT=0;**Phase 21 已复验关闭(2026-09-18,650 tests 全绿)** |
|
||||||
| Phase 21 账户生命周期增量复验 | 已完成(2026-09-18) | 见 `docs/Phase-21-V11-Recovery-Completion-Report-2026-09-18.md`;KEEP-VERIFY 复验发现 STATUS 标✅ 但含 **6 处真实缺陷 + 假绿测试**:① `AuthService.checkAccountStatus` 不拒 `CLOSED` → 注销后仍可登录(Gate ❌→✅);② `notifyActiveMatches` 为 TODO 桩 → 注销未关闭活跃 Match/会话(Gate ❌→✅,已实现关闭 Match + `chatService.closeConversationForMatch`);③ `cancelPendingActivityRegistrations` 为 TODO 桩 → 未来活动报名未关闭(Gate ❌→✅);④ `anonymizeServiceHistory` 已实现却**从未被 `closeAccount` 调用(死代码)** → 个人/咨询正文暴露(Gate ❌→✅,已接线 + 脱敏身份证占位符 + `counseling_followups` 正文);⑤ `closeAccount` 在 blockers 处硬抛异常使 `cancelXxx` 永远不可达(死代码矛盾 → 信息性日志 + 继续清理);⑥ 删除 7×`assertTrue(true)` 假绿 → 替换为 11 用例真实 DB 集成测试 + 3 用例登录守卫;跨模块防线(推荐引擎/匹配资格/Capability 已拒非 NORMAL)确认有效;后端 **650 tests 全绿**(起点 648,净 +2);**Phase 22 已复验关闭(2026-09-18,657 tests 全绿)** |
|
| Phase 21 账户生命周期增量复验 | 已完成(2026-09-18) | 见 `docs/Phase-21-V11-Recovery-Completion-Report-2026-09-18.md`;KEEP-VERIFY 复验发现 STATUS 标✅ 但含 **6 处真实缺陷 + 假绿测试**:① `AuthService.checkAccountStatus` 不拒 `CLOSED` → 注销后仍可登录(Gate ❌→✅);② `notifyActiveMatches` 为 TODO 桩 → 注销未关闭活跃 Match/会话(Gate ❌→✅,已实现关闭 Match + `chatService.closeConversationForMatch`);③ `cancelPendingActivityRegistrations` 为 TODO 桩 → 未来活动报名未关闭(Gate ❌→✅);④ `anonymizeServiceHistory` 已实现却**从未被 `closeAccount` 调用(死代码)** → 个人/咨询正文暴露(Gate ❌→✅,已接线 + 脱敏身份证占位符 + `counseling_followups` 正文);⑤ `closeAccount` 在 blockers 处硬抛异常使 `cancelXxx` 永远不可达(死代码矛盾 → 信息性日志 + 继续清理);⑥ 删除 7×`assertTrue(true)` 假绿 → 替换为 11 用例真实 DB 集成测试 + 3 用例登录守卫;跨模块防线(推荐引擎/匹配资格/Capability 已拒非 NORMAL)确认有效;后端 **650 tests 全绿**(起点 648,净 +2);**Phase 22 已复验关闭(2026-09-18,657 tests 全绿)** |
|
||||||
| Phase 22 部署运维配置扩展(Feature Flag)复验 | 已完成(2026-09-18) | 见 `docs/Phase-22-V11-Recovery-Completion-Report-2026-09-18.md`;KEEP-VERIFY 复验发现 STATUS 标✅ 但含 **2 处真实代码缺陷 + 2 处文档夸大**:① D-22-A(High)`FeatureFlagController.updateFlag` 仅支持布尔 flag(反射 `getMethod("set"+cap, boolean.class)`)→ 7 个非布尔 flag 经 API 更新必失败,已改为按字段类型分派 boolean/int/String;② D-22-B(Medium)`body.getOrDefault("value", false)` 缺 value 静默置 false + 非布尔转型崩,已改为缺 value/类型错→400 且不改原值;T01–T05 模块开关(`isCounselingEnabled` 等)全仓无业务调用点 → 死配置(暴露未 gate,遗留交各模块 Phase);T06 上传策略实测已在 `LocalEligibilityService`+`PhotoService` 真强制 ✅;T07 私有桶零引用 → 死配置(遗留);yml 无 `features:` 块、默认值实为 Java 字段默认(文档纠正);新增 `FeatureFlagControllerTest`(7 用例)+ 负向验证(仅布尔缺陷版恰 2 用例变红);后端 **657 tests 全绿**(起点 650,净 +7);**Phase 23 尚未获准** |
|
| Phase 22 部署运维配置扩展(Feature Flag)复验 | 已完成(2026-09-18) | 见 `docs/Phase-22-V11-Recovery-Completion-Report-2026-09-18.md`;KEEP-VERIFY 复验发现 STATUS 标✅ 但含 **2 处真实代码缺陷 + 2 处文档夸大**:① D-22-A(High)`FeatureFlagController.updateFlag` 仅支持布尔 flag(反射 `getMethod("set"+cap, boolean.class)`)→ 7 个非布尔 flag 经 API 更新必失败,已改为按字段类型分派 boolean/int/String;② D-22-B(Medium)`body.getOrDefault("value", false)` 缺 value 静默置 false + 非布尔转型崩,已改为缺 value/类型错→400 且不改原值;T01–T05 模块开关(`isCounselingEnabled` 等)全仓无业务调用点 → 死配置(暴露未 gate,遗留交各模块 Phase);T06 上传策略实测已在 `LocalEligibilityService`+`PhotoService` 真强制 ✅;T07 私有桶零引用 → 死配置(遗留);yml 无 `features:` 块、默认值实为 Java 字段默认(文档纠正);新增 `FeatureFlagControllerTest`(7 用例)+ 负向验证(仅布尔缺陷版恰 2 用例变红);后端 **657 tests 全绿**(起点 650,净 +7);**Phase 23 已复验(2026-09-18,部分关闭)** |
|
||||||
|
| Phase 23 可信系统测试重建(REFACTOR)复验 | ⚠️ 部分关闭(2026-09-18) | 见 `docs/Phase-23-V11-Recovery-Completion-Report-2026-09-18.md`;KEEP-VERIFY 复验发现 STATUS 标✅ 但含 **5 处真实缺陷**:① **D-23-A(Critical)** `V11InvariantsTest` 原 14 个用例全为常量字符串断言 —— INV-016「Banner 目标类型」实际断言**文章状态**常量(跑错域)、INV-017「首页 8 入口」是**对自己刚声明的字面量数组做 `length==4` 的重言式**(从未调用首页服务)、INV-018/019/020 用预约/文章/账号状态常量冒充权限与数据范围验证;已重写为 12 个用例,含 **4 个真实服务行为守卫**(INV-013 走真实 `CounselingService.getDetail` 断言 Matchmaker 读到 `userRequestSummary==null` 并以 assigned Counselor 能读正文作反证;INV-016 反射读生产 `ALLOWED_TARGET_TYPES` 断言不含外链;INV-017 真实 `getHome` 断言 8 入口编码与顺序固定;INV-019 断言 `content_editor` 读到 null)+ `assertBehaviorTestExists` 用 `Class.forName` 把「行为验证见 XxxTest」变成可执行契约;② **D-23-B(High)** `tests/e2e/` **44 个 `pass` 空壳**(4 个文件 100% 为空壳,另 2 个混合),违反 Gate「不允许 skip/pass 空壳」→ 已移除并改为诚实占位(保留完整场景清单);③ **D-23-C(High)** pytest E2E 套件**整体不可执行**:全仓无 `conftest.py`,约 15 个 fixture(`api_client`/`verified_user`/`banner_factory`…)不存在,唯一可执行的 `run_v11_tests.py`(16 用例)需活栈 + `E2E_PASSWORD`;④ **D-23-D(High,遗留未修)** CI 未串联 E2E(计划明确要求)→ 需先决定 CI 活栈供给方式,未实施;⑤ **D-23-E(Medium)** CI `migration` job 注释称「验证 SQL 语法」实为只查文件非空(真实语法校验在 `MigrationValidationTest`,随 backend job 跑)→ 改为真实序列校验(命名/非空/版本号唯一/无缺号,43 个文件 1~43 连续唯一)。**新增闸门**:`scripts/check-no-empty-tests.py`(AST 扫描阻断 `pass` 空壳与无条件 `skip`,已双向验证)+ CI `test-shell-guard` job。**证据**:负向注入 3 处生产缺陷 → 恰好 `inv013`/`inv016`/`inv017` 3 个用例精准变红、其余 9 绿、零无关失败,`md5sum -c` 还原一致;`655 tests / 通过 652 / 失败 0 / 错误 0 / 跳过 3`(起点 657,净 −2 因 14 空转用例合并为 12;3 个跳过均有显式原因,非空壳)。**遗留**:E2E 入 CI 待基础设施决策、pytest fixture 体系缺失、INV-011 缺显式守卫。**结论:不宣布「失败为零可进入 UAT」**;**Phase 24 尚未获准** |
|
||||||
| UI-CLEANUP-20260913 | 已完成 | 废弃 UI 代码、冲突规范、旧计划和无效验收证据已清理;`ui-document-baseline-contracts` 锁定唯一视觉入口 |
|
| UI-CLEANUP-20260913 | 已完成 | 废弃 UI 代码、冲突规范、旧计划和无效验收证据已清理;`ui-document-baseline-contracts` 锁定唯一视觉入口 |
|
||||||
| UI-ADMIN-20260913 | 已完成(待桌面端目视复验) | 管理端全局框架与工作台已按规格交付;18 modules + 22 routes,vue-tsc 与 vite build 全绿 |
|
| UI-ADMIN-20260913 | 已完成(待桌面端目视复验) | 管理端全局框架与工作台已按规格交付;18 modules + 22 routes,vue-tsc 与 vite build 全绿 |
|
||||||
| UI-MINIAPP-20260913 | 已完成 | 小程序 11 个核心页面已重构;11/11 页 TS-only,2026-09-16 通过用户验收 |
|
| UI-MINIAPP-20260913 | 已完成 | 小程序 11 个核心页面已重构;11/11 页 TS-only,2026-09-16 通过用户验收 |
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
# 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 一环仍缺。
|
||||||
+43
-22
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
> **2026-09-17 当前有效结论:`V1.1 INCREMENTAL RECOVERY PHASE 20 COMPLETE / UI REDESIGN ACCEPTED BY USER / GIT CLEAN`。**
|
> **2026-09-17 当前有效结论:`V1.1 INCREMENTAL RECOVERY PHASE 20 COMPLETE / UI REDESIGN ACCEPTED BY USER / GIT CLEAN`。**
|
||||||
>
|
>
|
||||||
> 阶段 0(在途收口)、阶段 1(小程序 C 组 + discover 筛选重设计)、阶段 2(管理端 Vue 新框架适配)、阶段 3(CapabilityService 独立化)、阶段 4(双端总验收)全部完成。三端构建通过、**13/13 契约全绿**、核心 11 页面**全部 TS-only(11/11)**。**UI 重设计首批 11 页已于 2026-09-16 通过用户验收(结论:基本达标)**;`/me/*` 前后端契约同轮收敛,17 个 404 调用已恢复。**Phase 08(Like / Match / Block)、Phase 09(Text Chat)、Phase 10(Image & Chat Safety)、Phase 11(Notification)、Phase 12(Relationship Progress)、Phase 13(公益红娘转介)、Phase 13A(婚恋咨询与婚前辅导)、Phase 14(多类型公益活动)、Phase 15(活动报名、审核与签到)、Phase 16(活动互选与双方开启沟通确认)、Phase 17(举报与风控)、Phase 18(申诉)、Phase 19(统一运营后台 + 用户端公益服务页面)、Phase 20(Analytics 数据统计与管理驾驶舱)复验已关闭**,十四轮共修复 130+ 处真实缺陷(含 3 处隐藏编译缺陷),并各自补齐测试保真度与守卫。**Phase 21、22 复验已关闭(2026-09-18);Phase 23 起需用户确认后启动。**
|
> 阶段 0(在途收口)、阶段 1(小程序 C 组 + discover 筛选重设计)、阶段 2(管理端 Vue 新框架适配)、阶段 3(CapabilityService 独立化)、阶段 4(双端总验收)全部完成。三端构建通过、**13/13 契约全绿**、核心 11 页面**全部 TS-only(11/11)**。**UI 重设计首批 11 页已于 2026-09-16 通过用户验收(结论:基本达标)**;`/me/*` 前后端契约同轮收敛,17 个 404 调用已恢复。**Phase 08(Like / Match / Block)、Phase 09(Text Chat)、Phase 10(Image & Chat Safety)、Phase 11(Notification)、Phase 12(Relationship Progress)、Phase 13(公益红娘转介)、Phase 13A(婚恋咨询与婚前辅导)、Phase 14(多类型公益活动)、Phase 15(活动报名、审核与签到)、Phase 16(活动互选与双方开启沟通确认)、Phase 17(举报与风控)、Phase 18(申诉)、Phase 19(统一运营后台 + 用户端公益服务页面)、Phase 20(Analytics 数据统计与管理驾驶舱)复验已关闭**,十四轮共修复 130+ 处真实缺陷(含 3 处隐藏编译缺陷),并各自补齐测试保真度与守卫。**Phase 21、22 复验已关闭(2026-09-18)**;**Phase 23 为「部分关闭」(2026-09-18)** —— 已清除 44 个 `pass` 空壳与 14 个只断言常量的假不变量测试,新增 4 个经负向注入验证的真实行为守卫与空壳阻断闸门,后端回归 655 全绿;但 **E2E 进入 CI 因需「活栈供给方式」基础设施决策而列为遗留,故不宣布「失败为零可进入 UAT」**。**Phase 24 起需用户确认后启动。**
|
||||||
|
|
||||||
最后更新: 2026-09-18 — 分支 `main`(`feat-ui-v11` 保留为冗余锚点)
|
最后更新: 2026-09-18 — 分支 `main`(`feat-ui-v11` 保留为冗余锚点)
|
||||||
|
|
||||||
@@ -462,7 +462,7 @@
|
|||||||
| 内容管理(婚恋课堂/反诈) | Phase 19 | ✅ 已完成(2026-09-17 复验关闭) |
|
| 内容管理(婚恋课堂/反诈) | Phase 19 | ✅ 已完成(2026-09-17 复验关闭) |
|
||||||
| 专项公益服务项目 | Phase 19 | ✅ 已完成(2026-09-17 复验关闭) |
|
| 专项公益服务项目 | Phase 19 | ✅ 已完成(2026-09-17 复验关闭) |
|
||||||
| 善缘服务站 + 用户端公益服务页面(U37/U49~U54/U60/U61/U64) | Phase 19 | ✅ 已完成(含 V042 临时公告) |
|
| 善缘服务站 + 用户端公益服务页面(U37/U49~U54/U60/U61/U64) | Phase 19 | ✅ 已完成(含 V042 临时公告) |
|
||||||
| V1.1新增不变量测试(INV-011~020) | Phase 23 | ⏳ 待开发 |
|
| V1.1新增不变量测试(INV-011~020) | Phase 23 | ⚠️ 部分完成(2026-09-18):原 14 个用例全为常量断言(INV-016 断言文章状态、INV-017 为自造字面量重言式);已重写为 12 个,其中 4 个为真实服务行为(INV-013/016/017/019),已过负向注入验证。详见 `docs/Phase-23-V11-Recovery-Completion-Report-2026-09-18.md` |
|
||||||
|
|
||||||
### 当前运行状态
|
### 当前运行状态
|
||||||
|
|
||||||
@@ -616,26 +616,47 @@ MinIO: localhost:9003 (凭证由本地环境变量提供)
|
|||||||
|
|
||||||
**Phase 21(账户生命周期增量)— 已完成** ✅
|
**Phase 21(账户生命周期增量)— 已完成** ✅
|
||||||
|
|
||||||
### Phase 23 — V1.1专项测试 ✅ 已完成
|
### Phase 23 — 可信系统测试重建(REFACTOR)⚠️ 部分关闭(KEEP-VERIFY 复验,2026-09-18)
|
||||||
| 任务 | 状态 |
|
|
||||||
|------|------|
|
|
||||||
| P23-T01 Home V1.1 E2E | ✅ (home_v11_test.py) |
|
|
||||||
| P23-T02 Banner timing filter | ✅ (home_v11_test.py) |
|
|
||||||
| P23-T03 Banner audience filter | ✅ (home_v11_test.py) |
|
|
||||||
| P23-T04 Quick Entry order | ✅ (home_v11_test.py) |
|
|
||||||
| P23-T05 Local Eligibility E2E | ✅ (local_eligibility_test.py) |
|
|
||||||
| P23-T06 Counseling E2E | ✅ (counseling_v11_test.py) |
|
|
||||||
| P23-T07 Counseling permission | ✅ (counseling_permission_test.py) |
|
|
||||||
| P23-T08 Service Appointment E2E | ✅ (service_appointment_test.py) |
|
|
||||||
| P23-T09 Content E2E | ✅ (content_e2e_test.py) |
|
|
||||||
| P23-T10 Multi-type Activity E2E | ✅ (multi_type_activity_test.py) |
|
|
||||||
| P23-T11 Activity Mutual Consent E2E | ✅ (activity_mutual_consent_test.py) |
|
|
||||||
| P23-T12 Service Station permission | ✅ (service_station_permission_test.py) |
|
|
||||||
| P23-T13 Organization relation isolation | ✅ (organization_isolation_test.py) |
|
|
||||||
| P23-T14 INV-011~INV-020 | ✅ (V11InvariantsTest.java) |
|
|
||||||
|
|
||||||
E2E测试文件:10个(~80+测试用例)
|
> 下方「Phase 23 — V1.1专项测试 ✅ 已完成」为**历史声明,经实测部分不成立**,以本节为准。
|
||||||
Invariant测试:10个(INV-011~INV-020)
|
|
||||||
|
| 项 | 结论 |
|
||||||
|
|---|---|
|
||||||
|
| 报告 | `docs/Phase-23-V11-Recovery-Completion-Report-2026-09-18.md` |
|
||||||
|
| 回归 | **655 tests / 通过 652 / 失败 0 / 错误 0 / 跳过 3**(起点 657,净 −2:原 14 个空转用例合并为 12 个) |
|
||||||
|
| 跳过原因 | `ConcurrentTests` ×2(`@EnabledIfEnvironmentVariable(CI=true)`,「本地并发测试跳过」);`CoreE2ETest` ×1(`@Disabled`「需要完整数据库连接」)—— 均有显式声明,非空壳 |
|
||||||
|
| D-23-A(Critical) | `V11InvariantsTest` 原 14 个用例全为常量断言:INV-016 断言**文章状态**常量(跑错域)、INV-017 是**自造字面量的重言式**(从未调用首页服务)、INV-018/019/020 用无关状态常量冒充权限验证 → 已重写,新增 4 个真实行为守卫(INV-013 脱敏、INV-016 白名单禁外链、INV-017 首页 8 入口顺序、INV-019 内容编辑员零数据)+ `assertBehaviorTestExists` 指针契约 |
|
||||||
|
| D-23-B(High) | `tests/e2e/` **44 个 `pass` 空壳**(4 个文件 100% 为空壳)→ 已移除,改为诚实占位并保留场景清单 |
|
||||||
|
| D-23-C(High) | pytest E2E 套件**整体不可执行**:全仓无 `conftest.py`,约 15 个 fixture 不存在;唯一可执行的 `run_v11_tests.py`(16 用例)需活栈 |
|
||||||
|
| D-23-D(High,遗留) | **CI 未串联 E2E**(计划明确要求)→ 需先决定 CI 活栈供给方式,未在本轮实施 |
|
||||||
|
| D-23-E(Medium) | CI `migration` job 注释称「验证 SQL 语法」但只查文件非空 → 改为真实序列校验(命名/非空/版本号唯一/无缺号),已本地实跑通过 |
|
||||||
|
| 新增闸门 | `scripts/check-no-empty-tests.py`(AST 扫描,阻断 `pass` 空壳与无条件 `skip`)+ CI `test-shell-guard` job |
|
||||||
|
| 遗留 | ① E2E 进入 CI 待基础设施决策;② pytest fixture 体系缺失;③ INV-011 无显式守卫(结构性满足);④ E2E 场景清单待实现(已保留在占位文件头) |
|
||||||
|
| 结论 | **部分关闭**:假覆盖已归零,但 UAT 前 Gate 要求的端到端链路仍未真实执行,**不宜据此宣布可进入 UAT** |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Phase 23 — V1.1专项测试 ✅ 已完成(历史声明,已被上方权威结论取代)
|
||||||
|
> ⚠️ 下表为历史进度声明,**经 Phase 23 KEEP-VERIFY 实测部分不成立**,以上方「Phase 23 — 可信系统测试重建(REFACTOR)」为准。
|
||||||
|
| 任务 | 历史声明 | 实测结论 |
|
||||||
|
|------|---------|---------|
|
||||||
|
| P23-T01 Home V1.1 E2E | ✅ (home_v11_test.py) | ❌ 该文件 7/9 为空壳且依赖不存在的 fixture,从未执行;首页聚合实由 `run_v11_tests.py::test_02_home` 覆盖 |
|
||||||
|
| P23-T02 Banner timing filter | ✅ (home_v11_test.py) | ❌ 空壳 `pass`,未实现 |
|
||||||
|
| P23-T03 Banner audience filter | ✅ (home_v11_test.py) | ❌ 空壳 `pass`,未实现 |
|
||||||
|
| P23-T04 Quick Entry order | ✅ (home_v11_test.py) | ⚠️ 原为自造字面量重言式;**现由真实用例 `V11InvariantsTest#inv017QuickEntriesFixedOrder` + `run_v11_tests.py::test_03` 覆盖** |
|
||||||
|
| P23-T05 Local Eligibility E2E | ✅ (local_eligibility_test.py) | ❌ 该文件 11/11 全为空壳,从未执行 |
|
||||||
|
| P23-T06 Counseling E2E | ✅ (counseling_v11_test.py) | ⚠️ 该文件真实可执行;其中 INV-013 一项为空壳,已移除,**改由 `V11InvariantsTest#inv013` 真实覆盖** |
|
||||||
|
| P23-T07 Counseling permission | ✅ (counseling_permission_test.py) | ❌ 该文件 10/10 全为空壳,从未执行 |
|
||||||
|
| P23-T08 Service Appointment E2E | ✅ (service_appointment_test.py) | ❌ 该文件 7/7 全为空壳,从未执行 |
|
||||||
|
| P23-T09 Content E2E | ✅ (content_e2e_test.py) | ✅ 属实(自包含 requests 用例,真实可执行) |
|
||||||
|
| P23-T10 Multi-type Activity E2E | ✅ (multi_type_activity_test.py) | ✅ 属实(自包含 requests 用例) |
|
||||||
|
| P23-T11 Activity Mutual Consent E2E | ✅ (activity_mutual_consent_test.py) | ❌ 该文件 8/8 全为空壳;不变量实由后端 `ActivityMutualConsentServiceTest` 真实覆盖 |
|
||||||
|
| P23-T12 Service Station permission | ✅ (service_station_permission_test.py) | ✅ 属实(自包含 requests 用例) |
|
||||||
|
| P23-T13 Organization relation isolation | ✅ (organization_isolation_test.py) | ✅ 属实(自包含 requests 用例) |
|
||||||
|
| P23-T14 INV-011~INV-020 | ✅ (V11InvariantsTest.java) | ❌ 原 14 个用例全为常量字符串断言(INV-016 断言文章状态、INV-017 重言式);**已重写为 12 个,含 4 个真实行为守卫** |
|
||||||
|
|
||||||
|
E2E测试文件:10个(~80+测试用例)→ **实测:6 个含 44 个 `pass` 空壳,其中 4 个 100% 为空壳;pytest 侧因无 `conftest.py` 整体不可执行**
|
||||||
|
Invariant测试:10个(INV-011~020)→ **实测:原 14 个用例无任何服务行为断言;现 12 个,其中 4 个为真实行为(已过负向注入验证)**
|
||||||
|
|
||||||
**Phase 22(部署运维配置扩展)— 已完成** ✅
|
**Phase 22(部署运维配置扩展)— 已完成** ✅
|
||||||
|
|
||||||
@@ -695,7 +716,7 @@ Invariant测试:10个(INV-011~INV-020)
|
|||||||
|-----------|------|
|
|-----------|------|
|
||||||
| Phase 01~12 (V1.0基线) | ✅ 已完成 |
|
| Phase 01~12 (V1.0基线) | ✅ 已完成 |
|
||||||
| Phase 13~22 (V1.1核心增量) | ✅ 已完成 |
|
| Phase 13~22 (V1.1核心增量) | ✅ 已完成 |
|
||||||
| Phase 23 (V1.1专项测试) | ✅ 已完成 |
|
| Phase 23 (V1.1专项测试) | ⚠️ 部分关闭(2026-09-18):假覆盖已清除 + 4 个真实守卫;E2E 入 CI 待决策 |
|
||||||
| Phase 24~26 (UAT/RC/Production) | ⏳ 运营/上线阶段 |
|
| Phase 24~26 (UAT/RC/Production) | ⏳ 运营/上线阶段 |
|
||||||
|
|
||||||
### Phase 24 — UAT增量 ✅ 已就绪
|
### Phase 24 — UAT增量 ✅ 已就绪
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
Phase 23 Gate 守卫:禁止测试空壳(skip / pass 空壳)。
|
||||||
|
|
||||||
|
计划 Phase 23 Gate 原文:
|
||||||
|
「不允许 skip/pass 空壳;报告列出总数、通过、失败、跳过及原因,失败为零才进入 UAT。」
|
||||||
|
|
||||||
|
背景(Phase 23 KEEP-VERIFY 实测)
|
||||||
|
--------------------------------
|
||||||
|
`tests/e2e/` 下曾有 **44 个**函数体只有 `pass` 的测试函数,分属 6 个文件,
|
||||||
|
其中 4 个文件 100% 由空壳构成;这些用例从未执行过(且依赖根本不存在的 pytest fixture),
|
||||||
|
却在 `docs/STATUS.md` 中被计为 P23-Txx ✅。
|
||||||
|
本守卫用 AST(而非 grep)在 CI 中机械阻断空壳回归。
|
||||||
|
|
||||||
|
检测项
|
||||||
|
------
|
||||||
|
1. 函数体为空、或仅为「docstring + pass」的函数(测试函数与非测试的 helper 均计入,
|
||||||
|
因为空壳 helper 同样是未实现逻辑);
|
||||||
|
2. 被 `@pytest.mark.skip` / `skipif` 无理由装饰、或函数体内直接
|
||||||
|
`pytest.skip(...)` 开头跳过的「测试函数」(`test_*`)。
|
||||||
|
|
||||||
|
用法
|
||||||
|
----
|
||||||
|
python3 scripts/check-no-empty-tests.py [目录 ...] # 默认扫描 tests/
|
||||||
|
|
||||||
|
退出码:0 = 无空壳;1 = 发现空壳(并打印 文件:行 清单)。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SKIP_DECORATORS = {"skip", "skipif"}
|
||||||
|
|
||||||
|
|
||||||
|
def is_docstring_only_then_pass(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
||||||
|
"""函数体是否为空,或仅为「docstring + pass」。"""
|
||||||
|
body = [stmt for stmt in node.body]
|
||||||
|
if not body:
|
||||||
|
return True
|
||||||
|
# 去掉开头的 docstring
|
||||||
|
if (
|
||||||
|
isinstance(body[0], ast.Expr)
|
||||||
|
and isinstance(body[0].value, ast.Constant)
|
||||||
|
and isinstance(body[0].value.value, str)
|
||||||
|
):
|
||||||
|
body = body[1:]
|
||||||
|
if not body:
|
||||||
|
return True
|
||||||
|
return all(isinstance(stmt, ast.Pass) for stmt in body)
|
||||||
|
|
||||||
|
|
||||||
|
def has_skip_marker(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
||||||
|
"""是否被无条件 skip 的装饰器标记。"""
|
||||||
|
for dec in node.decorator_list:
|
||||||
|
target = None
|
||||||
|
if isinstance(dec, ast.Attribute):
|
||||||
|
target = dec.attr
|
||||||
|
elif isinstance(dec, ast.Call) and isinstance(dec.func, ast.Attribute):
|
||||||
|
target = dec.func.attr
|
||||||
|
if target in SKIP_DECORATORS and not node.name.startswith("test_"):
|
||||||
|
continue
|
||||||
|
if target in SKIP_DECORATORS:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def starts_with_pytest_skip(node: ast.FunctionDef | ast.AsyncFunctionDef) -> bool:
|
||||||
|
"""函数体是否以 pytest.skip(...) 打头(除 docstring 外)。"""
|
||||||
|
body = list(node.body)
|
||||||
|
if (
|
||||||
|
body
|
||||||
|
and isinstance(body[0], ast.Expr)
|
||||||
|
and isinstance(body[0].value, ast.Constant)
|
||||||
|
and isinstance(body[0].value.value, str)
|
||||||
|
):
|
||||||
|
body = body[1:]
|
||||||
|
if not body:
|
||||||
|
return False
|
||||||
|
stmt = body[0]
|
||||||
|
if not isinstance(stmt, ast.Expr) or not isinstance(stmt.value, ast.Call):
|
||||||
|
return False
|
||||||
|
func = stmt.value.func
|
||||||
|
return isinstance(func, ast.Attribute) and func.attr == "skip"
|
||||||
|
|
||||||
|
|
||||||
|
def scan_file(path: Path) -> list[str]:
|
||||||
|
try:
|
||||||
|
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
|
||||||
|
except SyntaxError as exc: # 语法错误交给 pytest 自己去报,这里不重复处理
|
||||||
|
return [f"{path}:{exc.lineno or 0}: 无法解析(SyntaxError: {exc.msg})"]
|
||||||
|
|
||||||
|
findings: list[str] = []
|
||||||
|
for node in ast.walk(tree):
|
||||||
|
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||||
|
continue
|
||||||
|
if is_docstring_only_then_pass(node):
|
||||||
|
findings.append(
|
||||||
|
f"{path}:{node.lineno}: 空壳函数 `{node.name}`(函数体只有 pass / 为空)"
|
||||||
|
)
|
||||||
|
elif node.name.startswith("test_") and (
|
||||||
|
has_skip_marker(node) or starts_with_pytest_skip(node)
|
||||||
|
):
|
||||||
|
findings.append(
|
||||||
|
f"{path}:{node.lineno}: 测试 `{node.name}` 被无条件 skip(Gate 禁止 skip 空壳)"
|
||||||
|
)
|
||||||
|
return findings
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str]) -> int:
|
||||||
|
roots = [Path(a) for a in argv[1:]] or [Path("tests")]
|
||||||
|
files: list[Path] = []
|
||||||
|
for root in roots:
|
||||||
|
if root.is_file():
|
||||||
|
files.append(root)
|
||||||
|
else:
|
||||||
|
files.extend(sorted(root.rglob("*.py")))
|
||||||
|
|
||||||
|
all_findings: list[str] = []
|
||||||
|
for f in files:
|
||||||
|
all_findings.extend(scan_file(f))
|
||||||
|
|
||||||
|
if all_findings:
|
||||||
|
print("FAIL: 发现测试空壳(Phase 23 Gate 禁止 skip/pass 空壳):", file=sys.stderr)
|
||||||
|
for line in all_findings:
|
||||||
|
print(" " + line, file=sys.stderr)
|
||||||
|
print(f"\n合计 {len(all_findings)} 处。请实现真实断言,或删除该空壳。", file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
print(f"OK: 未发现 skip/pass 空壳(已扫描 {len(files)} 个 Python 文件)")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
raise SystemExit(main(sys.argv))
|
||||||
@@ -1,57 +1,35 @@
|
|||||||
"""
|
"""
|
||||||
V1.1 活动互选二阶段同意 E2E 测试骨架
|
V1.1 活动互选二阶段同意 E2E —— 空壳已移除(Phase 23 KEEP-VERIFY,2026-09-18)
|
||||||
测试项目: 活动互选流程重构(INV-014, INV-015)
|
|
||||||
|
历史问题
|
||||||
|
--------
|
||||||
|
原文件 8 个测试函数**全部**为 `pass` 空壳(注释「TODO: P16 实现后完善」),
|
||||||
|
依赖不存在的 `activity_factory` / `user_a` / `user_b` 等 fixture(全局无 `conftest.py`),
|
||||||
|
从未执行过,却在 `docs/STATUS.md` 中被计为 P23-T11「活动互选 E2E ✅」。
|
||||||
|
|
||||||
|
计划 Phase 23 Gate:「**不允许 skip/pass 空壳**」。
|
||||||
|
|
||||||
|
当前状态
|
||||||
|
--------
|
||||||
|
本文件现**不含任何测试函数**,pytest 收集 0 个用例。
|
||||||
|
|
||||||
|
仍待真实实现的场景(需求不因移除空壳而丢失)
|
||||||
|
------------------------------------------
|
||||||
|
1. Activity Mutual Interest 不直接创建 Match(INV-014)
|
||||||
|
2. 一方确认不创建 Match(INV-015)
|
||||||
|
3. 双方确认后创建新 Match Episode
|
||||||
|
4. 已有 active Match 时不重复创建(AGENTS.md #19)
|
||||||
|
5. 曾 Unmatch 后重新确认创建新 Episode(AGENTS.md #20)
|
||||||
|
6. 一方拒绝则不创建 Match
|
||||||
|
7. 超时(expires_at)处理
|
||||||
|
8. 单向意向不泄露给对方(INV-006)
|
||||||
|
|
||||||
|
现状补充(Phase 23 实测)
|
||||||
|
------------------------
|
||||||
|
INV-014 / INV-015 在后端已有**真实行为测试**覆盖:
|
||||||
|
`backend/.../activity_mutual/ActivityMutualConsentServiceTest`
|
||||||
|
(双确认 Gate、decline 不建 Match、复用已有 active Match、幂等、超时、软删过滤)。
|
||||||
|
即:E2E 层待补,但不变量本身并非无覆盖。
|
||||||
|
|
||||||
|
真实可执行的 V1.1 E2E 见:`tests/e2e/run_v11_tests.py`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.e2e
|
|
||||||
class TestActivityMutualConsent:
|
|
||||||
"""活动互选二阶段同意流程测试"""
|
|
||||||
|
|
||||||
def test_mutual_interest_does_not_create_match(self, api_client, activity_factory, user_a, user_b):
|
|
||||||
"""Activity Mutual Interest不直接创建Match(INV-014)"""
|
|
||||||
# TODO: P16 实现后完善
|
|
||||||
# Given: A和B在活动后互选
|
|
||||||
# When: 系统处理互选结果
|
|
||||||
# Then: 不创建Match,创建MutualConnectionConsent
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_one_side_confirm_no_match(self, api_client, activity_factory, user_a, user_b):
|
|
||||||
"""一方确认不创建Match(INV-015)"""
|
|
||||||
# TODO: P16 实现后完善
|
|
||||||
# Given: A和B已达成Mutual Activity Interest
|
|
||||||
# When: 只有A确认开启沟通
|
|
||||||
# Then: 不创建Match,不创建Conversation
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_both_confirm_creates_match_episode(self, api_client, activity_factory, user_a, user_b):
|
|
||||||
"""双方确认后创建新Match Episode"""
|
|
||||||
# TODO: P16 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_existing_active_match_not_duplicated(self, api_client, activity_factory, user_a, user_b):
|
|
||||||
"""已有active Match不重复创建"""
|
|
||||||
# TODO: P16 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_prior_unmatch_creates_new_episode(self, api_client, activity_factory, user_a, user_b):
|
|
||||||
"""已Unmatch后重新确认创建新Episode"""
|
|
||||||
# TODO: P16 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_one_side_decline_no_match(self, api_client, activity_factory, user_a, user_b):
|
|
||||||
"""一方拒绝不创建Match"""
|
|
||||||
# TODO: P16 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_timeout_handled(self, api_client, activity_factory, user_a, user_b):
|
|
||||||
"""超时处理"""
|
|
||||||
# TODO: P16 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_single_interest_not_leaked(self, api_client, activity_factory, user_a, user_b):
|
|
||||||
"""单向意向不泄露给对方"""
|
|
||||||
# TODO: P16 实现后完善
|
|
||||||
pass
|
|
||||||
|
|||||||
@@ -1,73 +1,41 @@
|
|||||||
"""
|
"""
|
||||||
V1.1 婚恋咨询权限 E2E 测试骨架
|
V1.1 咨询 / 服务站 / 内容编辑员权限 E2E —— 空壳已移除(Phase 23 KEEP-VERIFY,2026-09-18)
|
||||||
测试项目: Counselor权限隔离、红娘不可见咨询正文
|
|
||||||
|
历史问题
|
||||||
|
--------
|
||||||
|
原文件 10 个测试函数**全部**为 `pass` 空壳(注释「TODO: P13A / P17 / P19 实现后完善」),
|
||||||
|
依赖不存在的 `counselor_client` / `counseling_service_factory` / `content_editor_client`
|
||||||
|
等 fixture(全局无 `conftest.py`),从未执行过,
|
||||||
|
却在 `docs/STATUS.md` 中被计为 P23-T07「咨询权限 ✅」。
|
||||||
|
|
||||||
|
计划 Phase 23 Gate:「**不允许 skip/pass 空壳**」。
|
||||||
|
|
||||||
|
当前状态
|
||||||
|
--------
|
||||||
|
本文件现**不含任何测试函数**,pytest 收集 0 个用例。
|
||||||
|
|
||||||
|
仍待真实实现的场景(需求不因移除空壳而丢失)
|
||||||
|
------------------------------------------
|
||||||
|
1. Counselor 只能看到被分配给自己的咨询(INV-012)
|
||||||
|
2. Counselor 不可查看其他咨询师的服务
|
||||||
|
3. 红娘不可查看咨询正文(INV-013)
|
||||||
|
4. 普通管理员默认不可查看咨询敏感备注
|
||||||
|
5. 咨询管理员可分配咨询人员
|
||||||
|
6. 咨询过程中可转交风控
|
||||||
|
7. 服务站人员不能浏览全平台用户(INV-018)
|
||||||
|
8. 服务站人员可处理本服务站预约
|
||||||
|
9. 内容编辑员不能访问用户婚恋档案(INV-019)
|
||||||
|
10. 内容编辑员可管理文章内容
|
||||||
|
|
||||||
|
现状补充(Phase 23 实测)
|
||||||
|
------------------------
|
||||||
|
- INV-012 已由 `CounselingPhase13ATest` 的 4 个真实用例覆盖;
|
||||||
|
- INV-013 / INV-019 已由 Phase 23 新增的真实行为用例覆盖
|
||||||
|
(`V11InvariantsTest#inv013MatchmakerCannotReadCounselingBody`、
|
||||||
|
`V11InvariantsTest#inv019ContentEditorHasNoUserDataPermission`,
|
||||||
|
走真实 `CounselingService.getDetail`,已通过负向注入验证);
|
||||||
|
- INV-018 已由 `ServiceAppointmentDataScopeTest` 覆盖。
|
||||||
|
即:E2E 层待补,但不变量本身已有真实覆盖。
|
||||||
|
|
||||||
|
真实可执行的 V1.1 E2E 见:`tests/e2e/run_v11_tests.py`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.e2e
|
|
||||||
class TestCounselingPermission:
|
|
||||||
"""咨询权限测试(INV-012, INV-013)"""
|
|
||||||
|
|
||||||
def test_counselor_only_sees_assigned_services(self, counselor_client, counseling_service_factory):
|
|
||||||
"""Counselor只能看到被分配给自己的咨询(INV-012)"""
|
|
||||||
# GET /api/v1/operator/counseling/services
|
|
||||||
# TODO: P13A 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_counselor_cannot_view_other_assigned(self, counselor_client, another_counselor_service_factory):
|
|
||||||
"""Counselor不可查看其他咨询师的服务"""
|
|
||||||
# TODO: P13A 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_matchmaker_cannot_view_counseling_body(self, matchmaker_client, counseling_service_factory):
|
|
||||||
"""红娘不可查看咨询正文(INV-013)"""
|
|
||||||
# TODO: P13A 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_platform_admin_cannot_view_sensitive_note(self, platform_admin_client, counseling_service_factory):
|
|
||||||
"""普通管理员默认不可查看咨询敏感备注"""
|
|
||||||
# TODO: P13A 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_counseling_manager_can_assign(self, counseling_manager_client):
|
|
||||||
"""咨询管理员可分配咨询人员"""
|
|
||||||
# POST /api/v1/operator/counseling/services/{id}/assign
|
|
||||||
# TODO: P13A 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_counseling_risk_referral(self, counselor_client, counseling_service_factory):
|
|
||||||
"""咨询过程中可转交风控"""
|
|
||||||
# TODO: P13A + P17 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.e2e
|
|
||||||
class TestServiceAppointmentPermission:
|
|
||||||
"""服务站预约权限测试(INV-018)"""
|
|
||||||
|
|
||||||
def test_service_station_operator_cannot_browse_all_users(self, station_operator_client):
|
|
||||||
"""服务站人员不能浏览全平台用户(INV-018)"""
|
|
||||||
# TODO: P19 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_service_station_operator_can_process_appointments(self, station_operator_client, appointment_factory):
|
|
||||||
"""服务站人员可处理本服务站预约"""
|
|
||||||
# TODO: P19 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.e2e
|
|
||||||
class TestContentEditorPermission:
|
|
||||||
"""内容编辑员权限测试(INV-019)"""
|
|
||||||
|
|
||||||
def test_content_editor_cannot_access_user_profiles(self, content_editor_client):
|
|
||||||
"""内容编辑员不能访问用户婚恋档案(INV-019)"""
|
|
||||||
# TODO: P19 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_content_editor_can_manage_articles(self, content_editor_client):
|
|
||||||
"""内容编辑员可管理文章内容"""
|
|
||||||
# TODO: P19 实现后完善
|
|
||||||
pass
|
|
||||||
|
|||||||
@@ -106,11 +106,18 @@ def test_counselor_assigned_scope():
|
|||||||
|
|
||||||
|
|
||||||
# ---- INV-013: Matchmaker不能读取Counseling正文 ----
|
# ---- INV-013: Matchmaker不能读取Counseling正文 ----
|
||||||
def test_matchmaker_cannot_read_counseling_body():
|
# Phase 23 KEEP-VERIFY(2026-09-18):原 test_matchmaker_cannot_read_counseling_body
|
||||||
"""红娘不得查看咨询正文内容(通过权限隔离)"""
|
# 的函数体为 `pass` 空壳(注释自述「在当前测试框架下简化验证」),从未真实断言过任何权限,
|
||||||
# 此测试需要实际的红娘 token,验证其无 counseling.service.view 权限
|
# 却在 docs/STATUS.md 中被计为 P23-T06「咨询 E2E ✅」的一部分。
|
||||||
# 在当前测试框架下简化验证:仅确认 Counselor 端点不被普通用户访问
|
# 计划 Phase 23 Gate「不允许 skip/pass 空壳」→ 已移除该空壳。
|
||||||
pass
|
#
|
||||||
|
# INV-013 现由后端真实行为用例覆盖:
|
||||||
|
# backend/.../invariants/V11InvariantsTest#inv013MatchmakerCannotReadCounselingBody
|
||||||
|
# 走真实 CounselingService.getDetail:断言 Matchmaker 读到的 user_request_summary 为 null,
|
||||||
|
# 并以「assigned Counselor 能读到正文」作反证,避免全局脱敏造成的假阳性;
|
||||||
|
# 已通过负向注入验证(把 isMatchmaker 分支改为返回全文 → 该用例精准变红)。
|
||||||
|
#
|
||||||
|
# E2E 层待补场景:用真实红娘 token 调用咨询详情接口并断言被拒。
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
+42
-85
@@ -1,87 +1,44 @@
|
|||||||
"""
|
"""
|
||||||
V1.1 首页 E2E 测试骨架
|
V1.1 首页 E2E —— 空壳已移除(Phase 23 KEEP-VERIFY,2026-09-18)
|
||||||
测试项目: 善缘丹阳 V1.1 首页结构 + Banner + 8核心入口
|
|
||||||
|
历史问题
|
||||||
|
--------
|
||||||
|
原文件 9 个测试函数中 **7 个是 `pass` 空壳**,其余 5 个虽含断言,
|
||||||
|
但全部依赖 `api_client` / `authenticated_user` / `banner_factory` /
|
||||||
|
`user_factory` / `verified_user` / `searching_user` / `paused_user` /
|
||||||
|
`successful_user` / `admin_client` 等**在本仓库中根本不存在**的 pytest fixture
|
||||||
|
(全局无 `conftest.py`)→ 整个文件**一次都没执行过**,
|
||||||
|
却在 `docs/STATUS.md` 中被计为 P23-T01~T04「首页 E2E ✅」。
|
||||||
|
|
||||||
|
计划 Phase 23 Gate:「**不允许 skip/pass 空壳**」。
|
||||||
|
|
||||||
|
当前状态
|
||||||
|
--------
|
||||||
|
本文件现**不含任何测试函数**,pytest 收集 0 个用例。
|
||||||
|
|
||||||
|
仍待真实实现的场景(需求不因移除空壳而丢失)
|
||||||
|
------------------------------------------
|
||||||
|
1. `GET /api/v1/home` 返回 userState
|
||||||
|
2. Banner 按时间过滤(未开始 / 已过期不显示)
|
||||||
|
3. Banner 按 audience_scope 过滤
|
||||||
|
4. 首页 8 核心入口顺序固定(INV-017)
|
||||||
|
5. 未审核用户不返回真实推荐
|
||||||
|
6. Paused 用户不返回新推荐
|
||||||
|
7. Successful 用户不返回找对象推荐
|
||||||
|
8. 服务站可展示
|
||||||
|
9. Banner 不能创建外部 URL 跳转(INV-016)
|
||||||
|
10. `counselingEnabled` flag 控制 `canApplyCounseling`
|
||||||
|
11. Capability:`canReceiveRecommendations` 随账号状态变化
|
||||||
|
|
||||||
|
现状补充(Phase 23 实测)
|
||||||
|
------------------------
|
||||||
|
- 首页聚合与 8 入口已有真实 HTTP 覆盖:
|
||||||
|
`tests/e2e/run_v11_tests.py::test_02_home` 与 `::test_03_home_8_entries`;
|
||||||
|
- **INV-017 已由 Phase 23 新增的后端真实行为用例覆盖**
|
||||||
|
(`V11InvariantsTest#inv017QuickEntriesFixedOrder`,走真实
|
||||||
|
`HomeApplicationService.getHome(userId)`,断言 8 个编码与顺序完全固定);
|
||||||
|
- INV-016 已有 `BannerOperatorPhase19IntegrationTest` 真实覆盖。
|
||||||
|
即:E2E 层待补,但不变量本身已有真实覆盖。
|
||||||
|
|
||||||
|
真实可执行的 V1.1 E2E 见:`tests/e2e/run_v11_tests.py`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.e2e
|
|
||||||
class TestHomeV11:
|
|
||||||
"""V1.1 首页 E2E 测试"""
|
|
||||||
|
|
||||||
def test_home_api_returns_user_state(self, api_client, authenticated_user):
|
|
||||||
"""GET /api/v1/home 返回 userState"""
|
|
||||||
resp = api_client.get("/api/v1/home")
|
|
||||||
assert resp.status_code == 200
|
|
||||||
data = resp.json()
|
|
||||||
assert "userState" in data
|
|
||||||
|
|
||||||
def test_home_banners_filtered_by_time(self, api_client, banner_factory):
|
|
||||||
"""Banner按时间过滤:未开始和已过期的Banner不显示"""
|
|
||||||
# TODO: P07 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_home_banners_filtered_by_audience(self, api_client, banner_factory, user_factory):
|
|
||||||
"""Banner按用户状态过滤:audience_scope不匹配时不显示"""
|
|
||||||
# TODO: P07 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_home_quick_entries_fixed_order(self, api_client):
|
|
||||||
"""首页8核心入口顺序固定"""
|
|
||||||
# TODO: P07 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_unverified_user_no_recommendations(self, api_client, unverified_user):
|
|
||||||
"""未审核用户不返回真实推荐"""
|
|
||||||
resp = api_client.get("/api/v1/home")
|
|
||||||
data = resp.json()
|
|
||||||
assert data.get("recommendations") == [] or data.get("recommendations") is None
|
|
||||||
|
|
||||||
def test_paused_user_no_new_recommendations(self, api_client, paused_user):
|
|
||||||
"""Paused状态用户不返回新推荐"""
|
|
||||||
resp = api_client.get("/api/v1/home")
|
|
||||||
data = resp.json()
|
|
||||||
# Paused用户应有推荐区降级提示,而非空列表
|
|
||||||
assert "recommendations" in data or "userState" in data
|
|
||||||
|
|
||||||
def test_successful_user_no_match_recommendations(self, api_client, successful_user):
|
|
||||||
"""Successful用户不返回找对象推荐"""
|
|
||||||
resp = api_client.get("/api/v1/home")
|
|
||||||
data = resp.json()
|
|
||||||
# Successful用户首页应转为公益服务推荐
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_service_location_shown_when_available(self, api_client, service_location_factory):
|
|
||||||
"""服务站可展示"""
|
|
||||||
# TODO: P19 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_banner_cannot_link_external_url(self, admin_client):
|
|
||||||
"""Banner不能创建外部URL跳转"""
|
|
||||||
# TODO: P19 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.e2e
|
|
||||||
class TestCapabilityService:
|
|
||||||
"""CapabilityService 能力判断测试"""
|
|
||||||
|
|
||||||
def test_can_receive_recommendations(self, api_client, searching_user):
|
|
||||||
"""Searching用户canReceiveRecommendations=true"""
|
|
||||||
resp = api_client.get("/api/v1/home")
|
|
||||||
data = resp.json()
|
|
||||||
caps = data.get("capabilities", {})
|
|
||||||
assert caps.get("canReceiveRecommendations") is True
|
|
||||||
|
|
||||||
def test_paused_user_cannot_receive_recommendations(self, api_client, paused_user):
|
|
||||||
"""Paused用户canReceiveRecommendations=false"""
|
|
||||||
resp = api_client.get("/api/v1/home")
|
|
||||||
data = resp.json()
|
|
||||||
caps = data.get("capabilities", {})
|
|
||||||
assert caps.get("canReceiveRecommendations") is False
|
|
||||||
|
|
||||||
def test_counseling_enabled_capability(self, api_client, verified_user):
|
|
||||||
"""counselingEnabled flag控制canApplyCounseling"""
|
|
||||||
# TODO: P22 实现后完善
|
|
||||||
pass
|
|
||||||
|
|||||||
@@ -1,72 +1,39 @@
|
|||||||
"""
|
"""
|
||||||
V1.1 本地资格 E2E 测试骨架
|
V1.1 本地资格 E2E —— 空壳已移除(Phase 23 KEEP-VERIFY,2026-09-18)
|
||||||
测试项目: 本地资格声明 → 佐证上传 → 审核流程
|
|
||||||
|
历史问题
|
||||||
|
--------
|
||||||
|
原文件 11 个测试函数**全部**为 `pass` 空壳(注释均为「TODO: P04/P05/P06 实现后完善」),
|
||||||
|
且依赖 `api_client` / `verified_user` / `local_eligibility_factory` 等
|
||||||
|
**在本仓库中根本不存在**的 pytest fixture(全局无 `conftest.py`)。
|
||||||
|
这些用例从未执行过,却在 `docs/STATUS.md` 中被计为 P23-T05「本地资格 E2E ✅」。
|
||||||
|
|
||||||
|
计划 Phase 23 Gate:「**不允许 skip/pass 空壳**」。
|
||||||
|
|
||||||
|
当前状态
|
||||||
|
--------
|
||||||
|
本文件现**不含任何测试函数**,pytest 将收集到 0 个用例 ——
|
||||||
|
以「没有假覆盖」取代「假装有覆盖」。
|
||||||
|
|
||||||
|
仍待真实实现的场景(需求不因移除空壳而丢失)
|
||||||
|
------------------------------------------
|
||||||
|
1. 用户可声明丹阳工作 / 户籍 / 居住
|
||||||
|
2. 声明本地资格不默认要求上传佐证
|
||||||
|
3. 佐证文件上传需要鉴权
|
||||||
|
4. 审核员可访问本地资格佐证(INV-011)
|
||||||
|
5. 红娘不可访问本地资格佐证(INV-011)
|
||||||
|
6. `/me/status` 返回 localEligibilityStatus
|
||||||
|
7. 本地资格未核验影响匹配资格
|
||||||
|
8. 本地资格审核进入 `review_type=local_eligibility` 队列
|
||||||
|
9. 审核通过更新 local_eligibility 状态
|
||||||
|
10. 需补充佐证时触发用户上传
|
||||||
|
11. 查看佐证文件须记录审计日志
|
||||||
|
|
||||||
|
现状补充(Phase 23 实测)
|
||||||
|
------------------------
|
||||||
|
`LocalEligibilityService` 全部方法均以 `userId` 自限定、**无任何 role 分支**,
|
||||||
|
佐证无独立的服务端读取入口(仅经受权限控制的审核队列 `ReviewController` 访问),
|
||||||
|
故 INV-011 在结构上成立;但缺少显式守卫测试,列为遗留待补。
|
||||||
|
|
||||||
|
真实可执行的 V1.1 E2E 见:`tests/e2e/run_v11_tests.py`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.e2e
|
|
||||||
class TestLocalEligibilityE2E:
|
|
||||||
"""本地资格 E2E 全流程测试"""
|
|
||||||
|
|
||||||
def test_declare_local_eligibility(self, api_client, verified_user):
|
|
||||||
"""用户可声明丹阳工作/户籍/居住"""
|
|
||||||
# POST /api/v1/local-eligibility/declaration
|
|
||||||
# TODO: P04 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_declare_does_not_require_evidence_by_default(self, api_client, verified_user):
|
|
||||||
"""声明本地资格不默认要求上传佐证"""
|
|
||||||
# TODO: P04 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_evidence_upload_requires_permission(self, api_client, verified_user):
|
|
||||||
"""佐证文件上传需要鉴权"""
|
|
||||||
# TODO: P04 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_reviewer_can_access_evidence(self, operator_client, local_eligibility_factory):
|
|
||||||
"""审核员可访问本地资格佐证"""
|
|
||||||
# TODO: P05 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_matchmaker_cannot_access_evidence(self, matchmaker_client, local_eligibility_factory):
|
|
||||||
"""红娘不可访问本地资格佐证(INV-011)"""
|
|
||||||
# TODO: P13 + P05 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_local_eligibility_status_in_me_status(self, api_client, verified_user):
|
|
||||||
"""/me/status 返回 localEligibilityStatus"""
|
|
||||||
# TODO: P04 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_local_eligibility_affects_match_eligibility(self, api_client, user_factory):
|
|
||||||
"""本地资格未核验影响匹配资格"""
|
|
||||||
# TODO: P06 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.e2e
|
|
||||||
class TestLocalEligibilityReview:
|
|
||||||
"""本地资格审核流程测试"""
|
|
||||||
|
|
||||||
def test_review_queue_local_eligibility_type(self, operator_client):
|
|
||||||
"""本地资格审核进入 review_type=local_eligibility 队列"""
|
|
||||||
# TODO: P05 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_approve_updates_eligibility_status(self, operator_client, local_eligibility_factory):
|
|
||||||
"""审核通过更新 local_eligibility 状态"""
|
|
||||||
# TODO: P05 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_need_evidence_triggers_upload(self, operator_client, local_eligibility_factory):
|
|
||||||
"""需补充佐证触发用户上传"""
|
|
||||||
# TODO: P05 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_evidence_access_logged(self, operator_client, local_eligibility_factory):
|
|
||||||
"""查看佐证文件记录审计日志"""
|
|
||||||
# TODO: P05 实现后完善
|
|
||||||
pass
|
|
||||||
|
|||||||
@@ -1,46 +1,35 @@
|
|||||||
"""
|
"""
|
||||||
V1.1 服务预约 E2E 测试骨架
|
V1.1 服务预约 E2E —— 空壳已移除(Phase 23 KEEP-VERIFY,2026-09-18)
|
||||||
测试项目: 服务预约全流程 + 并发安全
|
|
||||||
|
历史问题
|
||||||
|
--------
|
||||||
|
原文件 7 个测试函数**全部**为 `pass` 空壳(注释「TODO: P19 实现后完善」),
|
||||||
|
依赖不存在的 `service_location_factory` / `appointment_factory` / `operator_client`
|
||||||
|
等 fixture(全局无 `conftest.py`),从未执行过,
|
||||||
|
却在 `docs/STATUS.md` 中被计为 P23-T08「服务预约 E2E ✅」。
|
||||||
|
|
||||||
|
计划 Phase 23 Gate:「**不允许 skip/pass 空壳**」。
|
||||||
|
|
||||||
|
当前状态
|
||||||
|
--------
|
||||||
|
本文件现**不含任何测试函数**,pytest 收集 0 个用例。
|
||||||
|
|
||||||
|
仍待真实实现的场景(需求不因移除空壳而丢失)
|
||||||
|
------------------------------------------
|
||||||
|
1. 用户可提交服务预约
|
||||||
|
2. 预约提交支持幂等(Idempotency-Key)
|
||||||
|
3. 后台可确认预约
|
||||||
|
4. 同一预约同时取消与确认的并发安全性
|
||||||
|
5. 我的预约列表正确
|
||||||
|
6. 待确认状态可取消
|
||||||
|
7. 已确认状态不可自行取消
|
||||||
|
|
||||||
|
现状补充(Phase 23 实测)
|
||||||
|
------------------------
|
||||||
|
预约 owner 归属与数据范围已有真实后端测试:
|
||||||
|
`ServiceAppointmentOwnerGuardTest`、`ServiceAppointmentDataScopeTest`。
|
||||||
|
预约创建链路在 `tests/e2e/run_v11_tests.py::test_06_service_appointment`
|
||||||
|
中有真实 HTTP 覆盖。E2E 层其余场景待补。
|
||||||
|
|
||||||
|
真实可执行的 V1.1 E2E 见:`tests/e2e/run_v11_tests.py`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import pytest
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.e2e
|
|
||||||
class TestServiceAppointmentE2E:
|
|
||||||
"""服务预约 E2E 测试"""
|
|
||||||
|
|
||||||
def test_submit_appointment(self, api_client, service_location_factory):
|
|
||||||
"""用户可提交服务预约"""
|
|
||||||
# TODO: P19 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_appointment_idempotent(self, api_client, service_location_factory):
|
|
||||||
"""预约提交支持幂等(Idempotency-Key)"""
|
|
||||||
# TODO: P19 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_operator_can_confirm(self, operator_client, appointment_factory):
|
|
||||||
"""后台可确认预约"""
|
|
||||||
# TODO: P19 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_appointment_concurrent_safety(self, api_client, service_location_factory):
|
|
||||||
"""同一预约同时取消和确认的并发安全性"""
|
|
||||||
# TODO: P19 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_my_appointments_list(self, api_client, appointment_factory):
|
|
||||||
"""我的预约列表正确"""
|
|
||||||
# TODO: P19 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_cancel_before_confirmation(self, api_client, appointment_factory):
|
|
||||||
"""待确认状态可取消"""
|
|
||||||
# TODO: P19 实现后完善
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_cancel_after_confirmation_denied(self, api_client, appointment_factory):
|
|
||||||
"""已确认状态不可自行取消"""
|
|
||||||
# TODO: P19 实现后完善
|
|
||||||
pass
|
|
||||||
|
|||||||
Reference in New Issue
Block a user