فهرست منبع

test(S-cfc-82): guhe.l1.pageTailClean 分页残段清洗下沉 RED 契约单测 (T12/L1-2)

Sisyphus Agent 4 روز پیش
والد
کامیت
12e4856918
1فایلهای تغییر یافته به همراه263 افزوده شده و 0 حذف شده
  1. 263 0
      cfc-backend/src/test/java/com/etotem/cfc/unit/GuhePageTailCleanContractTest.java

+ 263 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/GuhePageTailCleanContractTest.java

@@ -0,0 +1,263 @@
+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.pageTailClean})。
+ *
+ * <p><b>契约来源</b>(计划 2026-11-08-浠艾福平台程序修改计划.md §三-T12 / §一-L1 接入归一化 ② / §2.1):
+ * <blockquote>
+ *   T12 分页残段清洗下沉(L1-2):Adapter 调用 {@code extract_guhe_v2()} <b>自动获得清洗</b>;
+ *   <b>校验层断言</b>物种库条目不含 {@code \d{1,3}/16\d}。
+ * </blockquote>
+ * 输入基线 §2.1:分页残段清洗用 {@code _PAGE_TAIL} / {@code _strip_table_tail()} 根治
+ * 「与健康相关菌评估」分页表拼接(残段样例 {@code 34/167 0.0033 参考 0-0.1746% ND}),
+ * 个人层丰度不再混入知识层;清洗后 76 条、残段 0 命中。</p>
+ *
+ * <p><b>平台现状(src/main 探测,C 档 #1 Adapter 形态)</b>:L1 接入归一化层为
+ * <ul>
+ *   <li>{@code PdfParseService}(文本解析,{@code reportFormat} 仅 {@code "beijing"} / {@code "guhe"},无版本维度,
+ *       guhe 报告按 triplet/inline 解析,无分页残段清洗步骤)</li>
+ *   <li>{@code ReportParserDispatchService#parseReport}(指纹检测 → 专用解析器 / 2b. LLM 兜底)</li>
+ *   <li>{@code ReportFingerprintService#detect}(DB 指纹规则 → Python 引擎 → {@code typeId})</li>
+ * </ul>
+ * 全库无分页残段清洗入口、无 {@code _PAGE_TAIL} 残段标记常量、无 {@code \d{1,3}/16\d}
+ * 残段断言正则——本能力<b>实现未落地</b>。因此本测试以「反射结构契约」探测契约所需标记缺失:
+ * 能编译通过、能运行、断言失败(红),失败断言原文清晰可贴。</p>
+ *
+ * <p><b>GREEN 升级指引</b>:实现者在 L1 接入归一化层落地四件事后,本测试自动转绿——
+ * <ol>
+ *   <li>{@code PdfParseService}(或分派层)提供分页残段清洗<b>执行入口</b>(方法名含
+ *       {@code TableTailClean}/{@code PageTail}/{@code StripTail},如 {@code stripTableTail}——
+ *       Python {@code _strip_table_tail()} 的 Java 下沉),在 guhe 报告解析时自动调用;</li>
+ *   <li>提供分页残段清洗<b>校验断言入口</b>(方法名含 {@code AssertTailClean}/{@code ValidateTail}
+ *       等),断言清洗后产物「残段 0 命中」;</li>
+ *   <li>声明<b>残段标记常量</b>(静态 String 常量,字段名/值含 {@code PAGE_TAIL} / 「分页」/「残段」,
+ *       值含残段样例 {@code 34/167});</li>
+ *   <li>声明<b>残段断言正则</b>(静态 String 常量,值含 {@code \d{1,3}/16\d} 模式,
+ *       用于校验层断言物种库条目不含该模式)。</li>
+ * </ol>
+ * 届时可将结构探测升级为行为级验证(喂入含 {@code 34/167 0.0033 参考 0-0.1746% ND} 残段的文本,
+ * 断言清洗后物种库条目不含 {@code \d{1,3}/16\d}、清洗后 76 条)。</p>
+ *
+ * <p><b>与同批契约的边界</b>:{@code extract_guhe_v2} 提取器寻址(§2.1 Adapter 调用点)
+ * 由同批 {@code guhe.l1.shellGuard}(T13 空壳守卫)契约覆盖,本契约不重复探测;
+ * 本契约聚焦 T12 特有的「清洗入口 + 残段标记 + 残段断言正则」三面。</p>
+ *
+ * <p><b>权限流说明</b>:本能力为 L1 接入归一化层的内部解析/清洗管道(报告解析 → 残段清洗 →
+ * 校验断言),无用户权限维度,三流中的权限流不适用,以说明性用例标注。</p>
+ */
+class GuhePageTailCleanContractTest {
+
+    // ------------------------------------------------------------------
+    // 契约锚点(计划 §三-T12 / L1-2 / §2.1)
+    // ------------------------------------------------------------------
+
+    /** 能力键 */
+    private static final String CAP_KEY = "guhe.l1.pageTailClean";
+
+    /** 残段样例(§2.1:分页表拼接残段,如 34/167 ...) */
+    private static final String TAIL_SAMPLE = "34/167";
+
+    /** 残段断言正则语义片段(校验层断言物种库条目不含 \d{1,3}/16\d;Java 字符串中 \d 转义为 \\d) */
+    private static final String TAIL_REGEX_FRAGMENT = "/16\\d";
+
+    /** 清洗后条数(§2.1:清洗后 76 条)——GREEN 行为级验证锚点,结构探测阶段仅记录 */
+    private static final int CLEAN_COUNT_76 = 76;
+
+    // L1 接入归一化层现有类(src/main 现状探测;实现者在其上落地残段清洗)
+    private static final String PARSED_REPORT_RESULT = "com.etotem.cfc.dto.ParsedReportResult";
+    private static final String DISPATCH_SERVICE = "com.etotem.cfc.service.ReportParserDispatchService";
+    private static final String FINGERPRINT_SERVICE = "com.etotem.cfc.service.ReportFingerprintService";
+    private static final String PDF_PARSE_SERVICE = "com.etotem.cfc.service.PdfParseService";
+
+    /** 分页残段清洗「执行入口」方法名探针(大小写不敏感) */
+    private static final String[] CLEAN_ENTRY_FRAGMENTS = {
+            "cleantabletail", "striptabletail", "cleanpagetail", "strippagetail",
+            "tabletailclean", "pagetailclean", "removetail", "striptail"
+    };
+
+    /** 分页残段清洗「校验断言入口」方法名探针(大小写不敏感) */
+    private static final String[] ASSERT_ENTRY_FRAGMENTS = {
+            "asserttailclean", "assertnotail", "validatetail", "assertclean", "tailclean"
+    };
+
+    /** 残段标记常量名探针(大小写不敏感) */
+    private static final String[] TAIL_MARK_NAME_FRAGMENTS = {
+            "pagetail", "page_tail", "残段", "分页"
+    };
+
+    /** 残段断言正则常量名探针(大小写不敏感) */
+    private static final String[] TAIL_REGEX_NAME_FRAGMENTS = {
+            "tail", "残段", "regex", "pattern", "断言"
+    };
+
+    // ------------------------------------------------------------------
+    // 正常流:分页残段清洗执行入口(Adapter 调用 extract_guhe_v2() 自动获得清洗)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("正常流-1:L1 层应提供分页残段清洗执行入口(_strip_table_tail 的 Java 下沉)")
+    void l1Layer_providesTailCleanEntry() {
+        boolean hasEntry = anyClassHasMethodNamedLike(new String[]{PDF_PARSE_SERVICE, DISPATCH_SERVICE,
+                FINGERPRINT_SERVICE, PARSED_REPORT_RESULT}, CLEAN_ENTRY_FRAGMENTS);
+        assertTrue(hasEntry,
+                "[" + CAP_KEY + "] L1 接入归一化层(PdfParseService / ReportParserDispatchService / "
+                        + "ReportFingerprintService / ParsedReportResult)应提供分页残段清洗执行入口"
+                        + "(方法名含 TableTailClean / PageTail / StripTail,如 stripTableTail——"
+                        + "Python _strip_table_tail() 的 Java 下沉),在 guhe 报告解析时自动调用(T12/L1-2)。"
+                        + "当前 parseText 直接按 triplet/inline 解析,无分页残段清洗步骤,"
+                        + "「与健康相关菌评估」分页表拼接残段将混入个人层丰度。");
+    }
+
+    @Test
+    @DisplayName("正常流-2:L1 层应提供残段清洗校验断言入口(断言清洗后残段 0 命中)")
+    void l1Layer_providesTailCleanAssertEntry() {
+        boolean hasEntry = anyClassHasMethodNamedLike(new String[]{PDF_PARSE_SERVICE, DISPATCH_SERVICE,
+                FINGERPRINT_SERVICE, PARSED_REPORT_RESULT}, ASSERT_ENTRY_FRAGMENTS);
+        assertTrue(hasEntry,
+                "[" + CAP_KEY + "] L1 接入归一化层应提供残段清洗校验断言入口"
+                        + "(方法名含 AssertTailClean / ValidateTail / AssertClean),"
+                        + "断言清洗后产物「残段 0 命中」(§2.1 清洗后 76 条、残段 0 命中;T12/L1-2 校验层断言)。"
+                        + "当前无残段断言步骤,无法验证清洗效果。");
+    }
+
+    // ------------------------------------------------------------------
+    // 边界流:残段标记常量 _PAGE_TAIL
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("边界流-1:L1 层应声明 _PAGE_TAIL 残段标记常量(值含残段样例 34/167)")
+    void l1Layer_declaresPageTailMarker() {
+        boolean hasTailMark = anyClassHasStringConstantContaining(new String[]{PDF_PARSE_SERVICE,
+                        DISPATCH_SERVICE, FINGERPRINT_SERVICE, PARSED_REPORT_RESULT},
+                new String[]{TAIL_SAMPLE, "/16"}, TAIL_MARK_NAME_FRAGMENTS);
+        assertTrue(hasTailMark,
+                "[" + CAP_KEY + "] L1 接入归一化层应声明 _PAGE_TAIL 残段标记常量"
+                        + "(静态 String 常量,字段名/值含 PAGE_TAIL / 分页 / 残段,值含残段样例 "
+                        + TAIL_SAMPLE + "),作为分页表拼接残段的识别依据(§2.1 / T12)。"
+                        + "当前无残段标记常量,残段无法被识别与清洗。");
+    }
+
+    @Test
+    @DisplayName("边界流-2:L1 层应声明残段断言正则 \\d{1,3}/16\\d(校验层断言物种库条目不含)")
+    void l1Layer_declaresTailAssertRegex() {
+        boolean hasRegex = anyClassHasStringConstantContaining(new String[]{PDF_PARSE_SERVICE,
+                        DISPATCH_SERVICE, FINGERPRINT_SERVICE, PARSED_REPORT_RESULT},
+                new String[]{TAIL_REGEX_FRAGMENT}, TAIL_REGEX_NAME_FRAGMENTS);
+        assertTrue(hasRegex,
+                "[" + CAP_KEY + "] L1 接入归一化层应声明残段断言正则常量"
+                        + "(静态 String 常量,值含 " + TAIL_REGEX_FRAGMENT + " 即 \\d{1,3}/16\\d 模式,"
+                        + "用于校验层断言物种库条目不含 \\d{1,3}/16\\d,T12/L1-2)。"
+                        + "当前无残段断言正则,无法对清洗结果做「残段 0 命中」的程序化校验。");
+    }
+
+    // ------------------------------------------------------------------
+    // 结构契约:parseText 解析管线挂载点(已存在 → 说明性绿,GREEN 挂载点)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("结构契约-1:PdfParseService.parseText 解析管线入口应存在(残段清洗挂载点)")
+    void pdfParseService_hasParseTextPipeline() {
+        Class<?> clazz = loadClass(PDF_PARSE_SERVICE);
+        if (clazz == null) {
+            fail("[" + CAP_KEY + "] PdfParseService 类不存在(L1 文本解析入口),"
+                    + "无法验证 parseText 管线(GREEN 挂载点)。");
+        }
+        boolean found = false;
+        for (Method m : clazz.getDeclaredMethods()) {
+            if (m.getName().equals("parseText")) {
+                found = true;
+                break;
+            }
+        }
+        assertTrue(found,
+                "[" + CAP_KEY + "] PdfParseService 应提供 parseText 管线入口"
+                        + "(T12 分页残段清洗挂载点;GREEN 时在解析前/后调用残段清洗 + 校验断言即可)。");
+    }
+
+    // ------------------------------------------------------------------
+    // 权限流(不适用):L1 内部解析/清洗管道,无用户权限维度
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("权限流:本能力为 L1 接入归一化内部解析/清洗管道,无用户权限维度,权限流不适用")
+    void permissionFlow_notApplicable() {
+        // 说明性用例:契约 AC 为「报告解析 → 残段清洗 → 校验断言」内部管道,无权限校验维度。
+        // 不产生断言失败;仅为三流覆盖标注「不适用」。
+        assertTrue(true, "权限流不适用:" + CAP_KEY + " 为 L1 内部解析/清洗管道,无用户权限维度。");
+    }
+
+    // ------------------------------------------------------------------
+    // 反射辅助
+    // ------------------------------------------------------------------
+
+    private Class<?> loadClass(String name) {
+        try {
+            return Class.forName(name, false, Thread.currentThread().getContextClassLoader());
+        } catch (ClassNotFoundException e) {
+            return null;
+        }
+    }
+
+    /** 任一候选类存在方法名(忽略大小写)包含任一片段 */
+    private boolean anyClassHasMethodNamedLike(String[] classNames, String[] fragments) {
+        for (String cn : classNames) {
+            Class<?> clazz = loadClass(cn);
+            if (clazz == null) continue;
+            for (Method m : clazz.getDeclaredMethods()) {
+                String name = m.getName().toLowerCase();
+                for (String frag : fragments) {
+                    if (name.contains(frag)) return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 任一候选类存在静态 String 常量:值包含任一 valueFragment,且字段名(忽略大小写)
+     * 含任一 nameFragment —— 残段标记 / 残段断言正则常量探针。
+     * 收紧理由:PdfParseService 存在多个无关的 KNOWN_* 静态列表(元素含大量中文字符串),
+     * 仅按值匹配会误绿;常量字段名需带残段/断言语义以锚定「分页残段」契约(T12)。
+     */
+    private boolean anyClassHasStringConstantContaining(String[] classNames, String[] valueFragments,
+                                                         String[] nameFragments) {
+        for (String cn : classNames) {
+            Class<?> clazz = loadClass(cn);
+            if (clazz == null) continue;
+            for (Field f : clazz.getDeclaredFields()) {
+                if (f.getType() != String.class) continue;
+                if (!Modifier.isStatic(f.getModifiers()) || !Modifier.isFinal(f.getModifiers())) continue;
+                String name = f.getName().toLowerCase();
+                boolean nameMatch = false;
+                for (String nf : nameFragments) {
+                    if (name.contains(nf.toLowerCase())) {
+                        nameMatch = true;
+                        break;
+                    }
+                }
+                if (!nameMatch) continue;
+                try {
+                    f.setAccessible(true);
+                    Object v = f.get(null);
+                    if (!(v instanceof String)) continue;
+                    for (String vf : valueFragments) {
+                        if (((String) v).contains(vf)) return true;
+                    }
+                } catch (Exception ignored) {
+                    // 常量读取失败 → 跳过
+                }
+            }
+        }
+        return false;
+    }
+}