package com.nanri.aiimage.metrics; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; import org.junit.jupiter.api.Test; 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.assertTrue; /** * task-189:指标命名一致性契约(spec 11 §1 标准名)。 * 全部指标名 aiimage.* 前缀、小写点分、无拼写漂移;recorder 全量指标名快照与 spec 对齐。 */ class MetricNamingConsistencyTest { private final SimpleMeterRegistry registry = new SimpleMeterRegistry(); private final TaskObservabilityMetrics metrics = new TaskObservabilityMetrics("server-110", registry); private void createAllMetrics() { metrics.taskCreated("similarasin"); metrics.taskRunningDelta("similarasin", 1); metrics.taskTerminal("similarasin", "success"); metrics.taskTerminal("similarasin", "failed"); metrics.taskTerminal("similarasin", "cancelled"); metrics.taskDuration("similarasin", 1L); metrics.taskHeartbeatAge("similarasin", 1L); metrics.fileJobState("similarasin", "pending"); metrics.fileJobState("similarasin", "running"); metrics.fileJobState("similarasin", "success"); metrics.fileJobState("similarasin", "failed"); metrics.fileJobState("similarasin", "retry"); metrics.fileJobDuration("similarasin", 1L); metrics.upload("similarasin", 1L, 1L, true); metrics.upload("similarasin", 1L, 1L, false); metrics.disk("/tmp/x", 1L, 1L); } @Test void allNamesHaveAiimagePrefix() { createAllMetrics(); for (String name : names()) { assertTrue(name.startsWith("aiimage."), "指标应带 aiimage. 前缀: " + name); } } @Test void allNamesLowercaseDotSegments() { createAllMetrics(); for (String name : names()) { assertTrue(name.equals(name.toLowerCase()), "指标名应全小写: " + name); assertTrue(!name.contains(".."), "指标名不应有连续点: " + name); assertTrue(!name.endsWith("."), "指标名不应以点结尾: " + name); } } @Test void taskFamilyNamesFrozen() { createAllMetrics(); for (String n : List.of("aiimage.task.created", "aiimage.task.running", "aiimage.task.success", "aiimage.task.failed", "aiimage.task.cancelled", "aiimage.task.duration", "aiimage.task.heartbeat.age")) { assertTrue(names().contains(n), "缺少 spec 任务指标名: " + n); } } @Test void fileJobFamilyNamesFrozen() { createAllMetrics(); for (String n : List.of("aiimage.file-job.pending", "aiimage.file-job.running", "aiimage.file-job.success", "aiimage.file-job.failed", "aiimage.file-job.retry", "aiimage.file-job.duration")) { assertTrue(names().contains(n), "缺少 spec file-job 指标名: " + n); } } @Test void noTypoDriftSnapshot() { createAllMetrics(); Set names = names(); // 与 spec 11 §1 允许集完全一致:不允许出现拼写漂移产生的多余名字 Set allowed = Set.of( "aiimage.task.created", "aiimage.task.running", "aiimage.task.success", "aiimage.task.failed", "aiimage.task.cancelled", "aiimage.task.duration", "aiimage.task.heartbeat.age", "aiimage.file-job.pending", "aiimage.file-job.running", "aiimage.file-job.success", "aiimage.file-job.failed", "aiimage.file-job.retry", "aiimage.file-job.duration", "aiimage.upload.duration", "aiimage.upload.size", "aiimage.upload.result", "aiimage.upload.result.success", "aiimage.upload.result.failure", "aiimage.tmpdisk.capacity", "aiimage.tmpdisk.used"); Set unexpected = names.stream().filter(n -> !allowed.contains(n)).collect(Collectors.toSet()); assertEquals(Set.of(), unexpected, "不应出现 spec 之外的漂移指标名"); } @Test void snapshotAllMetricsCaptured() { createAllMetrics(); assertTrue(names().size() >= 19, "全量指标快照应覆盖任务/文件/上传/磁盘族,实际 " + names().size()); } @Test void caseConsistentAcrossFamilies() { createAllMetrics(); long upperCount = names().stream().filter(n -> n.chars().anyMatch(Character::isUpperCase)).count(); assertEquals(0, upperCount, "指标名应统一小写"); } @Test void metricNamingStableAcrossRepeatedCreation() { createAllMetrics(); Set first = names(); TaskObservabilityMetrics metrics2 = new TaskObservabilityMetrics("server-121", registry); metrics2.taskCreated("similarasin"); Set second = names(); assertEquals(first, second, "新增实例不应改变既有指标名集合"); } private Set names() { return registry.getMeters().stream() .map(m -> m.getId().getName()).collect(Collectors.toSet()); } }