|
|
@@ -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;
|
|
|
+ }
|
|
|
+}
|