Просмотр исходного кода

test(闭环): 第75轮 全量实测 2903例/2784过=95.90% + 新增18例SRS用例

- 后端(隔离树 bca7dec03): 289类/2398例/2279过=95.04%(剔环境95.69%), 35红类/115红例, 32:59 BUILD SUCCESS
- B-53 量化: 带研发WIP同11类 170例 48红→6红(42例可转绿), 后端可达96.79%, 第三次催办
- 浏览器+契约 25/25 全绿(admin 5/5, frontend 6/6, 健康契约 10/10, 商城排序 4/4)
- langgraph 59绿/0红/12skip(+8); frontend 313; web 90
- 新增用例 18例: TC-C16-01~05(C-16裁剪) TC-C10-01~06(C-10通知) TC-FA-18~24(家庭加入身份)
- blockers: 新增 B-57(用例断言错设已自处置), B-53 量化证据
E2E Test Bot 5 дней назад
Родитель
Сommit
8e8d495524

+ 159 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/family/FamilyInviteApproveRequestTest.java

@@ -0,0 +1,159 @@
+package com.etotem.cfc.controller.family;
+
+import com.etotem.cfc.dto.JoinApproveDTO;
+import com.etotem.cfc.service.FamilyJoinRequestService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.http.MediaType;
+import org.springframework.test.util.ReflectionTestUtils;
+import org.springframework.test.web.servlet.MockMvc;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+/**
+ * TC-FA-18~23 · 家庭加入「身份由邀请人审批时指定」契约.
+ *
+ * <p>场景来源:SRS 2026-11-07-family-join-dynamic-identity.md(e2eb3617d / 6567acc33 / 5bd559b6f)。
+ * 契约:{@code POST /api/family/invite/approve-request}
+ * 必须携带 {@code generationLevel}(辈分),{@code generationLevel=peer} 时还必须携带 {@code peerType};
+ * 缺参一律拒绝且不触达 service。</p>
+ *
+ * <p>MockMvc standalone + Mockito,不依赖数据库/微信/外部服务。</p>
+ */
+public class FamilyInviteApproveRequestTest {
+
+    @Mock
+    private FamilyJoinRequestService familyJoinRequestService;
+
+    private MockMvc mockMvc;
+
+    private static final String URL = "/api/family/invite/approve-request";
+
+    @BeforeEach
+    public void setUp() {
+        MockitoAnnotations.openMocks(this);
+        FamilyInviteController controller = new FamilyInviteController();
+        ReflectionTestUtils.setField(controller, "familyJoinRequestService", familyJoinRequestService);
+        mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
+    }
+
+    private String body(String requestId, String generationLevel, String peerType) {
+        StringBuilder sb = new StringBuilder("{");
+        if (requestId != null) {
+            sb.append("\"requestId\":").append(requestId).append(",");
+        }
+        if (generationLevel != null) {
+            sb.append("\"generationLevel\":\"").append(generationLevel).append("\",");
+        }
+        if (peerType != null) {
+            sb.append("\"peerType\":\"").append(peerType).append("\",");
+        }
+        if (sb.charAt(sb.length() - 1) == ',') {
+            sb.setLength(sb.length() - 1);
+        }
+        return sb.append("}").toString();
+    }
+
+    @Test
+    @DisplayName("TC-FA-18 未登录审批(无 userId 属性):400 绑定层拒绝且不触达 service")
+    public void anonymousApproveIsRejected() throws Exception {
+        mockMvc.perform(post(URL).contentType(MediaType.APPLICATION_JSON)
+                        .content(body("1", "parent", null)))
+                .andExpect(status().isBadRequest());
+
+        verify(familyJoinRequestService, never())
+                .approveJoinRequest(anyLong(), anyLong(), anyString(), anyString(), anyString());
+    }
+
+    @Test
+    @DisplayName("TC-FA-19 requestId 缺失:拒绝且不触达 service")
+    public void missingRequestIdIsRejected() throws Exception {
+        mockMvc.perform(post(URL).requestAttr("userId", 1L)
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(body(null, "parent", null)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.message").value("请求ID不能为空"));
+
+        verify(familyJoinRequestService, never())
+                .approveJoinRequest(anyLong(), anyLong(), anyString(), anyString(), anyString());
+    }
+
+    @Test
+    @DisplayName("TC-FA-20 generationLevel 缺失:拒绝(身份必须由邀请人指定)")
+    public void missingGenerationLevelIsRejected() throws Exception {
+        mockMvc.perform(post(URL).requestAttr("userId", 1L)
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(body("10", null, null)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.message").value("请选择辈分关系"));
+
+        verify(familyJoinRequestService, never())
+                .approveJoinRequest(anyLong(), anyLong(), anyString(), anyString(), anyString());
+    }
+
+    @Test
+    @DisplayName("TC-FA-21 peer 辈分缺 peerType:拒绝(同辈类型必填)")
+    public void peerWithoutPeerTypeIsRejected() throws Exception {
+        mockMvc.perform(post(URL).requestAttr("userId", 1L)
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(body("10", "peer", null)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.message").value("请选择同辈关系类型"));
+
+        verify(familyJoinRequestService, never())
+                .approveJoinRequest(anyLong(), anyLong(), anyString(), anyString(), anyString());
+    }
+
+    @Test
+    @DisplayName("TC-FA-22 正常审批(parent):参数完整下传 service")
+    public void validParentApproveIsDelegated() throws Exception {
+        mockMvc.perform(post(URL).requestAttr("userId", 7L)
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(body("10", "parent", null)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200));
+
+        verify(familyJoinRequestService, times(1))
+                .approveJoinRequest(eq(10L), eq(7L), eq(""), eq("parent"), any());
+    }
+
+    @Test
+    @DisplayName("TC-FA-23 正常审批(peer+sibling):同辈类型下传 service")
+    public void validPeerApproveIsDelegated() throws Exception {
+        mockMvc.perform(post(URL).requestAttr("userId", 7L)
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(body("11", "peer", "sibling")))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.code").value(200));
+
+        verify(familyJoinRequestService, times(1))
+                .approveJoinRequest(eq(11L), eq(7L), eq(""), eq("peer"), eq("sibling"));
+    }
+
+    @Test
+    @DisplayName("TC-FA-24 service 抛异常:错误信息透传,不 5xx")
+    public void serviceFailureReturnsErrorResult() throws Exception {
+        doThrow(new RuntimeException("该申请已被处理")).when(familyJoinRequestService)
+                .approveJoinRequest(anyLong(), anyLong(), anyString(), anyString(), any());
+
+        mockMvc.perform(post(URL).requestAttr("userId", 7L)
+                        .contentType(MediaType.APPLICATION_JSON)
+                        .content(body("12", "child", null)))
+                .andExpect(status().isOk())
+                .andExpect(jsonPath("$.message").value("该申请已被处理"));
+    }
+}

+ 166 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/BadgeServiceUnlockNoticeTest.java

@@ -0,0 +1,166 @@
+package com.etotem.cfc.unit;
+
+import com.etotem.cfc.entity.Badge;
+import com.etotem.cfc.entity.FamilyMember;
+import com.etotem.cfc.entity.MemberBadge;
+import com.etotem.cfc.mapper.BadgeMapper;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
+import com.etotem.cfc.mapper.MemberBadgeMapper;
+import com.etotem.cfc.service.BadgeService;
+import com.etotem.cfc.service.NotificationDispatchService;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * C-10 · 勋章解锁站内通知(badge.unlock.notice)契约级单测.
+ *
+ * <p>场景来源:SRS C-10「勋章解锁站内通知」(36b671565 落地)。
+ * 契约:{@code BadgeService.grantBadge} 授予成功后写 {@code user_notifications}(type=badge_unlock);
+ * 可登录成员通知本人,不可登录成员由同家庭能登录成员代收;
+ * 通知失败不阻断授予;重复授予不重复通知。</p>
+ *
+ * <p>纯单元测试:Mockito + 反射注入,不依赖数据库/事务/外部服务。</p>
+ */
+public class BadgeServiceUnlockNoticeTest {
+
+    @Mock
+    private BadgeMapper badgeMapper;
+
+    @Mock
+    private MemberBadgeMapper memberBadgeMapper;
+
+    @Mock
+    private FamilyMemberMapper familyMemberMapper;
+
+    @Mock
+    private NotificationDispatchService notificationDispatchService;
+
+    private BadgeService badgeService;
+
+    private static final Long MEMBER_ID = 2001L;
+    private static final Long BADGE_ID = 3001L;
+    private static final Long OWNER_USER_ID = 9001L;
+    private static final Long PROXY_USER_ID = 9002L;
+
+    @BeforeEach
+    public void setUp() {
+        MockitoAnnotations.openMocks(this);
+        badgeService = new BadgeService();
+        ReflectionTestUtils.setField(badgeService, "badgeMapper", badgeMapper);
+        ReflectionTestUtils.setField(badgeService, "memberBadgeMapper", memberBadgeMapper);
+        ReflectionTestUtils.setField(badgeService, "familyMemberMapper", familyMemberMapper);
+        ReflectionTestUtils.setField(badgeService, "notificationDispatchService", notificationDispatchService);
+
+        Badge badge = new Badge();
+        badge.setId(BADGE_ID);
+        badge.setBadgeId("b_first_task");
+        badge.setName("初次启程");
+        when(badgeMapper.selectById(BADGE_ID)).thenReturn(badge);
+        when(memberBadgeMapper.selectOne(any())).thenReturn(null);
+    }
+
+    private FamilyMember member(Long userId, Long familyId) {
+        FamilyMember m = new FamilyMember();
+        m.setId(MEMBER_ID);
+        m.setUserId(userId);
+        m.setFamilyId(familyId);
+        return m;
+    }
+
+    @Test
+    @DisplayName("TC-C10-01 可登录成员:通知发往本人")
+    public void noticeSentToLoginableMember() {
+        when(familyMemberMapper.selectById(MEMBER_ID)).thenReturn(member(OWNER_USER_ID, 10L));
+
+        badgeService.grantBadge(MEMBER_ID, BADGE_ID);
+
+        verify(memberBadgeMapper, times(1)).insert(any(MemberBadge.class));
+        verify(notificationDispatchService, times(1))
+                .sendByTemplate(eq("badge_unlock"), eq(OWNER_USER_ID), any(Map.class),
+                        eq("badge"), eq(BADGE_ID), anyString(), anyString());
+    }
+
+    @Test
+    @DisplayName("TC-C10-02 不可登录成员:由同家庭能登录成员代收")
+    public void noticeSentToProxyOfSameFamily() {
+        when(familyMemberMapper.selectById(MEMBER_ID)).thenReturn(member(null, 10L));
+        when(familyMemberMapper.selectOne(any())).thenReturn(member(PROXY_USER_ID, 10L));
+
+        badgeService.grantBadge(MEMBER_ID, BADGE_ID);
+
+        verify(notificationDispatchService, times(1))
+                .sendByTemplate(eq("badge_unlock"), eq(PROXY_USER_ID), any(Map.class),
+                        eq("badge"), eq(BADGE_ID), anyString(), anyString());
+    }
+
+    @Test
+    @DisplayName("TC-C10-03 无代管用户:不发送通知,但授予仍成功")
+    public void noProxyMeansNoNoticeButGrantSucceeds() {
+        when(familyMemberMapper.selectById(MEMBER_ID)).thenReturn(member(null, 10L));
+        when(familyMemberMapper.selectOne(any())).thenReturn(null);
+
+        badgeService.grantBadge(MEMBER_ID, BADGE_ID);
+
+        verify(memberBadgeMapper, times(1)).insert(any(MemberBadge.class));
+        verify(notificationDispatchService, never())
+                .sendByTemplate(anyString(), anyLong(), any(Map.class), anyString(), anyLong(),
+                        anyString(), anyString());
+    }
+
+    @Test
+    @DisplayName("TC-C10-04 通知发送失败:不阻断勋章授予(不抛异常)")
+    public void noticeFailureDoesNotBreakGrant() {
+        when(familyMemberMapper.selectById(MEMBER_ID)).thenReturn(member(OWNER_USER_ID, 10L));
+        doThrow(new RuntimeException("模板缺失")).when(notificationDispatchService)
+                .sendByTemplate(anyString(), anyLong(), any(Map.class), anyString(), anyLong(),
+                        anyString(), anyString());
+
+        assertDoesNotThrow(() -> badgeService.grantBadge(MEMBER_ID, BADGE_ID));
+        verify(memberBadgeMapper, times(1)).insert(any(MemberBadge.class));
+    }
+
+    @Test
+    @DisplayName("TC-C10-05 重复授予:不重复插入、不重复通知(幂等)")
+    public void duplicateGrantIsIdempotent() {
+        when(memberBadgeMapper.selectOne(any())).thenReturn(new MemberBadge());
+
+        badgeService.grantBadge(MEMBER_ID, BADGE_ID);
+
+        verify(memberBadgeMapper, never()).insert(any(MemberBadge.class));
+        verify(notificationDispatchService, never())
+                .sendByTemplate(anyString(), anyLong(), any(Map.class), anyString(), anyLong(),
+                        anyString(), anyString());
+    }
+
+    @Test
+    @DisplayName("TC-C10-06 通知文案变量含勋章名(模板渲染输入正确)")
+    public void noticeVarsCarryBadgeName() {
+        when(familyMemberMapper.selectById(MEMBER_ID)).thenReturn(member(OWNER_USER_ID, 10L));
+
+        badgeService.grantBadge(MEMBER_ID, BADGE_ID);
+
+        ArgumentCaptor<Map> captor = ArgumentCaptor.forClass(Map.class);
+        verify(notificationDispatchService).sendByTemplate(eq("badge_unlock"), eq(OWNER_USER_ID),
+                captor.capture(), eq("badge"), eq(BADGE_ID), anyString(), anyString());
+        assertEquals("初次启程", captor.getValue().get("badgeName"));
+    }
+}

+ 87 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/UserServiceMaskChildSensitiveFieldsTest.java

@@ -0,0 +1,87 @@
+package com.etotem.cfc.unit;
+
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.service.UserService;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+/**
+ * C-16 · child-zone-contract — 孩子端受限资料投影契约.
+ *
+ * <p>场景来源:SRS C-16「孩子端受限资料投影」(36b671565 落地)。
+ * 契约:{@link UserService#maskChildSensitiveFields(User)} 在 {@code role=child} 时就地裁剪
+ * idCard / phone / realName / openid / unionid 五类敏感字段;非 child 角色与 null 入参不改变任何字段。</p>
+ *
+ * <p>纯单元测试,不依赖数据库与 Spring 上下文。</p>
+ */
+public class UserServiceMaskChildSensitiveFieldsTest {
+
+    private static User buildUser(String role) {
+        User u = new User();
+        u.setId(1001L);
+        u.setRole(role);
+        u.setIdCard("110101199001011234");
+        u.setPhone("13800001111");
+        u.setRealName("张三");
+        u.setOpenid("openid-abc");
+        u.setUnionid("unionid-abc");
+        return u;
+    }
+
+    @Test
+    @DisplayName("TC-C16-01 孩子端:五类敏感字段全部裁剪")
+    public void childSensitiveFieldsAreMasked() {
+        User u = buildUser("child");
+        new UserService().maskChildSensitiveFields(u);
+
+        assertNull(u.getIdCard(), "idCard 应被裁剪");
+        assertNull(u.getPhone(), "phone 应被裁剪");
+        assertNull(u.getRealName(), "realName 应被裁剪");
+        assertNull(u.getOpenid(), "openid 应被裁剪");
+        assertNull(u.getUnionid(), "unionid 应被裁剪");
+        assertEquals(1001L, u.getId(), "非敏感标识 id 不应被裁剪");
+        assertEquals("child", u.getRole(), "role 为鉴权依据,不应被裁剪");
+    }
+
+    @Test
+    @DisplayName("TC-C16-02 家长端:资料完整不裁剪")
+    public void parentFieldsAreKept() {
+        User u = buildUser("parent");
+        new UserService().maskChildSensitiveFields(u);
+
+        assertEquals("110101199001011234", u.getIdCard());
+        assertEquals("13800001111", u.getPhone());
+        assertEquals("张三", u.getRealName());
+        assertEquals("openid-abc", u.getOpenid());
+        assertEquals("unionid-abc", u.getUnionid());
+    }
+
+    @Test
+    @DisplayName("TC-C16-03 规划师端:不误伤(角色判定仅 child 生效)")
+    public void teacherFieldsAreKept() {
+        User u = buildUser("teacher");
+        new UserService().maskChildSensitiveFields(u);
+
+        assertEquals("张三", u.getRealName());
+        assertEquals("13800001111", u.getPhone());
+    }
+
+    @Test
+    @DisplayName("TC-C16-04 空入参:不抛异常(防御性契约)")
+    public void nullUserDoesNotThrow() {
+        new UserService().maskChildSensitiveFields(null);
+    }
+
+    @Test
+    @DisplayName("TC-C16-05 角色为空:按非 child 处理,不裁剪")
+    public void nullRoleIsNotMasked() {
+        User u = buildUser(null);
+        new UserService().maskChildSensitiveFields(u);
+
+        assertEquals("张三", u.getRealName(), "role 为空不应判定为 child");
+        assertEquals("13800001111", u.getPhone());
+    }
+}

+ 281 - 0
docs/tdd/orchestration/report-test-2026-11-07-2305.md

@@ -0,0 +1,281 @@
+# 测试负责人工作汇报 · TDD 闭环自动化测试
+
+> 轮次:2026-11-07 23:05 GMT+8(定时闭环巡检)
+> 基线:`origin/cfclub` = `bca7dec03`(21:20 轮报告提交,工作区与远端 0/0 同步)
+> 执行档:review(离线);未联网、未安装依赖、未启停服务、未改生产代码
+> 拓扑执行实况:浏览器自动化 → 本机节点串行 25 例(admin 5 + frontend 6 + 健康契约 10 + 商城排序 4);189 节点 TCP 4090 OPEN 但 HTTP API 401(B-56 未解除)→ 不可派发;langgraph → 251 的 9000/4090 仍 CLOSED、80 为 openresty 404(B-55 未解除)→ 本地 venv 兜底
+
+---
+
+## 基本信息
+
+| 项目 | 内容 |
+|------|------|
+| 汇报人 | `cfc-tester`(测试负责人) |
+| 汇报周期 | 2026-11-07 23:05 GMT+8(定时闭环巡检,第 75 轮) |
+| 负责模块/单元 | 全栈测试闭环:cfc-backend JUnit + cfc-frontend + cfc-web + cfc-langgraph + 线上浏览器自动化(admin / 小程序 H5)+ 鉴权契约 + SRS 用例建设 |
+
+---
+
+## 一、本轮工作数据汇总
+
+### 1.1 测试用例建设
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **本轮新增测试用例数** | **18 个** | 补齐今日 3 条 SRS 变更的自动化绑定(详见 1.1b),全部实测通过 |
+| **本轮验证执行测试数** | **2903 个** | 后端 2398 + 本轮新增 18 + frontend 313 + web 90 + langgraph 59 + 浏览器/契约 25 |
+| **用例通过率** | **95.90%** | 2784 / 2903;未通过 119 例全部为后端既有红例(65F + 50E + 4S 计数口径见下) |
+
+**分套件明细(全部为本轮实测,非引用历史):**
+
+| 套件 | 执行 | 通过 | 失败 | 错误 | 跳过 | 通过率 | 耗时 | 结论 |
+|------|------|------|------|------|------|--------|------|------|
+| cfc-backend JUnit(隔离树 clean) | 2398 | 2279 | 65 | 50 | 4 | **95.04%** | 32:59 | ⚠️ 35 红类 / 115 红例 |
+| cfc-backend 本轮新增用例 | 18 | 18 | 0 | 0 | 0 | **100%** | 26s | ✅ TC-C16/C10/FA |
+| cfc-frontend(Jest 16 套件) | 313 | 313 | 0 | 0 | 0 | **100%** | 12.4s | ✅ EXIT 0 |
+| cfc-web(Jest 12 套件) | 90 | 90 | 0 | 0 | 0 | **100%** | 16.2s | ✅ EXIT 0 |
+| cfc-langgraph(pytest,`--ignore=tests/qna`) | 71 | 59 | 0 | 0 | 12 | **100%(执行口径)** | 221.5s | ✅ EXIT 0 |
+| 浏览器 admin-desktop(`cfc.iwintrue.com/admin`) | 5 | 5 | 0 | 0 | 0 | **100%** | 1.9m | ✅ TC-A-U01~U05 |
+| 浏览器 frontend-mobile(`/frontend`) | 6 | 6 | 0 | 0 | 0 | **100%** | 1.0m | ✅ TC-F-U01~U06 |
+| 健康鉴权契约(10 端点) | 10 | 10 | 0 | 0 | 0 | **100%** | 9.7s | ✅ B-50 保持关闭 |
+| 商城价格排序 API | 4 | 4 | 0 | 0 | 0 | **100%** | 10.7s | ✅ TC-S-U01~U04 |
+| tests 根级 Jest | 23 | 0 | 0 | 0 | 23 | — | 3.3s | ⚠️ 全 skip(B-36 未解除) |
+
+**后端度量校验(逐项复算,误差 0):**
+- 编译:main **2080** 源 + test **293** 源,0 错误;`BUILD SUCCESS`,`Total time 32:59`
+- 完整性:有 `@Test` 的测试类 **289/289** 均有 XML,**静默丢失 0**(脚本比对类名差集为空)
+- 恒等式:`@Test 2387 + 参数化展开 28(JwtInterceptorLoginRequiredPathTest: 2→30)− 早退截断 17(DataMigration 10 + TaskServiceMinigame 4 + TeacherLogin 3)= 2398`,误差 **0**
+- 环境红:**8 例 / 3 类**(`AdminFamilyOverviewControllerTest`、`FamilyAdminOverviewServiceTest`、`UserServiceFamilyNameLinkTest`),链路恒为硬编码 `127.0.0.1:3306`;剔除后通过率 **95.69%**
+- 2 份 `*.dump` 为 surefire `PpidChecker` 读父进程状态失败(Windows 环境噪声),**非用例崩溃**,对应 XML 均正常产出
+
+**与 20:10 基线(288 类 / 2389 例 / 95.02% / 35 红类 / 115 红例)对账:**
+
+| 维度 | 20:10 | 本轮 | Δ | 归因 |
+|------|-------|------|---|------|
+| 测试类 | 288 | 289 | +1 | 新增 `FamilyMemberRelationshipLabelTest`(8 例全绿,来自 `9e8dc5bd2`) |
+| 用例数 | 2389 | 2398 | +9 | 上述 8 例 + 1 例(`FamilyJoinRequestServiceReferrerBindTest` 新增用例) |
+| 通过 | 2270 | 2279 | +9 | 新增用例全绿,**无转红类** |
+| 失败/错误 | 65/50 | 65/50 | 0 | 逐项一致 |
+| 红类/红例 | 35/115 | 35/115 | 0 | 集合一致(差集为空) |
+
+### 1.1b SRS / 场景变更 → 用例更新(用户目标②的落地动作)
+
+本轮按「SRS、场景变更即更新用例」逐条核对今日 11 份 SRS 规格(`docs/superpowers/specs/2026-11-07-*.md`),按「规格内 API 路径是否被测试资产引用」量化覆盖,并对 0 覆盖项补写用例:
+
+| SRS 规格(今日变更) | 规格内 API 数 | 测试资产引用 | 本轮处置 |
+|----------------------|---------------|--------------|----------|
+| `family-join-dynamic-identity`(加入身份改由邀请人指定) | 5 | **0** | ✅ 新增 `FamilyInviteApproveRequestTest` **7 例**(TC-FA-18~24),覆盖未登录 400、requestId/辈分/同辈类型缺失、parent 与 peer+sibling 正常下发、service 异常透传 |
+| `child-zone-contract`(C-16 孩子端受限资料投影) | 40 | 27(**裁剪逻辑 0**) | ✅ 新增 `UserServiceMaskChildSensitiveFieldsTest` **5 例**(TC-C16-01~05),锁定 child 裁剪 5 字段 / parent、teacher、role 空不裁剪 / null 不抛 |
+| `badge`(C-10 勋章解锁站内通知) | — | 0(仅勋章查询) | ✅ 新增 `BadgeServiceUnlockNoticeTest` **6 例**(TC-C10-01~06),锁定本人通知、代管代收、无代收不发送、通知失败不阻断授予、重复授予幂等、模板变量 |
+| `shop-price-sort` | 1 | 1 | ✅ 已有 4 例(TC-S-U01~U04)本轮实跑通过,无需新增 |
+| `dx-health-contract`(华为/小米健康) | 16 | 4 | ✅ 鉴权契约 10 端点已覆盖并全绿;业务侧 12 端点仍缺口(登记 G-2) |
+| `ai-butler-contract` | 30 | 11 | ⚠️ 19 端点无引用 → 登记缺口(P1) |
+| `planner-zone-contract` | 11 | 4 | ⚠️ 7 端点无引用 → 登记缺口(P1) |
+| `parent-rest-contract` | 6 | 1 | ⚠️ 5 端点无引用 → 登记缺口(P1) |
+| `cross-role-contract` | 3 | 1 | ⚠️ 2 端点无引用 → 登记缺口(P2) |
+| `referral-team-profit-sharing`(分润 v0.7) | 3 | **0** | ❌ 0 引用 → 登记缺口(P1,口径今日刚拍板 15 项,待规格冻结后补) |
+| `service-provider-contract` | 1 | **0** | ❌ 0 引用 → 登记缺口(P2) |
+| `admin-ops-contract` | 1 | 1 | ✅ 已覆盖 |
+
+> 结论:3 条今日落地的代码级变更(C-16 / C-10 / 家庭加入身份)已从「0 自动化」变为「18 例自动化实测通过」;分润、ai-butler、planner-zone、parent-rest 仍为规格先于用例(缺口已登记,非静默放过)。
+
+### 1.2 会话与任务分发
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **分派任务数** | 2 个(尝试) | 浏览器 → `192.168.16.189:4090`;langgraph → `192.168.16.251` |
+| **分派成功数** | **0 个** | 189 的 HTTP API 返回 `{"_tag":"UnauthorizedError","message":"Authentication required"}`(需凭证,B-56);251 无 langgraph 端口(9000/4090 CLOSED,80 为 openresty 404,B-55) |
+| **直接执行数** | 6 个套件 | 后端全量、frontend、web、langgraph、浏览器 25 例、新增用例 18 例 | 
+| **恢复智能体会话数** | 0 个 | — |
+| **当前在用会话数** | 0 个 | 全部串行完成并退出 |
+
+### 1.2b 各会话当前工作状态列表
+
+| 会话标签 | 会话 ID | 当前状态 | 当前任务 | 静默/空闲时长 | 工作目录 | 备注 |
+|----------|---------|----------|----------|---------------|----------|------|
+| 本机 204(浏览器节点) | 直跑 | done | admin 5/5 + frontend 6/6 + 健康契约 10/10 + 商城 4/4 | — | `d:/cfc/tests`(**必须在 tests/ 内执行**,B-52 教训) | workers=1 串行,遵守「每节点单任务」 |
+| 本机 204(后端隔离树) | `D:\_wt_r2149` | done | `mvn -o -B clean test` 全量 | — | 隔离树 @`bca7dec03` | 跑前跑后 SHA 一致;主仓 73 项 WIP 零侵入 |
+| web-189-4090 | — | blocked | admin 浏览器自动化 | — | — | TCP 4090 OPEN,API 401 未鉴权 → 未派发 |
+| opencode 251(langgraph) | — | blocked | langgraph pytest | — | — | 9000/4090 CLOSED → 本地 venv 兜底 59/0/12 |
+
+### 1.3 问题处置
+
+| 指标 | 数量 | 填写说明 |
+|------|------|----------|
+| **新测试发现问题数** | 1 个 | B-57(测试用例自身断言错设:`@RequestAttribute` 缺失在绑定层即 400,非业务分支)——本轮已自处置闭环 |
+| **已解决测试相关问题数** | 1 个 | B-57(断言改为 400 + 补验「不触达 service」,18/18 绿) |
+| **待解决测试相关问题数** | 6 个 | B-47(已修待确认长期稳定)、B-53(P0·WIP 未提交)、B-54(langgraph 环境)、B-55(节点不可达)、B-56(派发鉴权/执行链路)、B-36(根级 23 例全 skip) |
+
+### 1.4 本轮 git 提交与推送证据
+
+| 提交 sha | 关联任务/能力键 | 阶段 | 触及文件类型 | 目标分支 | 已推送远端 | 备注 |
+|----------|-----------------|------|--------------|----------|------------|------|
+| `_见文末回填_` | 测试负责人闭环汇报 + 18 例新用例 + blockers B-57 | 报告/用例/台账 | md + java | `origin/cfclub` | `_见文末回填_` | 新增 3 个测试类为本轮作者(测试侧),**不触碰研发 73 项 WIP** |
+
+### 1.5 状态机阶段分布(全局进度)
+
+本轮为闭环巡检 + 用例补齐,未激活 red→yellow→green 主循环;账本维持 **14 DONE + 8 GREEN**(研发第 74 轮 21:03 复核:22 键证据 sha 全部 IN CHAIN)。
+
+### 1.6 数据源健康度
+
+| 数据源 | 读取键数 | 是否正常 | 备注 |
+|--------|----------|----------|------|
+| 需求侧(SRS 池) | 0 | **异常(未启用)** | `srs_section` 锚点 0,目标②仍不可量化(G-9) |
+| 契约常量(contract) | 正常 | 正常 | — |
+| 测试用例引用(test_case_map) | 784 | 正常 | 结构化用例 784 条 |
+| 执行结果回填(case_results) | ~1203 | 正常 | 侧车 `test-progress.json` |
+
+**静态度量(本轮实算,非沿用):** 测试文件 293(`@Test` 2387、`@Disabled` 4)|控制器 361|`@GetMapping` **8 处**(仍违 AGENTS.md「统一 `@PostMapping`」)|Playwright spec 94 个 / `test()` 568 处。
+
+---
+
+## 二、向研发提出的改进建议
+
+| 序号 | 建议标题 | 问题描述 | 根因分析 | 建议方案 | 优先级 | 状态 |
+|------|----------|----------|----------|----------|--------|------|
+| 1 | **B-53·WIP 未提交(P0)** | 主工作区 73 项未提交,含 11 个后端测试文件修复。本轮把它们带入隔离树定向复跑同 11 类:**170 例 4F+2E = 6 红**(clean 基线同 11 类为 48 红)→ **42 例可立即转绿**,后端通过率 95.04% → **96.79%**(越 G1)、红例 115 → **73** | 「已修未提交 = 未完成」(O-5) | 产生 WIP 的会话对这 11 个文件执行 `git add` + commit + push;PR 只含测试文件、不含生产代码 | P0 | 待处理(连续 3 轮未收口) |
+| 2 | **B-47 术后观察(P0→P1)** | 主干已恢复 JDK8 可编译(`636c42103`),本轮全量 2398 例跑通、testCompile 293 源 0 错 | 单点 JDK10+ API | 保持;建议 CI 增 `test-compile` 硬门禁,防止同类回归再次阻断 2398 例 | P1 | 已修复,待 CI 守护 |
+| 3 | **B-55 节点不可达(P1)** | `251:9000/4090` CLOSED、80 为 openresty 404;`189:4090` 可达但 API 401 | 运维端口/鉴权 | 放通 251 的 9000;为 189 配置 `auth` 与 `remote_dir`(B-42 已定位版本/目录不一致) | P1 | 待处理 |
+| 4 | **B-56 派发不生效(P2)** | 189 会话创建后 agent 零产出;本轮更进一步:API 直接 401 | 鉴权缺失 + agent run 未触发 | 补 `Authorization` 与 `POST /session/{id}/prompt` 的 run 触发;或改 `opencode run` 直执行 playwright | P2 | 待处理 |
+| 5 | **B-36 根级 23 例全 skip(P1)** | `tests` 根级 Jest 2 套件 23 例全部 skip(环境门控),接口层黑盒覆盖实际为 0 | 门控条件过严 | 改为「无外部依赖则本地 mock 跑、有依赖才 skip」,并把可跑子集纳入 CI | P1 | 待处理 |
+| 6 | **AI-butler / planner-zone / parent-rest 规格先行(P1)** | 三份规格 47 个端点中仅 16 个被测试资产引用(34%) | 规格先于用例产出 | 规格冻结后 5 个工作日内补契约级 MockMvc 用例(本轮已给出 C-16/FA 的可复用模板) | P1 | 待处理 |
+| 7 | **`@GetMapping` 8 处仍违规范(P2)** | `AGENTS.md` 要求统一 `@PostMapping`,实测 8 处 `@GetMapping`(3 文件) | 历史遗留 | 统一改 `@PostMapping` 并同步前端调用 | P2 | 待处理 |
+
+---
+
+## 三、本轮阻塞与风险
+
+| 阻塞项 | 影响范围 | 阻塞类型 | 处置进展 | 需要协调 |
+|--------|----------|----------|----------|----------|
+| B-53 WIP 未提交 | 主干后端通过率卡在 95.04%(差 1.75pp 到 96.79%) | 研发/流程 | 已量化「42 例 / +1.75pp」;测试侧不代提交 | WIP 归属会话 |
+| B-55 / B-56 节点分派 | 浏览器与 langgraph 无法按拓扑远程执行 | 运维/平台 | 本机单节点串行兜底,25 例全绿 | 运维放通 + 配置鉴权 |
+| B-54 langgraph 环境 | 12 例 skip(chromadb 原生依赖 / 凭据) | 环境 | 本轮 59 绿 0 红,未见回弹 | 装 VC++ 运行时或重建 venv |
+| SRS 池未启用 | 目标②覆盖率不可量化 | 需求/测试 | 本轮改用「规格 API 引用率」近似量化(见 1.1b) | 产品/测试启用 SRS 池 |
+
+---
+
+## 四、下轮工作计划
+
+| 计划项 | 前置依赖 | 备注 |
+|--------|----------|------|
+| 研发收口 B-53 后重跑后端全量,验证 ≥96.79% | 11 文件入库 | 本轮已备好对照数据 |
+| 189 配置鉴权 + `remote_dir` 后,admin 任务改派 189(本机只跑 frontend),落实「每节点单任务」 | B-56 修复 | 当前 25 例共用本机节点串行 |
+| 251 放通 9000 后派发 langgraph | B-55 修复 | 当前本地兜底 |
+| 按 1.1b 缺口表补 ai-butler(19)/ planner-zone(7)/ parent-rest(5)/ 分润(3)契约用例 | 规格冻结 | 复用本轮 3 个模板类 |
+| 根级 23 例门控改造,恢复接口层黑盒覆盖 | B-36 | — |
+
+---
+
+## 五、覆盖缺口清单(按优先级)
+
+| ID | 缺口类型 | 现状 | 风险 | 建议测试设计 | 优先级 | 闭环方式 |
+|----|----------|------|------|--------------|--------|----------|
+| G-1 | 后端红例 | 115 例 / 35 类(A 类夹具为主 + 环境 8) | 通过率 95.04% 不达 96% | ① 研发收口 B-53(42 例)② 修 3 类硬编码 3306(8 例)③ 余 65 例按堆栈首帧逐类修夹具 | P0/P1 | 全量复跑 ≥96% |
+| G-2 | 健康业务侧 | 鉴权 10 端点已全绿;同步/授权业务 12 端点无用例 | 数据同步正确性无回归网 | 契约级 MockMvc(mock huawei/xiaomi client)+ 1 条真实授权流 E2E | P1 | 新增用例 + 全绿 |
+| G-3 | 分润 v0.7 | 0 引用(今日刚拍板 15 项) | 口径变更无回归保护 | 分润计算单测(团队奖/星级份数/最少层数)+ 结算契约 | P1 | 用例入库并绿 |
+| G-4 | SRS 锚点 | `srs_section` 0 锚点 | 覆盖率不可量化 | 启用 SRS 池,TC ↔ SRS 双向锚定 | P0(流程) | 锚点 > 0 且可统计 |
+| G-5 | 场景执行 | 241 场景 / 跑通 15(6.2%) | 场景级验收缺口大 | 每场景 ≥1 条 e2e,优先 P0 场景 | P1 | 场景跑通率 ≥80% |
+| G-6 | 根级黑盒 | 23 例全 skip | 接口层覆盖为 0 | 见 B-36 | P1 | 恢复执行 |
+| G-7 | CI 门禁 | `.gitlab-ci.yml` 测试 job `allow_failure`,集成层被排除 | 绿灯 ≠ 可发布 | 拆 blocking / observed 两级 | P0(流程) | blocking 覆盖 ≥80% 例数 |
+| G-8 | 覆盖率/变异 | 无 jacoco/pitest,未实测 | 代码级覆盖不可知 | 联网授权后加插件(11:00 轮已备方案,未授权未执行) | P2 | 产出 jacoco 报告 |
+| G-9 | 无障碍/兼容 | 仅 chromium 桌面+移动视口 | 跨浏览器/无障碍未知 | 增 firefox/webkit project + axe 扫描 | P2 | 三内核全绿 |
+
+---
+
+## 六、门禁结论
+
+| 门禁 | 阈值 | 实测 | 结论 |
+|------|------|------|------|
+| G1 后端通过率 | ≥96% | **95.04%**(剔环境 95.69%) | ❌ |
+| G2 后端红例 | ≤20 | **115**(B-53 收口后 73) | ❌ |
+| G3 小程序前端 | 313 全绿 | **313/313** | ✅ |
+| G4 管理端前端 | 90 全绿 | **90/90** | ✅ |
+| G5 编译 + 报告完整性 | 0 错 / 无静默丢失 | 293 源 0 错 / 289-289 / 沉默 0 | ✅ |
+| G6 线上浏览器 | 双端全绿 | **25/25**(admin 5、frontend 6、契约 10、商城 4) | ✅ |
+| G7 langgraph | 0 失败 | **59 绿 / 0 红 / 12 skip** | ✅ |
+| G8 CI 硬门禁 | 测试失败阻断流水线 | `allow_failure` + 集成层排除 | ❌ |
+| G9 SRS 锚点 | > 0 可统计 | **0** | ❌ |
+| G10 场景跑通率 | ≥80% | 241 场景 / 跑通 15(6.2%) | ❌ |
+| G11 WIP 收口 | 0 | **73 项**(含 11 后端测试文件) | ❌ |
+| G12 测试资产入库 | 新用例 0 untracked | 本轮 3 个新测试类随提交入库 | ✅ |
+
+**总判定:不通过(有条件)。**
+- 可测且可构建的部分全部转绿并保持:三前端套件 + 线上浏览器/契约 **453/453**,langgraph 0 失败,后端编译与报告完整性首次在「新 P0 修复后」全量验证通过。
+- 未闭环的两条主线:**后端 115 红例**(其中 42 例只差一次提交,B-53 已连续 3 轮未收口)与**流程性门禁**(CI 软门禁、SRS 池未启用、场景跑通 6.2%)。
+- 复评条件:B-53 收口后后端 ≥96.79%;CI blocking job 覆盖 ≥80% 例数;SRS 锚点 > 0。
+
+---
+
+## 对照用户双目标
+
+- **① 所有测试用例全部通过**:⚠️ 部分达成。
+  - 全绿:cfc-frontend 313、cfc-web 90、cfc-langgraph 59(执行口径 100%)、浏览器 + 契约 + 商城 25/25、本轮新增 18/18。
+  - 未全绿:cfc-backend 2279/2398 = **95.04%**(115 红例;其中 **42 例的修复已在研发工作区,未提交**)。
+  - 未执行:根级 23 例(全 skip)、覆盖率/变异/压测/SAST(无工具且未授权联网)、`@Disabled` 4 例。
+- **② 覆盖所有 SRS + 用户场景(含异常流)**:❌ 未达成,但本轮有实质推进。
+  - 今日 11 份 SRS 变更中,3 条代码级变更从「0 自动化」→ **18 例自动化实测通过**(C-16 裁剪、C-10 通知、家庭加入身份审批 7 例异常流全覆盖)。
+  - 仍缺口:分润 v0.7(3 端点 0 引用)、ai-butler 19 端点、planner-zone 7 端点、parent-rest 5 端点、健康业务侧 12 端点;SRS 池 0 锚点使覆盖率仍无法百分数化。
+
+---
+
+## 变更触发动作(「结果有变化时提交远程 + 通知研发」)
+
+**与 20:10 / 21:20 基线相比的实质变化:**
+1. **后端首次在「B-47 修复后」完成全量实跑**(21:20 轮因误判「本机无 Maven」未跑):289 类 / 2398 例 / **95.04%**,与 20:10 全字段零漂移(+1 新类 8 例全绿,红类/红例集合零变化)。
+2. **浏览器节点从 11 例扩到 25 例**:新增商城排序 4 例 + 健康契约保持 10/10(B-50 未回弹)。
+3. **langgraph 59 绿 0 红**(21:20 为 51 绿)→ +8 例转绿,B-54 未回弹。
+4. **新增 18 例用例入库**:3 条 SRS 变更的自动化绑定(此前 0 覆盖)。
+5. 节点分派仍不可用(189 API 401、251 端口 CLOSED)→ 已如实登记,未虚报派发。
+
+**处置:** 提交报告 + 3 个测试类 + `blockers.md`(新增 B-57,更新 B-53 量化)→ push `origin/cfclub`。
+**研发通知载体:** Agent Mail 未开通(`status:not_bound`,工具明确提示勿重试),故以 **仓库通知文件 + 台账** 为正式通知载体,并在下文「附录 D」列出研发需当日处理的 3 项。
+
+---
+
+## 附录 D:需研发当日处理(通知正文)
+
+1. **B-53(P0,第三次催办)**:请产生 WIP 的会话对 11 个后端测试文件 `git add` + commit + push。实测证据:带入后同 11 类 170 例由 **48 红 → 6 红**,后端整体 95.04% → **96.79%**(越过 G1 阈值)。文件清单见 `blockers.md` B-53。
+2. **B-55/B-56(P1,分派链路)**:251 需放通 9000;189 的 opencode HTTP API 现要求鉴权(`{"_tag":"UnauthorizedError"}`),需配置 `auth` 与 `remote_dir: c:/code/cfc`。
+3. **B-36(P1)**:根级 23 例黑盒用例被环境门控全 skip,接口层覆盖实际归零,建议改 mock 门控。
+
+---
+
+## 附录 E:本轮实测命令与证据文件
+
+```
+# 后端全量(隔离树 @bca7dec03,JDK 1.8.0_25 + IDEA Maven 3.9.9)
+cd D:/_wt_r2149/cfc-backend
+JAVA_HOME=D:\jdk1.8.0_25 mvn -o -B clean test -DskipTests=false -Dmaven.test.failure.ignore=true
+  → BUILD SUCCESS, 32:59, 289 类 / 2398 例
+
+# 本轮新增 18 例
+mvn -o -B test -DskipTests=false -Dtest=UserServiceMaskChildSensitiveFieldsTest,BadgeServiceUnlockNoticeTest,FamilyInviteApproveRequestTest
+  → Tests run: 18, Failures: 0, Errors: 0
+
+# B-53 量化(带 11 个 WIP 测试文件)
+mvn -o -B test -DskipTests=false -Dtest=ArticleControllerTest,AuthControllerTest,...,StatsControllerTest
+  → Tests run: 170, Failures: 4, Errors: 2(clean 同 11 类为 48 红)
+
+# 前端
+cfc-frontend: node node_modules/jest/bin/jest.js --ci --silent   → 313/313
+cfc-web:      node node_modules/jest/bin/jest.js --ci --silent   → 90/90
+cfc-langgraph: .venv/Scripts/python.exe -m pytest --ignore=tests/qna -p no:cacheprovider -q → 59 passed / 12 skipped
+
+# 线上浏览器(必须在 tests/ 目录内执行,B-52 教训)
+cd d:/cfc/tests
+node node_modules/@playwright/test/cli.js test -c playwright.config.live.js --project=admin-desktop    → 5 passed
+node node_modules/@playwright/test/cli.js test -c playwright.config.live.js --project=frontend-mobile  → 6 passed
+node node_modules/@playwright/test/cli.js test -c playwright.config.live.js --project=shop-api         → 4 passed
+node node_modules/@playwright/test/cli.js test -c playwright.config.health.js                          → 10 passed
+```
+
+证据文件:`_r2149_backend.log`、`_r2149_frontend.log`、`_r2149_cfcweb.log`、`_r2149_pytest.log`、`_r2149_admin.log`、`_r2149_frontend_pw.log`、`_r2149_health.log`、`_r2149_shop.log`(均在 `D:\`)。
+
+---
+
+## 提交与推送回填
+
+- 提交 sha:`_TBD_`
+- `git merge-base --is-ancestor <sha> origin/cfclub`:`_TBD_`
+- 纪律:仅提交本报告、3 个新测试类、`blockers.md`;**不触碰 73 项他人 WIP**(O-5/B-53)。

+ 2 - 0
docs/tdd/test/blockers.md

@@ -61,3 +61,5 @@
 | B-54 | cfc-langgraph 本机 venv 不可用(chromadb 原生 DLL 缺失 + LLM 凭据无效 + test_memory 签名漂移) | 研发/测试(双方) | **P1 环境+夹具缺口(20:10 轮实测)**。pytest 51 例:34 绿 / 17 红。① **C 类环境 12 例**:`ImportError DLL load failed while importing chromadb_rust_bindings: 找不到指定的模块`(`chromadb 1.5.9` 原生扩展缺 VC++ 运行时)→ chat/analyze 端点 500;`api key ****ocal is invalid` → intent 4 例退化为 CHAT、auto_tag 5 例断言失败;report_parse 2 例用例写死容器路径 `/app/cfc/...` 本机 400。② **A 类夹具 3 例**:`tests/test_memory.py:7/14/22` `MemoryManager()` 未传 `embeddings`(`app/memory/store.py:15` 已改必填),因 chromadb 不可用无法判定修复后是否转绿。闭环:装 VC++ 运行时或重建 venv(`pip install --force-reinstall chromadb`)+ 用例补 `FakeEmbeddings`,再按指令改派 251 节点复跑 ≥45 绿 | 测试负责人(20:10 轮登记) | 2026-11-07 20:10 | 待处理 |
 | B-55 | 测试节点 192.168.16.189 / 192.168.16.251 均不可达,按节点分派无法执行 | 运维 | **P1 分派缺口(20:10 轮实测,B-42 延续)**。`curl` 实测:`192.168.16.189` 的 TCP 22/80/4090 **全部拒绝**(http_code 000);`192.168.16.251:9000/health` **超时**(000)。影响:本轮浏览器任务(admin/frontend)与 langgraph 任务均无法按新拓扑分派,全部回退本机串行(遵守「每节点仅一个浏览器任务」)。建议:① 开放 189 的 22(SSH)/4090(opencode)并对 251 放通 9000;② 测试侧补 `tests/node-probe.spec.js`,每轮先探活再分派,避免静默回退。**21:20 轮复测修正**:`192.168.16.189:4090` 与 `192.168.16.204:4091` 现 TCP OPEN(可达),`192.168.16.251:9000/4090` 仍 CLOSED;但 189/204 的 opencode agent 派发任务均零产出(见 B-56),故 189 仍无法实跑 admin、251 仍不可派发 langgraph。闭环改为:189 agent 任务执行链路修复后重测;251 放通端口 | 测试负责人(20:10 轮登记) | 2026-11-07 20:10 | 待处理 |
 | B-56 | 测试节点 opencode 派发任务执行不生效(204/189 均 TCP 可达但 session 创建后 agent 零产出) | 测试/运维 | **P2 分派机制缺陷(21:20 轮实测)**。`python` 经 opencode HTTP API(Basic `opencode:IwinTrue@123`)向 `192.168.16.204:4091` / `192.168.16.189:4090` `POST /api/session` 创建会话成功(200,返回 `ses_*`),`POST /api/session/{id}/prompt` 返回 200,但轮询 480s 内 `GET /api/session/{id}` 的 `assistant` 消息始终为空(`len=0`)→ 派发的浏览器任务未被 agent 实际执行,无任何产出。影响:按新拓扑「admin→189 / frontend→204」的远程派发链路失效,本轮改为在本机(204)直接用本地 Playwright CLI 实跑两个浏览器套件(frontend 6/6、admin 5/5 全绿)作为兜底证据。建议:修复 204/189 opencode 节点的 agent 任务执行链路(会话创建后需触发 agent run,而非仅登记 prompt),或将浏览器任务改为 opencode `run` 接口直接执行 playwright 命令。闭环:204/189 节点派发任务能回传真实 playwright 结果 JSON | 测试负责人(21:20 轮实测登记) | 2026-11-07 21:20 | 待处理 |
+| B-53-Update(第 75 轮量化) | B-53 精确复现:带 WIP 同 11 类 170 例 48 红 → 6 红(42 例可转绿) | 研发 | **P0 第三次催办,量化证据已具备**。隔离树 @`bca7dec03` 带入主工作区 11 个 `M` 状态后端测试文件后定向复跑同 11 类:`Tests run: 170, Failures: 4, Errors: 2`(`GuideFamilyTaskControllerTest` 2F+1E、`GuideRolePermissionTest` 1F+1E、`StatsControllerTest` 1F),对比 clean 基线同 11 类 **48 红**(Article 9 / Auth 4 / Family 1 / GuideFamilyTask 5 / GuideOrder 8 / GuideRolePermission 5 / GuideTeam 2 / Payment 4 / Product 3 / ProductOrder 6 / Stats 1)→ **42 例可立即转绿**。整体影响:后端 2279/2398 = 95.04% → **2321/2398 = 96.79%**(越过 G1 96% 阈值),红例 115 → **73**,红类 35 → **29**。闭环:11 文件入库后全量复跑 ≥96.79% | 测试负责人(第 75 轮实测量化) | 2026-11-07 23:05 | 待处理(连续 3 轮未收口) |
+| B-57 | 新用例断言错设:`@RequestAttribute` 缺失在 Spring 绑定层即 400,非业务分支 | 测试(自处置) | **A 类用例缺陷(本轮自发现并修复)**。`FamilyInviteApproveRequestTest` 首版把「未登录审批」断言为 `200 + message=请先登录`,实测 **400 Bad Request**——`@RequestAttribute("userId")` 为必需属性,缺属性时 `RequestAttributeMethodArgumentResolver` 在进入方法体前即抛 `ServletRequestBindingException`,controller 内 `approverUserId == null` 分支在 MockMvc 下不可达(仅当属性存在且为 null 才进入,而 `requestAttr(k, null)` 本身被 MockMvc 拒绝)。影响:若按首版断言入库,该用例会永久红;若为让用例变绿而放宽断言,则失去「未登录不得触达业务」的守护意义。修复:断言改为 `status().isBadRequest()` + `verify(service, never())`(既锁定拒绝、又锁定不触达业务);删除不可达的「属性为 null」用例并在此登记该分支为防御性代码。闭环:`FamilyInviteApproveRequestTest` **7/7 绿**,与另两类合计 **18/18 绿** | 测试负责人(第 75 轮实测登记) | 2026-11-07 23:05 | 已处置(用例已修,18/18 绿) |

+ 47 - 0
docs/tdd/test/notice-2026-11-07-2305-75round.md

@@ -0,0 +1,47 @@
+# 【测试通知·第 75 轮】后端 95.04%(+42 例只差一次提交)· 浏览器/契约 25/25 全绿 · langgraph 59 绿 0 红
+
+> 发件:测试负责人(`cfc-tester`)|时间:2026-11-07 23:05 GMT+8|基线 `bca7dec03`
+> 载体说明:Agent Mail 未开通(`not_bound`),本文件 + `docs/tdd/test/blockers.md` 为正式通知载体。
+
+## 一、需要研发当日处理的 3 项
+
+| # | 事项 | 证据 | 影响 | 请谁处理 |
+|---|------|------|------|----------|
+| 1 | **B-53 收口 WIP(P0,第三次催办)** | 带 WIP 定向复跑同 11 类:`170 例 4F+2E=6 红`;clean 基线同 11 类 48 红 → **42 例可立即转绿** | 后端 **95.04% → 96.79%**(越 G1 阈值),红例 115 → 73,红类 35 → 29 | 产生 WIP 的会话:对 11 个 `cfc-backend/src/test/.../integration/controller/*ControllerTest.java` 执行 add + commit + push(只含测试文件) |
+| 2 | **B-55 / B-56 分派链路(P1)** | 251:9000/4090 CLOSED,80 为 openresty 404;189:4090 TCP OPEN 但 API `{"_tag":"UnauthorizedError"}` | 浏览器与 langgraph 无法按拓扑分派,测试侧只能本机串行兜底 | 运维:放通 251:9000;为 189 配 `auth` + `remote_dir: c:/code/cfc` |
+| 3 | **B-36 根级黑盒(P1)** | `tests` 根级 Jest 2 套件 23 例 **全部 skip** | 接口层黑盒覆盖实际为 0 | 研发/测试:门控改「无外部依赖则 mock 跑」 |
+
+## 二、本轮实测结果(全量,非引用)
+
+| 套件 | 结果 |
+|------|------|
+| cfc-backend JUnit(隔离树 `bca7dec03`) | 289 类 / **2398 例 / 2279 过 = 95.04%**(剔环境红 95.69%);65F + 50E + 4S;35 红类 / 115 红例;32:59 BUILD SUCCESS |
+| 本轮新增用例(3 类) | **18/18 绿**(TC-C16-01~05、TC-C10-01~06、TC-FA-18~24) |
+| cfc-frontend | 313/313 |
+| cfc-web | 90/90 |
+| cfc-langgraph | **59 绿 / 0 红 / 12 skip**(上轮 51 绿 → +8) |
+| 浏览器 admin-desktop | 5/5(含真实登录 + 滑块) |
+| 浏览器 frontend-mobile | 6/6 |
+| 健康鉴权契约 | **10/10**(B-50 未回弹,华为 5 端点仍 401) |
+| 商城价格排序 API | 4/4 |
+| 合计 | **2903 例 / 2784 过 = 95.90%** |
+
+## 三、好消息(已闭环,无需动作)
+
+- **B-47 术后稳定**:JDK8 下 2080 主源 + 293 测试源 **0 编译错误**,2398 例全量可跑(上轮曾整仓不可编译)。
+- **B-50 保持关闭**:华为健康 5 端点未鉴权均 401,与小米对称,契约 10/10。
+- **零漂移**:与 20:10 基线相比 F65/E50/S4、35 红类、115 红例逐项一致;新增 1 类 8 例全绿,无转红类。
+
+## 四、本轮新增用例的验收价值(SRS 变更绑定)
+
+| SRS 变更 | 用例 | 锁定契约 |
+|----------|------|----------|
+| C-16 孩子端受限资料投影 | `UserServiceMaskChildSensitiveFieldsTest`(5 例) | child 裁剪 idCard/phone/realName/openid/unionid;parent/teacher/role 空不裁剪;null 不抛 |
+| C-10 勋章解锁站内通知 | `BadgeServiceUnlockNoticeTest`(6 例) | 本人通知 / 同家庭代管代收 / 无代收不发送 / 通知失败不阻断授予 / 重复授予幂等 / 模板变量 |
+| 家庭加入身份由邀请人指定 | `FamilyInviteApproveRequestTest`(7 例) | 未登录 400 且不触达 service;requestId/辈分/同辈类型缺失拒绝;parent 与 peer+sibling 正常下发;service 异常透传 |
+
+## 五、门禁
+
+**不通过(有条件)**:G1 95.04% ❌、G2 115 ❌、G8 CI 软门禁 ❌、G9 SRS 锚点 0 ❌、G10 场景 6.2% ❌、G11 WIP 73 项 ❌;
+达标:G3 313 ✅、G4 90 ✅、G5 编译与完整性 ✅、G6 浏览器 25/25 ✅、G7 langgraph 0 失败 ✅、G12 新用例入库 ✅。
+**复评条件**:B-53 收口后后端 ≥96.79%;CI blocking 覆盖 ≥80% 例数;SRS 锚点 > 0。