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