fix(delivery,health): U14 deleteOf 结构化 JSON + U12 健康检查真实 ping(ACM2-10 复审核验项②③)

- U14(复审②):deleteOf 字符串模板在 partitionKey=null/非空时均产出非法 JSON,
  改 Jackson 结构化序列化(null→null 字面量,非空→带引号转义);
  DispatcherTickTest 新增 phase B 两样例(readTree 合法性 + refs 精确断言)。
  注:该分支仅 Phase B 且 ES 投递成功后可达,当前属潜伏缺陷。
- U12(复审③):Redis/Kafka 健康指示器由仅查 bean 存在升级为真实 ping
  (false/异常→DOWN;缺 bean 仍 DOWN)。FlightRedisClient/DeliveryPort 增 ping()
  (stub 返回 true;DeliveryPort 默认实现,真实 Kafka 实装时覆写为 metadata 校验)。
  ping 判定拆为可单测纯函数,HealthIndicatorsTest 覆盖 UP/DOWN/异常/缺 bean 四态。

验证:./gradlew test 37 个测试全绿(本地,未推送)。
This commit is contained in:
windyboy
2026-09-06 22:12:23 +08:00
parent 6de3fa6c9c
commit 6c887adf56
7 changed files with 121 additions and 18 deletions
@@ -56,7 +56,9 @@ class DispatcherTickTest {
attempts = attempts ?: events[i].attempts)
}
override fun insertSync(events: List<MsgEvent>) = Unit
override fun insertSync(events: List<MsgEvent>) { syncInserted += events }
val syncInserted = mutableListOf<MsgEvent>()
}
private class FakePort : DeliveryPort {
@@ -181,4 +183,24 @@ class DispatcherTickTest {
assertEquals(1, repo.events.first { it.eventId == 1L }.attempts) // schd 进退避
assertEquals(EventStatus.PENDING, repo.events.first { it.eventId == 1L }.state)
}
@Test
fun `delete events always carry legal JSON refs - null and non-null partitionKey`() {
val props = PipelineProps().apply { phase = PipelineProps.Phase.B }
val repo = FakeRepo()
val port = FakePort()
val d = dispatcher(repo, port, props)
repo.enqueue(ev(1, Targets.ES_FLIGHT_HTS, "F1", """{"h":1}"""))
repo.enqueue(ev(2, Targets.ES_FLIGHT_HTS, null, """{"h":2}"""))
d.tick(); d.tick() // 每 target 每 tick 仅出队一条:两条 ES 事件需两个 tick
val deletes = repo.syncInserted.filter { it.target == Targets.REDIS_FLIGHT_INFO }
assertEquals(2, deletes.size)
val mapper = com.fasterxml.jackson.databind.ObjectMapper()
val parsed = deletes.map { mapper.readTree(it.payloadJson) } // readTree 即合法性断言
assertEquals(listOf("delete", "delete"), parsed.map { it.get("op").asText() })
assertEquals("F1", parsed[0].get("refs").asText()) // 非空 → 带引号字符串
assertTrue(parsed[1].get("refs").isNull) // null → null 字面量
}
}
@@ -0,0 +1,57 @@
package com.gzzn.omms.msgexchange.nextgen.infra.health
import com.gzzn.omms.msgexchange.nextgen.delivery.DeliveryPort
import com.gzzn.omms.msgexchange.nextgen.infra.redis.FlightRedisClient
import com.gzzn.omms.msgexchange.nextgen.infra.redis.RedisScript
import io.micronaut.health.HealthStatus
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
/**
* U12:健康指示器 UP 判据为真实 ping(复审 P1 修正)——
* bean 缺失 / ping false / ping 抛异常 → DOWN;仅 ping 成功 → UP。
*/
class HealthIndicatorsTest {
private class FakeRedis(private val pingResult: Boolean) : FlightRedisClient {
override fun eval(script: RedisScript, setPairs: List<Pair<String, String>>, delFields: List<String>) = Unit
override fun hgetAllFlightInfo(): Map<String, String> = emptyMap()
override fun ping(): Boolean = pingResult
}
private class ThrowingRedis : FlightRedisClient {
override fun eval(script: RedisScript, setPairs: List<Pair<String, String>>, delFields: List<String>) = Unit
override fun hgetAllFlightInfo(): Map<String, String> = emptyMap()
override fun ping(): Boolean = throw RuntimeException("connection refused")
}
private class FakePort(private val pingResult: Boolean) : DeliveryPort {
override fun sendKafka(topic: String, payloadJson: String) = Unit
override fun indexFlightHts(payloadJson: String) = Unit
override fun projectRedis(payloadJson: String) = Unit
override fun ping(): Boolean = pingResult
}
private class ThrowingPort : DeliveryPort {
override fun sendKafka(topic: String, payloadJson: String) = Unit
override fun indexFlightHts(payloadJson: String) = Unit
override fun projectRedis(payloadJson: String) = Unit
override fun ping(): Boolean = throw RuntimeException("metadata fetch failed")
}
@Test
fun `redis up only when ping succeeds`() {
assertEquals(HealthStatus.UP, redisHealth(FakeRedis(true)).status)
assertEquals(HealthStatus.DOWN, redisHealth(FakeRedis(false)).status)
assertEquals(HealthStatus.DOWN, redisHealth(ThrowingRedis()).status)
assertEquals(HealthStatus.DOWN, redisHealth(null).status) // bean 缺失
}
@Test
fun `kafka up only when ping succeeds`() {
assertEquals(HealthStatus.UP, kafkaHealth(FakePort(true)).status)
assertEquals(HealthStatus.DOWN, kafkaHealth(FakePort(false)).status)
assertEquals(HealthStatus.DOWN, kafkaHealth(ThrowingPort()).status)
assertEquals(HealthStatus.DOWN, kafkaHealth(null).status) // bean 缺失
}
}
@@ -138,6 +138,7 @@ class MessageProcessorTest {
private object FakeRedis : FlightRedisClient {
override fun eval(script: RedisScript, setPairs: List<Pair<String, String>>, delFields: List<String>) = Unit
override fun hgetAllFlightInfo(): Map<String, String> = emptyMap()
override fun ping(): Boolean = true
}
private class FakeRefData : RefDataRepository {