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

test(guhe.kb.versionSense): S-cfc-83 add RED contract tests for KB version sensing (T9 L2-1)

UnifiedKnowledgeBaseService should expose +guhe2026.2 version mark and the
guhe2026-new-extension module (11 subkeys incl low-side patch subkey),
with idempotent upgrade script (importFromClasspath: overwrite, no accumulation).
RED: 9/11 fail - seed version is 2.0, extension module not registered/loaded.
Sisyphus Agent 4 дней назад
Родитель
Сommit
ec90b7b932

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

@@ -0,0 +1,263 @@
+package com.etotem.cfc.unit;
+
+import com.etotem.cfc.service.UnifiedKnowledgeBaseService;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * 契约级单测:KB 版本感知(能力键 {@code guhe.kb.versionSense},T9 / L2-1)。
+ *
+ * <p>契约来源(2026-11-08-浠艾福平台程序修改计划.md):</p>
+ * <ul>
+ *   <li><b>T9 KB 版本感知(L2-1)</b>:KB 服务读取并暴露「谷禾2026新版扩展知识」(11 子键);
+ *       升级脚本幂等(重复运行只覆盖不累积)。</li>
+ *   <li><b>§三 L2 事实与知识 ①</b>:KB 版本感知 {@code +guhe2026.2}。</li>
+ *   <li><b>§2.2 知识库</b>:{@code 统一知识库.json} 扩展块版本
+ *       {@code 3.0+guhe2026.2}(主)/ {@code 3.1+guhe2026.2}(专家);
+ *       新增子键 {@code 条件致病菌低侧判读补充(人工)}(判读铁律:U 型双向 / 参考下限 0 /
+ *       16S 粒度 / 不推荐补菌 / 关联非因果;大肠杆菌偏低侧四机制;报告原文口径引用,个人值剥离)。</li>
+ * </ul>
+ *
+ * <p><b>实现现状(C 档探测,src/main 现状核实)</b>:{@link UnifiedKnowledgeBaseService}
+ * 已实现 12 个顶层模块({@code MODULE_KEYS} 硬编码,DB 为查询源、classpath 种子兜底),
+ * 但 {@code MODULE_KEYS} 未注册「谷禾2026新版扩展知识」、classpath 种子
+ * {@code /knowledge-base/统一知识库.json} 版本为 {@code 2.0}(无 {@code +guhe2026.2} 标记)、
+ * 顶层无扩展模块。因此本测试以「反射结构契约 + classpath 资源探测 + classpath 内存加载行为」探测实现缺失:
+ * 能编译通过、能运行、断言失败(红),失败断言原文清晰可贴。</p>
+ *
+ * <p><b>RED 预期</b>:上述版本感知与扩展块暴露均未落地,正常流/边界流用例应失败(红);
+ * T9 ③ 的升级脚本入口 {@code importFromClasspath} 已存在(结构确认用例为绿,作为 GREEN 挂载点)。</p>
+ *
+ * <p><b>GREEN 升级指引</b>:实现者(1)同步种子 {@code 统一知识库.json},
+ * 版本改为 {@code 3.0+guhe2026.2} 并在顶层新增 {@code 谷禾2026新版扩展知识} 模块
+ * (11 子键,含 {@code 条件致病菌低侧判读补充(人工)});
+ * (2)在 {@link UnifiedKnowledgeBaseService#MODULE_KEYS} 注册该模块使其可加载、可查询;
+ * 本测试自动转绿,且验证「重复运行升级脚本不累积子键」的幂等语义。</p>
+ *
+ * <p><b>权限流说明</b>:本能力为 KB 加载/版本感知(后端内部数据资产层),无用户权限维度,
+ * 「权限(适用时)」三流中的权限流不适用,以说明性用例标注。</p>
+ */
+class GuheKbVersionSenseContractTest {
+
+    /** 能力键 */
+    private static final String KB_VERSION_SENSE_KEY = "guhe.kb.versionSense";
+
+    /** 契约版本标记(§2.2:3.0+guhe2026.2 主 / 3.1+guhe2026.2 专家) */
+    private static final String VERSION_MARK = "guhe2026.2";
+
+    /** T9 要求暴露的 KB 扩展模块键 */
+    private static final String EXTENSION_MODULE = "谷禾2026新版扩展知识";
+
+    /** 扩展块 11 子键中的低侧补丁块(§2.2,T1 可查询对象) */
+    private static final String LOW_SIDE_PATCH = "条件致病菌低侧判读补充(人工)";
+
+    /** 低侧补丁块判读铁律关键概念(§2.2) */
+    private static final List<String> LOW_SIDE_CONCEPTS = Arrays.asList(
+            "参考下限", "16S", "不推荐补菌", "关联非因果");
+
+    /** classpath 种子文件(与 UnifiedKnowledgeBaseService.SEED_CLASSPATH 同源) */
+    private static final String SEED_CLASSPATH = "/knowledge-base/统一知识库.json";
+
+    // ------------------------------------------------------------------
+    // 正常流:KB 版本感知(T9 L2-1 ①:版本 +guhe2026.2)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("正常流-1:种子 统一知识库.json 版本应含 guhe2026.2 标记")
+    void seedJson_metaVersion_containsGuhe2026_2() throws Exception {
+        Map<String, Object> seed = readSeed();
+        Map<String, Object> meta = asMap(seed.get("元数据"));
+        assertNotNull(meta,
+                "[" + KB_VERSION_SENSE_KEY + "] 种子 元数据 模块缺失(T9 L2-1 ①,§2.2)。");
+        Object v = meta.get("版本");
+        assertNotNull(v,
+                "[" + KB_VERSION_SENSE_KEY + "] 种子 元数据.版本 缺失(T9 版本感知锚点)。");
+        String version = String.valueOf(v);
+        assertTrue(version.contains(VERSION_MARK),
+                "[" + KB_VERSION_SENSE_KEY + "] 统一知识库.json 版本应含 +guhe2026.2(当前=" + version
+                        + ")。契约:3.0+guhe2026.2(主)/ 3.1+guhe2026.2(专家),T9 L2-1 ①。");
+    }
+
+    @Test
+    @DisplayName("正常流-2:UnifiedKnowledgeBaseService.getVersion() 应含 guhe2026.2 标记")
+    void unifiedKbService_getVersion_containsGuhe2026_2() throws Exception {
+        UnifiedKnowledgeBaseService svc = newLoadedService();
+        String version = svc.getVersion();
+        assertTrue(version != null && version.contains(VERSION_MARK),
+                "[" + KB_VERSION_SENSE_KEY + "] UnifiedKnowledgeBaseService.getVersion() 应含 +guhe2026.2"
+                        + "(当前=" + version + ")。T9:KB 服务读取并暴露版本感知标记。");
+    }
+
+    // ------------------------------------------------------------------
+    // 正常流:暴露「谷禾2026新版扩展知识」模块(T9 L2-1 ②)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("正常流-3:种子顶层应含「谷禾2026新版扩展知识」模块")
+    void seedJson_containsExtensionModule() throws Exception {
+        Map<String, Object> seed = readSeed();
+        assertTrue(seed.containsKey(EXTENSION_MODULE),
+                "[" + KB_VERSION_SENSE_KEY + "] 统一知识库.json 顶层应含「谷禾2026新版扩展知识」"
+                        + "(当前只有 " + seed.size() + " 个模块)。T9:KB 读取并暴露扩展知识(§2.2)。");
+    }
+
+    @Test
+    @DisplayName("正常流-4:UnifiedKnowledgeBaseService.MODULE_KEYS 应注册「谷禾2026新版扩展知识」")
+    void unifiedKbService_moduleKeys_registerExtensionModule() throws Exception {
+        Field f = UnifiedKnowledgeBaseService.class.getDeclaredField("MODULE_KEYS");
+        f.setAccessible(true);
+        @SuppressWarnings("unchecked")
+        List<String> moduleKeys = (List<String>) f.get(null);
+        assertTrue(moduleKeys.contains(EXTENSION_MODULE),
+                "[" + KB_VERSION_SENSE_KEY + "] UnifiedKnowledgeBaseService.MODULE_KEYS 应注册「谷禾2026新版扩展知识」"
+                        + "(当前 " + moduleKeys.size() + " 个模块均不含)。T9:注册后 KB 服务加载时才会读取并暴露。");
+    }
+
+    @Test
+    @DisplayName("正常流-5:getSection(谷禾2026新版扩展知识) 应非空(可查询)")
+    void unifiedKbService_getSection_extensionModule_notNull() throws Exception {
+        UnifiedKnowledgeBaseService svc = newLoadedService();
+        Map<String, Object> ext = svc.getSection(EXTENSION_MODULE);
+        assertNotNull(ext,
+                "[" + KB_VERSION_SENSE_KEY + "] UnifiedKnowledgeBaseService.getSection(谷禾2026新版扩展知识)"
+                        + " 应非空。T9:KB 服务读取并暴露扩展块(L2-1 ②)。");
+    }
+
+    // ------------------------------------------------------------------
+    // 边界流:扩展块 11 子键 + 低侧补丁块(§2.2 / T9 T1)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("边界流-1:扩展块应含 11 子键")
+    void extensionModule_hasElevenSubkeys() throws Exception {
+        UnifiedKnowledgeBaseService svc = newLoadedService();
+        Map<String, Object> ext = svc.getSection(EXTENSION_MODULE);
+        assertNotNull(ext,
+                "[" + KB_VERSION_SENSE_KEY + "] getSection(谷禾2026新版扩展知识) 为空,无法验证 11 子键(T9)。");
+        assertEquals(11, ext.size(),
+                "[" + KB_VERSION_SENSE_KEY + "] 谷禾2026新版扩展知识 应含 11 子键(当前 " + ext.size()
+                        + ")。契约:扩展块 11 子键逐字段一致(§2.2)。");
+    }
+
+    @Test
+    @DisplayName("边界流-2:11 子键应含「条件致病菌低侧判读补充(人工)」")
+    void extensionModule_containsLowSidePatchSubkey() throws Exception {
+        UnifiedKnowledgeBaseService svc = newLoadedService();
+        Map<String, Object> ext = svc.getSection(EXTENSION_MODULE);
+        assertNotNull(ext,
+                "[" + KB_VERSION_SENSE_KEY + "] getSection(谷禾2026新版扩展知识) 为空,无法验证低侧补丁块(§2.2)。");
+        assertTrue(ext.containsKey(LOW_SIDE_PATCH),
+                "[" + KB_VERSION_SENSE_KEY + "] 谷禾2026新版扩展知识 应含子键「条件致病菌低侧判读补充(人工)」"
+                        + "(当前子键 " + ext.keySet() + ")。新增子键为该补丁块(§2.2)。");
+    }
+
+    @Test
+    @DisplayName("边界流-3:低侧补丁块应含判读铁律关键概念")
+    void lowSidePatch_containsJudgmentConcepts() throws Exception {
+        UnifiedKnowledgeBaseService svc = newLoadedService();
+        Map<String, Object> ext = svc.getSection(EXTENSION_MODULE);
+        assertNotNull(ext,
+                "[" + KB_VERSION_SENSE_KEY + "] getSection(谷禾2026新版扩展知识) 为空,无法验证低侧补丁块内容(§2.2)。");
+        Map<String, Object> patch = asMap(ext.get(LOW_SIDE_PATCH));
+        assertNotNull(patch,
+                "[" + KB_VERSION_SENSE_KEY + "] 低侧补丁块「条件致病菌低侧判读补充(人工)」应为可查询子键对象(§2.2)。");
+        String json = new ObjectMapper().writeValueAsString(patch);
+        for (String concept : LOW_SIDE_CONCEPTS) {
+            assertTrue(json.contains(concept),
+                    "[" + KB_VERSION_SENSE_KEY + "] 低侧补丁块应含判读铁律概念「" + concept
+                            + "」(U 型双向 / 参考下限 0 / 16S 粒度 / 不推荐补菌 / 关联非因果,§2.2)。");
+        }
+    }
+
+    // ------------------------------------------------------------------
+    // 边界流:升级脚本幂等(T9 L2-1 ③:重复运行只覆盖不累积)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("边界流-4:升级脚本重复运行后扩展块子键数恒为 11(幂等不累积)")
+    void upgradeScript_repeatRun_idempotentNoAccumulation() throws Exception {
+        UnifiedKnowledgeBaseService svc = newLoadedService();
+        svc.importFromClasspath(false);
+        svc.importFromClasspath(false);
+        Map<String, Object> ext = svc.getSection(EXTENSION_MODULE);
+        assertNotNull(ext,
+                "[" + KB_VERSION_SENSE_KEY + "] 升级脚本(importFromClasspath)运行后 getSection(谷禾2026新版扩展知识)"
+                        + " 应非空。T9 ③:重复运行只覆盖不累积。");
+        assertEquals(11, ext.size(),
+                "[" + KB_VERSION_SENSE_KEY + "] 重复运行升级脚本后扩展块子键数应恒为 11(幂等:只覆盖不累积,"
+                        + "若累积会变为 22;当前 " + ext.size() + ")。T9 L2-1 ③。");
+    }
+
+    // ------------------------------------------------------------------
+    // 结构契约:T9 ③ 升级脚本入口挂载点(已存在 → 说明性绿,作为 GREEN 指引)
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("结构契约-1:升级脚本入口 importFromClasspath 应存在(T9 ③ 挂载点)")
+    void unifiedKbService_hasUpgradeScriptEntry() throws Exception {
+        boolean found = false;
+        for (Method m : UnifiedKnowledgeBaseService.class.getDeclaredMethods()) {
+            if (m.getName().equals("importFromClasspath")) {
+                found = true;
+                break;
+            }
+        }
+        assertTrue(found,
+                "[" + KB_VERSION_SENSE_KEY + "] UnifiedKnowledgeBaseService 应提供 importFromClasspath 升级脚本入口"
+                        + "(T9 ③ 幂等挂载点;GREEN 时向其中注册「谷禾2026新版扩展知识」即可)。");
+    }
+
+    // ------------------------------------------------------------------
+    // 权限流(不适用):KB 版本感知为后端数据资产层,无用户权限维度
+    // ------------------------------------------------------------------
+
+    @Test
+    @DisplayName("权限流:本能力为 KB 加载/版本感知(后端数据资产层),无用户权限维度,权限流不适用")
+    void permissionFlow_notApplicable() {
+        // 说明性用例:契约 AC 为「KB 服务读取并暴露版本感知 + 扩展块」,属内部数据资产层,无权限校验维度。
+        // 不产生断言失败;仅为三流覆盖标注「不适用」。
+        assertTrue(true, "权限流不适用:guhe.kb.versionSense 为 KB 加载/版本感知,无用户权限维度。");
+    }
+
+    // ------------------------------------------------------------------
+    // 反射/资源辅助(无 Spring / 无 DB / 无 Mockito 之外的依赖)
+    // ------------------------------------------------------------------
+
+    /** 实例化 UnifiedKnowledgeBaseService,反射注入 ObjectMapper,classpath 内存加载(mapper 为 null 时内部降级捕获) */
+    private UnifiedKnowledgeBaseService newLoadedService() throws Exception {
+        UnifiedKnowledgeBaseService svc = new UnifiedKnowledgeBaseService();
+        Field om = UnifiedKnowledgeBaseService.class.getDeclaredField("objectMapper");
+        om.setAccessible(true);
+        om.set(svc, new ObjectMapper());
+        svc.load(); // public 加载入口:DB 访问异常被服务内部降级捕获,回退 classpath 种子内存模式
+        return svc;
+    }
+
+    /** 读取 classpath 种子 统一知识库.json 为 Map */
+    private Map<String, Object> readSeed() throws Exception {
+        try (InputStream is = getClass().getResourceAsStream(SEED_CLASSPATH)) {
+            assertNotNull(is,
+                    "[" + KB_VERSION_SENSE_KEY + "] classpath 种子 /knowledge-base/统一知识库.json 不存在。");
+            return new ObjectMapper().readValue(is, new TypeReference<Map<String, Object>>() {
+            });
+        }
+    }
+
+    @SuppressWarnings("unchecked")
+    private Map<String, Object> asMap(Object o) {
+        return o instanceof Map ? (Map<String, Object>) o : null;
+    }
+}