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

merge(S-cfc-89): re-sync cfclub after concurrent pushes before push

Sisyphus Agent 4 дней назад
Родитель
Сommit
8ab7d079ba

+ 278 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/GuheV2RouteContractTest.java

@@ -0,0 +1,278 @@
+package com.etotem.cfc.unit;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.lang.reflect.Modifier;
+
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * 契约级单测:guhe_v2 指纹路由(能力键 {@code guhe.l1.guheV2Route})。
+ *
+ * <p><b>契约来源</b>(计划 2026-11-08-浠艾福平台程序修改计划.md §三-T7 / §一 L1 接入归一化 ① / §2.1):
+ * <blockquote>
+ *   T7 guhe_v2 指纹路由(L1-1):判定顺序 {@code aidefansi→beijing→guhe_v2→standard};
+ *   触发词「疾病相似评估 / 厚壁菌比拟杆菌比值 / 亚健康及营养饮食不合理」;键数自检 32 键。
+ * </blockquote>
+ * 输入基线 §2.1:谷禾 2026-11 新版标准报告 = 旧 21 键打底 + 11 增量键 = <b>32 键</b>;
+ * 判定顺序 {@code aidefansi → beijing → guhe_v2 → standard},旧三类路由不变。</p>
+ *
+ * <p><b>平台现状(src/main 探测,C 档 #1 Adapter 形态)</b>:L1 接入归一化层为
+ * <ul>
+ *   <li>{@code PdfParseService}(本地文本格式检测:isBeijingFormat → beijing,triplet/inline → guhe,无 guhe_v2 分支)</li>
+ *   <li>{@code ReportFingerprintService#detect}(DB 指纹规则 → Python 引擎 → {@code typeId},无 guhe_v2 指纹规则层)</li>
+ *   <li>{@code ReportParserDispatchService#parseReport}(指纹检测 → 专用解析器 / LLM 兜底)</li>
+ * </ul>
+ * 全库(src/main)无 {@code guhe_v2} / {@code aidefansi} 字符串,无 T7 触发词锚点,无 32 键自检入口——
+ * 本能力<b>实现未落地</b>。因此本测试以「反射结构契约」探测契约所需路由标记缺失:能编译、
+ * 能运行、断言失败(红),失败断言原文清晰可贴。</p>
+ *
+ * <p><b>GREEN 升级指引</b>:实现者在 L1 指纹路由层落地四件事后,本测试自动转绿——
+ * <ol>
+ *   <li>注册 {@code guhe_v2} 判定阶段(静态 String 常量值为 {@code "guhe_v2"}),
+ *       使判定链 {@code aidefansi→beijing→guhe_v2→standard} 中 guhe_v2 阶段可寻址;</li>
+ *   <li>暴露三个触发词锚点「疾病相似评估」「厚壁菌比拟杆菌比值」「亚健康及营养饮食不合理」
+ *       作为 guhe_v2 指纹识别特征(DB 指纹规则或 Java 常量均可,测试探测任意静态 String 常量);</li>
+ *   <li>声明判定链顺序常量 {@code aidefansi→beijing→guhe_v2→standard}(值含完整箭头链);</li>
+ *   <li>提供 <b>32 键自检</b> 入口:方法名含 {@code keyCount} / {@code selfCheck},
+ *       或静态 int 常量名含 {@code GUHE_V2} 且值为 32。</li>
+ * </ol>
+ * 届时可将结构探测升级为行为级验证(mock 指纹引擎断言 guhe_v2 报告 → 32 键自检通过)。</p>
+ *
+ * <p><b>权限流说明</b>:本能力为 L1 接入归一化层的内部指纹路由管道(PDF 上传 → 指纹判定 → 提取器分派),
+ * 无用户权限维度,「权限(适用时)」三流中的权限流不适用,以说明性用例标注。</p>
+ */
+class GuheV2RouteContractTest {
+
+    // ------------------------------------------------------------------
+    // 契约锚点(计划 §三-T7 / L1-1 / §2.1)
+    // ------------------------------------------------------------------
+
+    /** 新版 32 键 guhe_v2 格式标记(§2.1:判定链 aidefansi→beijing→guhe_v2→standard) */
+    private static final String ROUTE_STAGE_GUHE_V2 = "guhe_v2";
+
+    /** 判定链顺序(§2.1 / T7):aidefansi→beijing→guhe_v2→standard */
+    private static final String ROUTE_CHAIN = "aidefansi→beijing→guhe_v2→standard";
+
+    /** T7 触发词锚点:guhe_v2 指纹识别特征(§2.1 / T7) */
+    private static final String[] TRIGGER_WORDS = {
+            "疾病相似评估", "厚壁菌比拟杆菌比值", "亚健康及营养饮食不合理"
+    };
+
+    /** T7 键数自检:32 键(旧 21 键打底 + 11 增量键,§2.1) */
+    private static final int GUHE_V2_KEY_COUNT = 32;
+
+    // L1 接入归一化层现有类(src/main 现状探测;实现者在其上落地 guhe_v2 指纹路由分支)
+    private static final String[] ROUTE_CLASSES = {
+            "com.etotem.cfc.service.PdfParseService",
+            "com.etotem.cfc.service.ReportFingerprintService",
+            "com.etotem.cfc.service.ReportParserDispatchService"
+    };
+
+    // ------------------------------------------------------------------
+    // 正常流:guhe_v2 判定阶段注册(判定链中 guhe_v2 阶段可寻址)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("正常流-1:L1 指纹路由层应注册 guhe_v2 判定阶段")
+    void routeLayer_registersGuheV2Stage() {
+        for (String stage : new String[]{ROUTE_STAGE_GUHE_V2, "aidefansi"}) {
+            boolean addressed = false;
+            for (String clazzName : ROUTE_CLASSES) {
+                Class<?> clazz = loadClass(clazzName);
+                if (clazz == null) {
+                    fail("[guhe.l1.guheV2Route] L1 路由类不存在:" + clazzName
+                            + "(T7/L1-1 判定链落点)。");
+                }
+                if (hasStringConstantContaining(clazz, stage)) {
+                    addressed = true;
+                    break;
+                }
+            }
+            assertTrue(addressed,
+                    "[guhe.l1.guheV2Route] L1 指纹路由层(PdfParseService / ReportFingerprintService"
+                            + " / ReportParserDispatchService)应注册判定阶段「" + stage + "」"
+                            + "(静态 String 常量值含之),使判定链 aidefansi→beijing→guhe_v2→standard"
+                            + " 的阶段可寻址(T7/L1-1)。当前 src/main 无「" + stage + "」字符串,实现未落地。");
+        }
+    }
+
+    // ------------------------------------------------------------------
+    // 正常流:guhe_v2 触发词锚点(T7 指纹识别特征)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("正常流-2:guhe_v2 触发词锚点应可寻址(疾病相似评估)")
+    void routeLayer_registersTriggerWord_diseaseSimilarAssessment() {
+        assertTriggerWordAddressable(TRIGGER_WORDS[0]);
+    }
+
+    @Test
+    @DisplayName("正常流-3:guhe_v2 触发词锚点应可寻址(厚壁菌比拟杆菌比值)")
+    void routeLayer_registersTriggerWord_firmicutesBacteroidetesRatio() {
+        assertTriggerWordAddressable(TRIGGER_WORDS[1]);
+    }
+
+    @Test
+    @DisplayName("正常流-4:guhe_v2 触发词锚点应可寻址(亚健康及营养饮食不合理)")
+    void routeLayer_registersTriggerWord_subhealthAndDietUnreasonable() {
+        assertTriggerWordAddressable(TRIGGER_WORDS[2]);
+    }
+
+    // ------------------------------------------------------------------
+    // 边界流:判定链顺序常量 + 键数自检 32 键(T7 / L1-1)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("边界流-1:L1 路由层应声明判定链顺序 aidefansi→beijing→guhe_v2→standard")
+    void routeLayer_declaresRouteChainOrder() {
+        boolean declared = false;
+        for (String clazzName : ROUTE_CLASSES) {
+            Class<?> clazz = loadClass(clazzName);
+            if (clazz == null) continue;
+            if (hasStringConstantContaining(clazz, ROUTE_CHAIN)) {
+                declared = true;
+                break;
+            }
+        }
+        assertTrue(declared,
+                "[guhe.l1.guheV2Route] L1 指纹路由层应声明判定链顺序常量(值含 "
+                        + "aidefansi→beijing→guhe_v2→standard 完整链),保证 guhe_v2 在 beijing 之后、"
+                        + "standard 之前被判定(T7/L1-1,§2.1)。当前未声明,32 键新版报告可能被误判为"
+                        + "标准格式而丢失增量 11 键。");
+    }
+
+    @Test
+    @DisplayName("边界流-2:guhe_v2 路由应提供 32 键自检入口(旧21+增量11=32)")
+    void routeLayer_providesGuheV2KeyCountSelfCheck() {
+        boolean hasSelfCheck = false;
+        for (String clazzName : ROUTE_CLASSES) {
+            Class<?> clazz = loadClass(clazzName);
+            if (clazz == null) continue;
+            // 方法名含 keyCount / selfCheck
+            if (hasMethodNamedLike(clazz, "keyCount") || hasMethodNamedLike(clazz, "selfCheck")) {
+                hasSelfCheck = true;
+                break;
+            }
+            // 或静态 int 常量名含 GUHE_V2 且值为 32
+            if (hasIntConstantNamedLike(clazz, "GUHE_V2", GUHE_V2_KEY_COUNT)) {
+                hasSelfCheck = true;
+                break;
+            }
+        }
+        assertTrue(hasSelfCheck,
+                "[guhe.l1.guheV2Route] L1 路由层应提供 guhe_v2 <b>32 键自检</b>入口"
+                        + "(方法名含 keyCount/selfCheck,或静态 int 常量名含 GUHE_V2 且值为 32)。"
+                        + "契约:旧 21 键打底 + 11 增量键 = 32 键,路由/解析后须自检键数齐全"
+                        + "(T7 键数自检 32 键,§2.1)。当前无自检入口,32 键报告键数完整性无法保证。");
+    }
+
+    // ------------------------------------------------------------------
+    // 结构契约:T7 判定链落点挂载类应存在(GREEN 指引锚点)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("结构契约-1:L1 路由挂载类应存在(GREEN 时在其上注册 guhe_v2 分支)")
+    void routeLayer_mountClassesExist() {
+        for (String clazzName : ROUTE_CLASSES) {
+            Class<?> clazz = loadClass(clazzName);
+            assertTrue(clazz != null,
+                    "[guhe.l1.guheV2Route] L1 路由挂载类不存在:" + clazzName
+                            + "(T7 判定链 aidefansi→beijing→guhe_v2→standard 的落点;"
+                            + "若平台已重构路由层,请按当前实现更新本挂载探测)。");
+        }
+    }
+
+    // ------------------------------------------------------------------
+    // 权限流(不适用):L1 内部指纹路由管道,无用户权限维度
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("权限流:本能力为 L1 内部指纹路由管道,无用户权限维度,权限流不适用")
+    void permissionFlow_notApplicable() {
+        // 说明性用例:契约 AC 为「PDF 上传 → 指纹判定 → 提取器分派」内部管道,无权限校验维度。
+        // 不产生断言失败;仅为三流覆盖标注「不适用」。
+        assertTrue(true, "权限流不适用:guhe.l1.guheV2Route 为 L1 内部指纹路由管道,无用户权限维度。");
+    }
+
+    // ------------------------------------------------------------------
+    // 断言辅助
+    // ------------------------------------------------------------------
+
+    private void assertTriggerWordAddressable(String triggerWord) {
+        boolean addressed = false;
+        for (String clazzName : ROUTE_CLASSES) {
+            Class<?> clazz = loadClass(clazzName);
+            if (clazz == null) continue;
+            if (hasStringConstantContaining(clazz, triggerWord)) {
+                addressed = true;
+                break;
+            }
+        }
+        assertTrue(addressed,
+                "[guhe.l1.guheV2Route] L1 指纹路由层应可寻址 guhe_v2 触发词锚点「" + triggerWord
+                        + "」(静态 String 常量值含之,T7 指纹识别特征,§2.1)。"
+                        + "当前 src/main 无此触发词,32 键 guhe_v2 新版报告无法被指纹识别路由。");
+    }
+
+    // ------------------------------------------------------------------
+    // 反射辅助
+    // ------------------------------------------------------------------
+
+    private Class<?> loadClass(String name) {
+        try {
+            return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
+        } catch (ClassNotFoundException e) {
+            return null;
+        }
+    }
+
+    /** 是否存在静态 String 常量值包含 target(路由阶段/触发词/判定链探针) */
+    private boolean hasStringConstantContaining(Class<?> clazz, String target) {
+        if (clazz == null) return false;
+        for (Field f : clazz.getDeclaredFields()) {
+            if (f.getType() != String.class) continue;
+            if (!Modifier.isStatic(f.getModifiers()) || !Modifier.isFinal(f.getModifiers())) continue;
+            try {
+                f.setAccessible(true);
+                Object v = f.get(null);
+                if (v instanceof String && ((String) v).contains(target)) return true;
+            } catch (Exception ignored) {
+                // 常量读取失败 → 跳过
+            }
+        }
+        return false;
+    }
+
+    /** 是否存在静态 int 常量名含 fragment 且值等于 target(键数自检探针) */
+    private boolean hasIntConstantNamedLike(Class<?> clazz, String fragment, int target) {
+        if (clazz == null) return false;
+        for (Field f : clazz.getDeclaredFields()) {
+            if (f.getType() != int.class && f.getType() != Integer.class) continue;
+            if (!Modifier.isStatic(f.getModifiers()) || !Modifier.isFinal(f.getModifiers())) continue;
+            if (!f.getName().toUpperCase().contains(fragment.toUpperCase())) continue;
+            try {
+                f.setAccessible(true);
+                Object v = f.get(null);
+                if (v instanceof Integer && ((Integer) v) == target) return true;
+            } catch (Exception ignored) {
+                // 常量读取失败 → 跳过
+            }
+        }
+        return false;
+    }
+
+    /** 是否存在方法名(忽略大小写)包含片段 */
+    private boolean hasMethodNamedLike(Class<?> clazz, String fragment) {
+        if (clazz == null) return false;
+        String needle = fragment.toLowerCase();
+        for (Method m : clazz.getDeclaredMethods()) {
+            if (m.getName().toLowerCase().contains(needle)) return true;
+        }
+        return false;
+    }
+}