diff --git a/src/test/java/com/codechecker/cache/RecordingTodoReplaceScenarioTest.java b/src/test/java/com/codechecker/cache/RecordingTodoReplaceScenarioTest.java new file mode 100644 index 0000000..7d65bbe --- /dev/null +++ b/src/test/java/com/codechecker/cache/RecordingTodoReplaceScenarioTest.java @@ -0,0 +1,139 @@ +package com.codechecker.cache; + +import com.codechecker.cache.detector.RedisWritePointDetector; +import com.codechecker.cache.detector.WritePoint; +import com.codechecker.cache.diff.ChangeType; +import com.codechecker.cache.diff.SchemaChange; +import com.codechecker.cache.report.CheckReport; +import com.codechecker.cache.report.KeyStructureChange; +import com.codechecker.cache.report.ReportBuilder; +import com.codechecker.cache.schema.JavaSchemaExtractor; +import com.codechecker.cache.schema.SkeletonJsonRenderer; +import com.codechecker.cache.schema.SourceIndex; +import com.codechecker.cache.schema.TypeSchema; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.Collections; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * 复现业务仓实跑误报(jnpf-java-cloud / RecordingTodoDomainServiceImpl#replaceText): + *
+ * - Key --> recording:todo:replace:*:*
+ *   类型: <未解析>
+ *   value 新增为: “{}”
+ * - Key --> recording:todo:replace:title:*:*
+ *   类型: <未解析>
+ *   value 新增为: “{}”
+ * 
+ * 根因:{@code stringRedisTemplate.opsForValue().set(key, String变量)} 被 W04 命中, + * String 无法展开 Schema → 空骨架 + WRITE_POINT_ADDED。 + */ +class RecordingTodoReplaceScenarioTest { + + private static final Set PATTERNS = new HashSet<>(Arrays.asList("W01", "W02", "W03", "W04", "W05")); + + @Test + void reproducesUnresolvedStringCacheFalsePositiveLikeWeCom() { + String constants = TestSupport.fixture("fixtures/recording-todo/RecordingTodoConstants.txt"); + String service = TestSupport.fixture("fixtures/recording-todo/RecordingTodoDomainServiceImpl.txt"); + + SourceIndex index = new SourceIndex(); + index.addSource(constants); + index.addSource(service); + + List wps = new RedisWritePointDetector(index, PATTERNS).detect("RecordingTodoDomainServiceImpl.java", service); + wps = wps.stream().sorted(Comparator.comparing(WritePoint::getLineNumber)).collect(Collectors.toList()); + + assertEquals(2, wps.size(), "应检出 content / title 两处 set"); + + WritePoint contentWp = wps.get(0); + WritePoint titleWp = wps.get(1); + + // --- 检测层:与实跑一致 --- + assertEquals("W04", contentWp.getPattern()); + assertEquals("W04", titleWp.getPattern()); + assertEquals("recording:todo:replace:*:*", contentWp.getResolvedKeyPattern()); + assertEquals("recording:todo:replace:title:*:*", titleWp.getResolvedKeyPattern()); + assertNull(contentWp.getResolvedValueType(), "String 不在本仓 SourceIndex,FQN 应为 null"); + assertNull(titleWp.getResolvedValueType()); + assertTrue(contentWp.getEnclosingMethod().contains("replaceText") || "replaceText".equals(contentWp.getEnclosingMethod())); + assertTrue(contentWp.getEnclosingClass().endsWith("RecordingTodoDomainServiceImpl")); + + // --- Schema:空结构 → "{}" --- + JavaSchemaExtractor extractor = new JavaSchemaExtractor(index, 8); + SkeletonJsonRenderer renderer = new SkeletonJsonRenderer(); + TypeSchema contentSchema = extractor.extract(contentWp.getResolvedValueType(), contentWp.isRootArray()); + TypeSchema titleSchema = extractor.extract(titleWp.getResolvedValueType(), titleWp.isRootArray()); + assertTrue(contentSchema.isEmpty()); + assertTrue(titleSchema.isEmpty()); + assertEquals("{}", renderer.render(contentSchema)); + assertEquals("{}", renderer.render(titleSchema)); + + // --- 报告层:复现企微「value 新增为 / 类型未解析」 --- + CheckReport report = buildAddedWritePointReport(contentWp, titleWp, extractor, renderer); + String md = new ReportBuilder("【序列化结构变更】").toMarkdown(report); + + assertTrue(md.contains("recording:todo:replace:*:*") || md.contains("recording:todo:replace:*:*"), "应包含 content key(企微转义后可为全角*),实际:\n" + md); + assertTrue(md.contains("recording:todo:replace:title:*:*") || md.contains("recording:todo:replace:title:*:*"), "应包含 title key,实际:\n" + md); + assertTrue(md.contains("<未解析>"), "类型应展示为 <未解析>,实际:\n" + md); + assertTrue(md.contains("value 新增为"), "应走新增写入文案,实际:\n" + md); + assertTrue(md.contains("“{}”") || md.contains("\"{}\"") || md.contains("{}"), "骨架应为空对象 {},实际:\n" + md); + assertTrue(md.contains("RecordingTodoDomainServiceImpl#replaceText"), "位置应指向 replaceText,实际:\n" + md); + } + + /** + * 模拟 SchemaCheckAnalyzer 对「文件新增写入点」的聚合与展示路径(无 git)。 + */ + private static CheckReport buildAddedWritePointReport(WritePoint contentWp, WritePoint titleWp, + JavaSchemaExtractor extractor, + SkeletonJsonRenderer renderer) { + CheckReport report = new CheckReport(); + report.setRepository("jnpf-java-cloud"); + report.setBranch("patrol/dev_patrol_2.0"); + report.setOldSha("adbb15c6"); + report.setNewSha("6ddc72dc"); + report.setModifier("xgl"); + report.setModifyTime("2026-07-29 17:57:02"); + + for (WritePoint wp : Arrays.asList(contentWp, titleWp)) { + SchemaChange added = new SchemaChange(ChangeType.WRITE_POINT_ADDED); + added.setKeyPattern(wp.getResolvedKeyPattern()); + added.setKeyExpression(wp.getKeyExpression()); + added.setWriteLocation(wp.location()); + added.setValueType(displayType(wp)); + added.setMessage("新增缓存写入点,value 类型: " + displayType(wp)); + report.getChanges().add(added); + + TypeSchema schema = extractor.extract(wp.getResolvedValueType(), wp.isRootArray()); + KeyStructureChange kc = new KeyStructureChange(); + kc.setKeyPattern(wp.getResolvedKeyPattern()); + kc.setKeyExpression(wp.getKeyExpression()); + kc.setWriteLocation(wp.location()); + kc.setValueType(displayType(wp)); + kc.setKeyUnresolved(false); + kc.setOldSkeletonJson(""); + kc.setNewSkeletonJson(renderer.render(schema, Collections.emptySet(), SkeletonJsonRenderer.DEFAULT_MAX_LEN)); + kc.getFieldDetails().add(added); + report.getKeyChanges().add(kc); + } + return report; + } + + private static String displayType(WritePoint wp) { + String fqn = wp.getResolvedValueType(); + if (fqn == null) { + return "<未解析>"; + } + return fqn.contains(".") ? fqn.substring(fqn.lastIndexOf('.') + 1) : fqn; + } +} diff --git a/src/test/resources/fixtures/recording-todo/RecordingTodoConstants.txt b/src/test/resources/fixtures/recording-todo/RecordingTodoConstants.txt new file mode 100644 index 0000000..19d52e1 --- /dev/null +++ b/src/test/resources/fixtures/recording-todo/RecordingTodoConstants.txt @@ -0,0 +1,13 @@ +package com.fantaibao.iot.domain.constant; + +/** + * 精简自业务仓 RecordingTodoConstants(复现非序列化 String 缓存误报)。 + */ +public final class RecordingTodoConstants { + + private RecordingTodoConstants() { + } + + /** 待办内容替换原文缓存的 Redis key 前缀 */ + public static final String REDIS_KEY_PREFIX = "recording:todo:replace:"; +} diff --git a/src/test/resources/fixtures/recording-todo/RecordingTodoDomainServiceImpl.txt b/src/test/resources/fixtures/recording-todo/RecordingTodoDomainServiceImpl.txt new file mode 100644 index 0000000..a299835 --- /dev/null +++ b/src/test/resources/fixtures/recording-todo/RecordingTodoDomainServiceImpl.txt @@ -0,0 +1,22 @@ +package com.fantaibao.iot.infrastructure.service.recording; + +import com.fantaibao.iot.domain.constant.RecordingTodoConstants; +import org.springframework.data.redis.core.StringRedisTemplate; + +/** + * 精简自业务仓 RecordingTodoDomainServiceImpl#replaceText: + * 将待办原文 content/title(纯 String)写入 StringRedisTemplate,无 JSON 序列化。 + * 对应企微误报:类型 <未解析>、value 新增为 “{}”。 + */ +public class RecordingTodoDomainServiceImpl { + + private StringRedisTemplate stringRedisTemplate; + + public void replaceText(String tenantId, String todoId, String originalContent, String originalTitle) { + String contentRedisKey = RecordingTodoConstants.REDIS_KEY_PREFIX + todoId + ":" + tenantId; + stringRedisTemplate.opsForValue().set(contentRedisKey, originalContent); + + String titleRedisKey = RecordingTodoConstants.REDIS_KEY_PREFIX + "title:" + todoId + ":" + tenantId; + stringRedisTemplate.opsForValue().set(titleRedisKey, originalTitle); + } +}