|
|
@@ -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("菌株产品");
|
|
|
+ }
|
|
|
+}
|