Explorar o código

test(guhe.lowSide.noSupplement): S-cfc-86 add RED contract tests for low-side no-supplement redline (T4/L4-3, contract not landed)

RED contract tests for guhe.lowSide.noSupplement (2026-11-08 guhe_v2 plan Section 3 T4): assert L4 attribution engine must not produce supplement recommendations for low-side nodes - probe missing RULE_NO_SUPPLEMENT / NO_SUPPLEMENT_HINT surface in AttributionLowSideRules, isNoSupplement() node flag, and '不推荐补菌' meaning on low E. coli reading via IndicatorInterpretEngine. Currently 4/6 fail (RED), implementation not landed.
Sisyphus Agent hai 4 días
pai
achega
801902f2e0

+ 225 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/GuheLowSideNoSupplementContractTest.java

@@ -0,0 +1,225 @@
+package com.etotem.cfc.unit;
+
+import com.etotem.cfc.constant.AttributionLowSideRules;
+import com.etotem.cfc.service.attribution.AttributionComplianceGate;
+import com.etotem.cfc.service.attribution.IndicatorInterpretEngine;
+import com.etotem.cfc.service.attribution.model.IndicatorReading;
+import com.etotem.cfc.service.attribution.model.MemberAttribution;
+import com.etotem.cfc.service.attribution.model.MemberProfile;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * 契约级单测:谷禾 v2 低侧口径「低位补菌红线」(能力键 {@code guhe.lowSide.noSupplement})。
+ *
+ * <p>契约来源(2026-11-08《浠艾福平台程序修改计划(guhe_v2 兼容 · 知识库低侧口径 · 专家同步)》§三任务分解 T4,P0):
+ * <blockquote>
+ *   T4 低位补菌红线(L4-3):<b>无成熟证据支持「缺大肠杆菌→补大肠杆菌」</b>;
+ *   ⛔ 禁「报告缺 X → 买含 X 菌株产品」桥接(叠加 §5.2 16S 粒度声明)。
+ *   <br>验收:任意「偏低」归因节点 {@code association_only==true};存量无「缺大肠杆菌→补菌」措辞;
+ *   低侧机制证据全部标 PUB/KB。
+ * </blockquote></p>
+ *
+ * <p><b>契约语义</b>:本能力键不是「事后文本扫描」而是 <b>L4 归因引擎层面的禁补菌红线</b> ——
+ * 归因引擎对任意「偏低」归因节点(含条件致病菌如大肠杆菌)<b>不生成补菌建议</b>,
+ * 并在解读产出中显式携带「偏低不推荐补菌」口径(无成熟证据支持缺X→补X),
+ * 使「报告缺 X → 买含 X 菌株产品」桥接在 <b>源头</b>被拦截,而非仅靠门禁事后回扫。</p>
+ *
+ * <p><b>实现现状(src/main 探测,2026-11-08)</b>:L4 归因引擎与 G-1 门禁已落地
+ * ({@code IndicatorInterpretEngine} / {@code AttributionComplianceGate} / {@code AttributionLowSideRules}),
+ * 但已落地的是 <b>T5 G-1 低侧红线组</b>({@code association_only} / {@code no_causal_bridge} /
+ * {@code no_prescription} 三规则码 + 措辞白名单 + 门禁文本回扫 {@code scan})。
+ * <b>T4 低位补菌红线(L4-3)未落地</b>:{@code AttributionLowSideRules} 无独立
+ * {@code no_supplement} 规则码、无「偏低不推荐补菌」口径常量;{@code LowSideNode} 无节点级
+ * 禁补菌标记;{@code IndicatorInterpretEngine} 对偏低节点仅产通用「先医后补」含义,未产
+ * 「不推荐补菌」口径。故本测试以「反射结构 + 纯规则行为」双通道探测实现缺失:能编译、
+ * 能运行、断言失败(红),失败断言原文清晰可贴。</p>
+ *
+ * <p><b>GREEN 升级指引</b>:实现者在 {@code AttributionLowSideRules} 增加
+ * {@code RULE_NO_SUPPLEMENT="no_supplement"} 与 {@code NO_SUPPLEMENT_HINT}(含「不推荐补菌」),
+ * 在 {@code AttributionComplianceGate.LowSideNode} 暴露 {@code isNoSupplement()}(低侧节点恒 true),
+ * 并在 {@code IndicatorInterpretEngine} 的偏低含义生成路径注入「偏低不推荐补菌」口径后,本测试自动转绿。</p>
+ *
+ * <p><b>权限流说明</b>:本能力为后端内部 L4 归因规则,无用户权限维度,权限流不适用,以说明性用例标注。</p>
+ */
+class GuheLowSideNoSupplementContractTest {
+
+    /** T4 期望的独立规则码(区别于 T5 的 no_causal_bridge) */
+    private static final String RULE_NO_SUPPLEMENT = "no_supplement";
+
+    /** T4 期望的「偏低不推荐补菌」口径常量名 */
+    private static final String HINT_FIELD = "NO_SUPPLEMENT_HINT";
+
+    /** T4 期望的节点级禁补菌标记方法名 */
+    private static final String NODE_FLAG_METHOD = "isNoSupplement";
+
+    // ------------------------------------------------------------------
+    // 正常流:规则层应暴露独立的 low-side 禁补菌红线(T4 规则码 + 口径)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("正常流-1:AttributionLowSideRules 应暴露 RULE_NO_SUPPLEMENT 规则码")
+    void lowSideRules_exposesNoSupplementRuleCode() {
+        Field f = declaredField(AttributionLowSideRules.class, "RULE_NO_SUPPLEMENT");
+        assertNotNull(f,
+                "[guhe.lowSide.noSupplement] AttributionLowSideRules 缺少 RULE_NO_SUPPLEMENT 规则码:"
+                        + "T4(L4-3)要求独立「低位补菌红线」——无成熟证据支持「缺大肠杆菌→补大肠杆菌」。"
+                        + "当前仅落地 T5 的 no_causal_bridge,未区分「引擎不产出补菌建议」与「文本回扫」。");
+        try {
+            Object v = f.get(null);
+            assertNotNull(v, "[guhe.lowSide.noSupplement] RULE_NO_SUPPLEMENT 常量值不应为 null。");
+            assertTrue(RULE_NO_SUPPLEMENT.equals(String.valueOf(v)),
+                    "[guhe.lowSide.noSupplement] RULE_NO_SUPPLEMENT 应取值为 \"" + RULE_NO_SUPPLEMENT
+                            + "\",实际为 \"" + v + "\"。");
+        } catch (IllegalAccessException e) {
+            assertTrue(false, "[guhe.lowSide.noSupplement] 读取 RULE_NO_SUPPLEMENT 失败:" + e.getMessage());
+        }
+    }
+
+    @Test
+    @DisplayName("正常流-2:AttributionLowSideRules 应暴露「偏低不推荐补菌」口径常量")
+    void lowSideRules_exposesNoSupplementHintWording() {
+        Field f = declaredField(AttributionLowSideRules.class, HINT_FIELD);
+        assertNotNull(f,
+                "[guhe.lowSide.noSupplement] AttributionLowSideRules 缺少 " + HINT_FIELD
+                        + " 口径常量:T4 要求「偏低不推荐补菌」显式口径(无成熟证据支持缺X→补X),"
+                        + "供 L4 引擎与报告侧引用,而非仅通用「先医后补」。");
+        try {
+            Object v = f.get(null);
+            assertNotNull(v, "[guhe.lowSide.noSupplement] " + HINT_FIELD + " 常量值不应为 null。");
+            String text = String.valueOf(v);
+            assertTrue(text.contains("不推荐补菌"),
+                    "[guhe.lowSide.noSupplement] " + HINT_FIELD + " 应含「不推荐补菌」口径(§5.1.1 专家口径第四条),"
+                            + "实际为:「" + text + "」。");
+            assertTrue(text.contains("缺"),
+                    "[guhe.lowSide.noSupplement] " + HINT_FIELD + " 应含「无成熟证据支持缺X→补X」表述,"
+                            + "实际为:「" + text + "」。");
+        } catch (IllegalAccessException e) {
+            assertTrue(false, "[guhe.lowSide.noSupplement] 读取 " + HINT_FIELD + " 失败:" + e.getMessage());
+        }
+    }
+
+    @Test
+    @DisplayName("正常流-3:低侧节点应暴露节点级禁补菌标记 isNoSupplement()")
+    void lowSideNode_exposesNoSupplementFlag() {
+        boolean hasFlag = false;
+        for (Method m : AttributionComplianceGate.LowSideNode.class.getDeclaredMethods()) {
+            if (NODE_FLAG_METHOD.equals(m.getName())) {
+                hasFlag = true;
+                break;
+            }
+        }
+        assertTrue(hasFlag,
+                "[guhe.lowSide.noSupplement] AttributionComplianceGate.LowSideNode 缺少 " + NODE_FLAG_METHOD
+                        + "() 节点级禁补菌标记:T4 要求「偏低」归因节点在源头即标注不推荐补菌,"
+                        + "而非仅门禁事后回扫。");
+    }
+
+    // ------------------------------------------------------------------
+    // 边界流:L4 解读引擎对「偏低」条件致病菌节点不产补菌建议(源头拦截)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("边界流-1:偏低大肠杆菌节点的干预含义应携带「不推荐补菌」口径")
+    void interpret_lowSideEColi_meaningCarriesNoSupplementRedline() {
+        IndicatorReading reading = lowSideEColi();
+        MemberProfile member = profile();
+        IndicatorInterpretEngine engine = new IndicatorInterpretEngine();
+
+        List<MemberAttribution> attrs = engine.interpret(Collections.singletonList(reading),
+                Collections.singletonList(member));
+        assertNotNull(attrs, "[guhe.lowSide.noSupplement] interpret 不应返回 null。");
+        assertFalse(attrs.isEmpty(), "[guhe.lowSide.noSupplement] interpret 应产出至少 1 人归因。");
+        IndicatorReading out = attrs.get(0).getReadings().get(0);
+        String meaning = out.getMeaning() == null ? "" : out.getMeaning();
+        assertTrue(meaning.contains("不推荐补菌"),
+                "[guhe.lowSide.noSupplement] 偏低大肠杆菌节点干预含义应携带「偏低不推荐补菌」口径"
+                        + "(T4 L4-3,无成熟证据支持缺X→补X),实际为:「" + meaning + "」。"
+                        + "当前仅产通用「先医后补」,未落地 T4 源头禁补菌。");
+    }
+
+    @Test
+    @DisplayName("边界流-2:偏低大肠杆菌节点的应对方向不得含补菌产品桥接措辞")
+    void interpret_lowSideEColi_actionsContainNoSupplementBridge() {
+        IndicatorReading reading = lowSideEColi();
+        MemberProfile member = profile();
+        IndicatorInterpretEngine engine = new IndicatorInterpretEngine();
+
+        List<MemberAttribution> attrs = engine.interpret(Collections.singletonList(reading),
+                Collections.singletonList(member));
+        for (String action : attrs.get(0).getActions()) {
+            assertFalse(containsSupplementBridge(action),
+                    "[guhe.lowSide.noSupplement] 偏低大肠杆菌节点应对方向不得含「补菌产品」桥接措辞"
+                            + "(⛔ 禁「报告缺 X → 买含 X 菌株产品」),实际为:「" + action + "」。");
+        }
+    }
+
+    // ------------------------------------------------------------------
+    // 权限流(不适用):后端内部 L4 归因规则,无用户权限维度
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("权限流:本能力为后端内部 L4 归因规则,无用户权限维度,权限流不适用")
+    void permissionFlow_notApplicable() {
+        // 说明性用例:契约 AC 为「L4 归因引擎 → 低侧禁补菌」内部规则,无权限校验维度。
+        assertTrue(true, "权限流不适用:guhe.lowSide.noSupplement 为后端内部 L4 归因规则,无用户权限维度。");
+    }
+
+    // ------------------------------------------------------------------
+    // 反射与数据构造辅助
+    // ------------------------------------------------------------------
+
+    private static Field declaredField(Class<?> clazz, String name) {
+        for (Field f : clazz.getDeclaredFields()) {
+            if (f.getName().equals(name)) {
+                f.setAccessible(true);
+                return f;
+            }
+        }
+        return null;
+    }
+
+    /** 偏低条件致病菌读数(大肠杆菌,P3,参考下限 0 / ND 口径) */
+    private static IndicatorReading lowSideEColi() {
+        IndicatorReading r = new IndicatorReading();
+        r.setMemberId(1L);
+        r.setMemberName("妈妈");
+        r.setCode("Escherichia_coli");
+        r.setDisplayName("大肠杆菌");
+        r.setValue("0.5%");
+        r.setPercentile("P3");
+        r.setStatus(IndicatorReading.STATUS_LOW);
+        r.setBand(IndicatorReading.BAND_LOW);
+        return r;
+    }
+
+    private static MemberProfile profile() {
+        MemberProfile p = new MemberProfile();
+        p.setMemberId(1L);
+        p.setName("妈妈");
+        p.setRole("mom");
+        p.setRoleLabel("妈妈");
+        return p;
+    }
+
+    /** 补菌产品桥接措辞(「报告缺 X → 买含 X 菌株产品」) */
+    private static boolean containsSupplementBridge(String text) {
+        if (text == null) {
+            return false;
+        }
+        String t = text;
+        return t.contains("补大肠杆菌") || t.contains("补充大肠杆菌")
+                || t.contains("补菌") || t.contains("需要补充") || t.contains("需补充")
+                || t.contains("买含") || t.contains("购买") || t.contains("选购")
+                || t.contains("下单") || t.contains("菌株产品");
+    }
+}