diff --git a/backend-java/src/test/java/com/nanri/aiimage/metrics/TaskHeartbeatAgeMetricsTest.java b/backend-java/src/test/java/com/nanri/aiimage/metrics/TaskHeartbeatAgeMetricsTest.java new file mode 100644 index 00000000..1615b720 --- /dev/null +++ b/backend-java/src/test/java/com/nanri/aiimage/metrics/TaskHeartbeatAgeMetricsTest.java @@ -0,0 +1,79 @@ +package com.nanri.aiimage.metrics; + +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * task-184:task.heartbeat.age 心跳年龄 gauge 契约(spec 11 §1)。 + * 年龄 = 距最后心跳的毫秒数,由 TaskHeartbeatService/巡检写入;新鲜/陈旧可观测; + * 刚心跳≈0;时钟负保护;未设置不产生 meter。 + */ +class TaskHeartbeatAgeMetricsTest { + + private final SimpleMeterRegistry registry = new SimpleMeterRegistry(); + private final TaskObservabilityMetrics metrics = new TaskObservabilityMetrics("server-110", registry); + + private io.micrometer.core.instrument.Gauge gauge(String moduleType) { + return registry.get("aiimage.task.heartbeat.age").tag("moduleType", moduleType).gauge(); + } + + @Test + void ageComputedFreshAndStale() { + metrics.taskHeartbeatAge("similarasin", 200L); + assertEquals(200.0, gauge("similarasin").value(), "新鲜心跳年龄应小"); + metrics.taskHeartbeatAge("similarasin", 3_600_000L); + assertEquals(3_600_000.0, gauge("similarasin").value(), "陈旧心跳年龄应大"); + } + + @Test + void ageJustHeartbeatIsZero() { + metrics.taskHeartbeatAge("similarasin", 0L); + assertEquals(0.0, gauge("similarasin").value()); + } + + @Test + void ageClockNegativeGuard() { + metrics.taskHeartbeatAge("similarasin", -100L); + assertEquals(0.0, gauge("similarasin").value(), "时钟回拨导致的负年龄按 0 处理"); + } + + @Test + void ageUpdatesOnEachWrite() { + metrics.taskHeartbeatAge("similarasin", 1_000L); + metrics.taskHeartbeatAge("similarasin", 2_000L); + assertEquals(2_000.0, gauge("similarasin").value()); + } + + @Test + void ageSplitByModule() { + metrics.taskHeartbeatAge("similarasin", 500L); + metrics.taskHeartbeatAge("publish", 900L); + assertEquals(500.0, gauge("similarasin").value()); + assertEquals(900.0, gauge("publish").value()); + } + + @Test + void noHeartbeatYetNoGauge() { + assertNull(registry.find("aiimage.task.heartbeat.age") + .tag("moduleType", "withdraw").gauge(), + "从未写入心跳年龄时不应产生 gauge"); + } + + @Test + void ageNameFrozen() { + metrics.taskHeartbeatAge("similarasin", 10L); + assertTrue(registry.getMeters().stream() + .anyMatch(m -> m.getId().getName().equals("aiimage.task.heartbeat.age"))); + } + + @Test + void ageCarriesInstanceLabel() { + metrics.taskHeartbeatAge("similarasin", 10L); + assertEquals("server-110", + registry.get("aiimage.task.heartbeat.age").gauge().getId().getTag("instanceId")); + } +}