|
@@ -0,0 +1,254 @@
|
|
|
|
|
+package com.etotem.cfc.unit;
|
|
|
|
|
+
|
|
|
|
|
+import com.etotem.cfc.constant.AttributionLowSideRules;
|
|
|
|
|
+import com.etotem.cfc.constant.ConditionalPathogenLowSideRules;
|
|
|
|
|
+import com.etotem.cfc.service.attribution.AttributionComplianceGate;
|
|
|
|
|
+import com.etotem.cfc.service.attribution.IndicatorInterpretEngine;
|
|
|
|
|
+import com.etotem.cfc.service.attribution.NormCompareEngine;
|
|
|
|
|
+import com.etotem.cfc.service.attribution.model.CompareResult;
|
|
|
|
|
+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 com.etotem.cfc.service.attribution.model.NormEntry;
|
|
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
|
|
+
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.Arrays;
|
|
|
|
|
+import java.util.Collections;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.*;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * T3(L4-1/L4-2)条件致病菌低侧判读规则入库的单元测试。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>覆盖:
|
|
|
|
|
+ * <ul>
|
|
|
|
|
+ * <li>L4-2 参考下限 0 的菌群不标「偏低超标」(ND/低位只是低于检出丰度),且仅作用于菌群常模;</li>
|
|
|
|
|
+ * <li>L4-1 条件致病菌(大肠杆菌/埃希氏菌属/脆弱拟杆菌)偏低侧按四机制/双刃剑解释,
|
|
|
|
|
+ * 全部标「关联非因果」+ U 型双向口径;非条件致病菌不注入;</li>
|
|
|
|
|
+ * <li>低侧机制文本不命中 G-1「缺 X 桥接 / 处方化」红线(合规门禁反向回扫自证)。</li>
|
|
|
|
|
+ * </ul>
|
|
|
|
|
+ */
|
|
|
|
|
+class ConditionalPathogenLowSideRuleTest {
|
|
|
|
|
+
|
|
|
|
|
+ private final NormCompareEngine normCompareEngine = new NormCompareEngine();
|
|
|
|
|
+ private final IndicatorInterpretEngine interpretEngine = new IndicatorInterpretEngine();
|
|
|
|
|
+ private final AttributionComplianceGate gate = new AttributionComplianceGate();
|
|
|
|
|
+
|
|
|
|
|
+ // ============================================================
|
|
|
|
|
+ // L4-2 参考下限 0 → 不标「偏低超标」
|
|
|
|
|
+ // ============================================================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void 大肠杆菌参考下限0_零值不标偏低超标() {
|
|
|
|
|
+ IndicatorReading r = gutReading("大肠杆菌", "0", 0.0);
|
|
|
|
|
+ CompareResult result = normCompareEngine.compare(Collections.emptyList(),
|
|
|
|
|
+ Collections.singletonList(r), Collections.singletonList(ecoliNorm(0.0)), Collections.emptyMap());
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(IndicatorReading.STATUS_NORMAL, r.getStatus(),
|
|
|
|
|
+ "参考下限 0 的菌,ND/0 只是低于检出丰度,不应标偏低(L4-2)");
|
|
|
|
|
+ assertEquals(IndicatorReading.BAND_MID, r.getBand());
|
|
|
|
|
+ assertFalse(r.isAbnormal());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void 大肠杆菌参考下限0_低位小值不标偏低超标() {
|
|
|
|
|
+ IndicatorReading r = gutReading("大肠杆菌", "0.15", 0.15);
|
|
|
|
|
+ normCompareEngine.compare(Collections.emptyList(),
|
|
|
|
|
+ Collections.singletonList(r), Collections.singletonList(ecoliNorm(0.0)), Collections.emptyMap());
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(IndicatorReading.STATUS_NORMAL, r.getStatus());
|
|
|
|
|
+ assertFalse(r.isAbnormal());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void 参考下限0仅作用于菌群常模_DAN常模不受影响() {
|
|
|
|
|
+ NormEntry dan = new NormEntry();
|
|
|
|
|
+ dan.setNormType("dan");
|
|
|
|
|
+ dan.setCode("注意力");
|
|
|
|
|
+ dan.setP5(0.0);
|
|
|
|
|
+ dan.setP25(2.0);
|
|
|
|
|
+ dan.setP50(3.0);
|
|
|
|
|
+ dan.setP75(5.0);
|
|
|
|
|
+ dan.setP95(9.0);
|
|
|
|
|
+ dan.setSource("cognitive_norms");
|
|
|
|
|
+ IndicatorReading r = new IndicatorReading();
|
|
|
|
|
+ r.setSource("dan");
|
|
|
|
|
+ r.setMemberId(1L);
|
|
|
|
|
+ r.setCode("注意力");
|
|
|
|
|
+ r.setDisplayName("注意力");
|
|
|
|
|
+ r.setValue("0.5");
|
|
|
|
|
+ r.setNumericValue(0.5);
|
|
|
|
|
+
|
|
|
|
|
+ normCompareEngine.compare(Collections.emptyList(),
|
|
|
|
|
+ Collections.singletonList(r), Collections.singletonList(dan), Collections.emptyMap());
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(IndicatorReading.STATUS_LOW, r.getStatus(),
|
|
|
|
|
+ "参考下限 0 规则仅针对菌群常模(gut_flora),DAN 等常模判分位逻辑不变");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void 参考下限非0的菌群_低位仍正常判偏低() {
|
|
|
|
|
+ IndicatorReading r = gutReading("脆弱拟杆菌", "0.5", 0.5);
|
|
|
|
|
+ NormEntry norm = ecoliNorm(1.0);
|
|
|
|
|
+ norm.setCode("脆弱拟杆菌");
|
|
|
|
|
+ norm.setDisplayName("脆弱拟杆菌");
|
|
|
|
|
+
|
|
|
|
|
+ normCompareEngine.compare(Collections.emptyList(),
|
|
|
|
|
+ Collections.singletonList(r), Collections.singletonList(norm), Collections.emptyMap());
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(IndicatorReading.STATUS_LOW, r.getStatus(),
|
|
|
|
|
+ "参考下限非 0 时,低位仍按常模判偏低(防止该规则误伤真实偏低)");
|
|
|
|
|
+ assertTrue(r.isAbnormal());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ============================================================
|
|
|
|
|
+ // L4-1 条件致病菌偏低侧四机制 / 双刃剑解释 + 关联非因果
|
|
|
|
|
+ // ============================================================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void 大肠杆菌偏低_注入四机制关联非因果与U型口径() {
|
|
|
|
|
+ MemberAttribution a = interpretLow("大肠杆菌", "大肠杆菌");
|
|
|
|
|
+ IndicatorReading r = a.getReadings().get(0);
|
|
|
|
|
+
|
|
|
|
|
+ assertNotNull(r.getMeaning());
|
|
|
|
|
+ assertTrue(r.getMeaning().contains("关联非因果"), "应标关联非因果");
|
|
|
|
|
+ assertTrue(r.getMeaning().contains("U 型双向"), "应含 U 型双向口径");
|
|
|
|
|
+ assertTrue(r.getMeaning().contains("低就是好"), "应包含\"低就是好\"不成立的表述");
|
|
|
|
|
+ assertTrue(r.getMeaning().contains("耗氧"), "机制①耗氧维持肠腔厌氧");
|
|
|
|
|
+ assertTrue(r.getMeaning().contains("定植抵抗"), "机制②定植抵抗与生态位");
|
|
|
|
|
+ assertTrue(r.getMeaning().contains("维生素 K2/B 族"), "机制③维生素合成");
|
|
|
|
|
+ assertTrue(r.getMeaning().contains("免疫训练") || r.getMeaning().contains("LPS"), "机制④免疫训练");
|
|
|
|
|
+ assertTrue(r.isAssociationOnly(), "条件致病菌偏低应标 association_only=true");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void 埃希氏菌属偏低_按四机制解释() {
|
|
|
|
|
+ MemberAttribution a = interpretLow("Escherichia", "埃希氏菌属");
|
|
|
|
|
+ IndicatorReading r = a.getReadings().get(0);
|
|
|
|
|
+
|
|
|
|
|
+ assertTrue(r.getMeaning().contains("关联非因果"));
|
|
|
|
|
+ assertTrue(r.getMeaning().contains("耗氧"));
|
|
|
|
|
+ assertTrue(r.getMeaning().contains("定植抵抗"));
|
|
|
|
|
+ assertTrue(r.isAssociationOnly());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void 脆弱拟杆菌偏低_注入双刃剑口径() {
|
|
|
|
|
+ MemberAttribution a = interpretLow("脆弱拟杆菌", "脆弱拟杆菌");
|
|
|
|
|
+ IndicatorReading r = a.getReadings().get(0);
|
|
|
|
|
+
|
|
|
|
|
+ assertNotNull(r.getMeaning());
|
|
|
|
|
+ assertTrue(r.getMeaning().contains("关联非因果"), "应标关联非因果");
|
|
|
|
|
+ assertTrue(r.getMeaning().contains("U 型双向"), "应含 U 型双向口径");
|
|
|
|
|
+ assertTrue(r.getMeaning().contains("双刃剑"), "脆弱拟杆菌为双刃剑(非产毒株诱导 Treg / 产毒株降解 E-cadherin)");
|
|
|
|
|
+ assertTrue(r.getMeaning().contains("Treg"));
|
|
|
|
|
+ assertTrue(r.isAssociationOnly());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void 非条件致病菌偏低_不注入四机制() {
|
|
|
|
|
+ MemberAttribution a = interpretLow("双歧杆菌", "双歧杆菌");
|
|
|
|
|
+ IndicatorReading r = a.getReadings().get(0);
|
|
|
|
|
+
|
|
|
|
|
+ assertNotNull(r.getMeaning());
|
|
|
|
|
+ assertFalse(r.getMeaning().contains("耗氧"), "非条件致病菌不按四机制解释");
|
|
|
|
|
+ assertFalse(r.getMeaning().contains("定植抵抗"));
|
|
|
|
|
+ assertFalse(r.isAssociationOnly(), "非条件致病菌的 association_only 由 G-1 门禁在 L4 后 enforce,解读段不预设");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void 参考下限0的菌判正常后_不产生低侧归因() {
|
|
|
|
|
+ IndicatorReading r = gutReading("大肠杆菌", "0", 0.0);
|
|
|
|
|
+ CompareResult compare = normCompareEngine.compare(Collections.emptyList(),
|
|
|
|
|
+ Collections.singletonList(r), Collections.singletonList(ecoliNorm(0.0)), Collections.emptyMap());
|
|
|
|
|
+ r = compare.getReadings().get(0);
|
|
|
|
|
+
|
|
|
|
|
+ MemberProfile m = new MemberProfile();
|
|
|
|
|
+ m.setMemberId(1L);
|
|
|
|
|
+ m.setName("孩子");
|
|
|
|
|
+ m.setRole("child");
|
|
|
|
|
+ m.setRoleLabel("孩子");
|
|
|
|
|
+ List<MemberAttribution> attributions = interpretEngine.interpret(
|
|
|
|
|
+ Collections.singletonList(r), Collections.singletonList(m));
|
|
|
|
|
+
|
|
|
|
|
+ MemberAttribution a = attributions.get(0);
|
|
|
|
|
+ assertEquals(0, a.getAbnormalCount(), "参考下限 0 判正常后不产生低侧归因节点");
|
|
|
|
|
+ assertNull(a.getReadings().get(0).getMeaning(), "正常项不注入低侧判读文本");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ============================================================
|
|
|
|
|
+ // 低侧机制文本自证:不命中 G-1「缺 X 桥接 / 处方化」红线
|
|
|
|
|
+ // ============================================================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ void 低侧机制文本不命中G1红线_禁桥接禁处方() {
|
|
|
|
|
+ List<String> meanings = Arrays.asList(
|
|
|
|
|
+ interpretLow("大肠杆菌", "大肠杆菌").getReadings().get(0).getMeaning(),
|
|
|
|
|
+ interpretLow("脆弱拟杆菌", "脆弱拟杆菌").getReadings().get(0).getMeaning());
|
|
|
|
|
+
|
|
|
|
|
+ for (String meaning : meanings) {
|
|
|
|
|
+ assertNotNull(meaning);
|
|
|
|
|
+ assertFalse(AttributionLowSideRules.hitsCausalBridge(meaning),
|
|
|
|
|
+ "不应命中缺 X 桥接:\n" + meaning);
|
|
|
|
|
+ assertFalse(AttributionLowSideRules.hitsPrescription(meaning),
|
|
|
|
|
+ "不应命中处方化:\n" + meaning);
|
|
|
|
|
+ // L4-3 偏低不推荐补菌红线(hitsLowSideSupplement)为 T4 任务(WIP,未入 cfclub),
|
|
|
|
|
+ // 本测试不依赖其 API;模板措辞已避开「补/买/吃 + 菌」目标(偏低估不推荐补菌),
|
|
|
|
|
+ // 开发期已对 WIP 门禁超集(含 supplement/strain 回扫)反向验证零命中。
|
|
|
|
|
+ assertTrue(gate.scan(meaning).isEmpty(), "合规门禁 scan 应零违规:\n" + meaning);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ============================================================
|
|
|
|
|
+ // 工具
|
|
|
|
|
+ // ============================================================
|
|
|
|
|
+
|
|
|
|
|
+ private MemberAttribution interpretLow(String code, String displayName) {
|
|
|
|
|
+ List<IndicatorReading> readings = new ArrayList<>();
|
|
|
|
|
+ IndicatorReading r = new IndicatorReading();
|
|
|
|
|
+ r.setMemberId(1L);
|
|
|
|
|
+ r.setSource("gut_flora");
|
|
|
|
|
+ r.setCode(code);
|
|
|
|
|
+ r.setDisplayName(displayName);
|
|
|
|
|
+ r.setValue("0.5");
|
|
|
|
|
+ r.setNumericValue(0.5);
|
|
|
|
|
+ r.setStatus(IndicatorReading.STATUS_LOW);
|
|
|
|
|
+ r.setBand(IndicatorReading.BAND_VERY_LOW);
|
|
|
|
|
+ readings.add(r);
|
|
|
|
|
+
|
|
|
|
|
+ MemberProfile m = new MemberProfile();
|
|
|
|
|
+ m.setMemberId(1L);
|
|
|
|
|
+ m.setName("孩子");
|
|
|
|
|
+ m.setRole("child");
|
|
|
|
|
+ m.setRoleLabel("孩子");
|
|
|
|
|
+ return interpretEngine.interpret(readings, Collections.singletonList(m)).get(0);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static IndicatorReading gutReading(String name, String value, Double numeric) {
|
|
|
|
|
+ IndicatorReading r = new IndicatorReading();
|
|
|
|
|
+ r.setMemberId(1L);
|
|
|
|
|
+ r.setSource("gut_flora");
|
|
|
|
|
+ r.setCode(name);
|
|
|
|
|
+ r.setDisplayName(name);
|
|
|
|
|
+ r.setValue(value);
|
|
|
|
|
+ r.setNumericValue(numeric);
|
|
|
|
|
+ return r;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static NormEntry ecoliNorm(double p5) {
|
|
|
|
|
+ NormEntry n = new NormEntry();
|
|
|
|
|
+ n.setNormType("gut_flora");
|
|
|
|
|
+ n.setCode("大肠杆菌");
|
|
|
|
|
+ n.setDisplayName("大肠杆菌");
|
|
|
|
|
+ n.setAgeBand("all");
|
|
|
|
|
+ n.setP5(p5);
|
|
|
|
|
+ n.setP25(1.5);
|
|
|
|
|
+ n.setP50(3.0);
|
|
|
|
|
+ n.setP75(5.0);
|
|
|
|
|
+ n.setP95(9.0);
|
|
|
|
|
+ n.setSource("attribution_norm_values");
|
|
|
|
|
+ return n;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|