feat(codec): 观测未落库的 SRVT/VIPF 集合
SRVT/VIPF 只保留在 wire/domain 并计数告警,不落明细表、不参与合并: 出现事实不再被静默丢弃,为 Q13 定案提供真实流量证据([G-SRVT-VIPF])。 - wire DTO:SRVT/SERVICEDATA、VIPF/VIPDATA 与嵌套 VIPT(OPER 为元素属性) - 出现即留键:缺席与"出现但为空"不再等价;已落库 10 类集合语义不变 - 新增 msgx.pipeline.codec.srvt_seen.total / vipf_seen.total(reference.md 已登记) - 一并提交此前的 MAFL 文档改动(INV-21/INV-22、flight-state §2.3、G-MAFL 措辞)
This commit is contained in:
@@ -2,6 +2,7 @@ package com.gzzn.omms.msgexchange.codec
|
||||
|
||||
import com.gzzn.omms.msgexchange.domain.MsgKind
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertInstanceOf
|
||||
import org.junit.jupiter.api.Assertions.assertNull
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
@@ -87,6 +88,79 @@ class JacksonXmlCodecTest {
|
||||
assertEquals(listOf(mapOf("GTNO" to "0")), body.collections["GTDT"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SRVT and VIPF sections keep order attributes and nested VIPT`() {
|
||||
val raw = """
|
||||
<MSG>
|
||||
<META><SNDR>AODB</SNDR><SEQN>7</SEQN><DTTM>20021010090311</DTTM><TYPE>SCHD</TYPE><STYP>DNLD</STYP></META>
|
||||
<SCHD>
|
||||
<RECS>1</RECS>
|
||||
<FLTR>
|
||||
<FLID>121112312</FLID>
|
||||
<ALCD>CA</ALCD><FLNO>CA101</FLNO><SODT>15DEC261723</SODT>
|
||||
<SRVT OPER="ADD"><SRTC>SR01</SRTC><SRQT>2</SRQT><SRPR>9001</SRPR></SRVT>
|
||||
<SRVT OPER="DEL"><SRTC>SR02</SRTC></SRVT>
|
||||
<VIPF OPER="ADD"><VPCD>V001</VPCD><VFES>3</VFES>
|
||||
<VIPT OPER="UPD"><VSCD>VS01</VSCD><VTQY>4</VTQY><VTET>15DEC262000</VTET></VIPT>
|
||||
</VIPF>
|
||||
</FLTR>
|
||||
</SCHD>
|
||||
</MSG>
|
||||
""".trimIndent()
|
||||
|
||||
val result = codec.decode(raw) as DecodeResult.Ok
|
||||
val record = (result.message.body as ScheduleBody).records.single()
|
||||
|
||||
assertEquals(2, record.collections["SRVT"]!!.size)
|
||||
assertEquals("ADD", record.collections["SRVT"]!![0]["OPER"]) // OPER 是元素属性
|
||||
assertEquals("SR01", record.collections["SRVT"]!![0]["SRTC"])
|
||||
assertEquals("2", record.collections["SRVT"]!![0]["SRQT"])
|
||||
assertEquals("9001", record.collections["SRVT"]!![0]["SRPR"])
|
||||
assertEquals("DEL", record.collections["SRVT"]!![1]["OPER"]) // 保留报文先后顺序
|
||||
assertEquals("SR02", record.collections["SRVT"]!![1]["SRTC"])
|
||||
|
||||
val vip = record.collections["VIPF"]!!.single()
|
||||
assertEquals("V001", vip["VPCD"])
|
||||
assertEquals("3", vip["VFES"])
|
||||
assertEquals("UPD", vip["VIPT_OPER"])
|
||||
assertEquals("VS01", vip["VIPT_VSCD"])
|
||||
assertEquals("4", vip["VIPT_VTQY"])
|
||||
assertEquals("15DEC262000", vip["VIPT_VTET"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `absent and empty SRVT VIPF stay distinguishable while mapped collections keep dropping empties`() {
|
||||
val withEmpty = """
|
||||
<MSG>
|
||||
<META><SNDR>AODB</SNDR><SEQN>8</SEQN><DTTM>20021010090311</DTTM><TYPE>SCHD</TYPE><STYP>DNLD</STYP></META>
|
||||
<SCHD><RECS>1</RECS><FLTR><FLID>121</FLID><SRVT/><VIPF></VIPF><CLDT/></FLTR></SCHD>
|
||||
</MSG>
|
||||
""".trimIndent()
|
||||
val present = ((codec.decode(withEmpty) as DecodeResult.Ok).message.body as ScheduleBody)
|
||||
.records.single().collections
|
||||
|
||||
// 出现但为空段保留键,不再与"未出现"混为一谈 [G-SRVT-VIPF]
|
||||
assertTrue(present.containsKey("SRVT"))
|
||||
assertTrue(present.containsKey("VIPF"))
|
||||
assertTrue(present.getValue("SRVT").all { it.isEmpty() })
|
||||
assertTrue(present.getValue("VIPF").all { it.isEmpty() })
|
||||
// 已落库的 10 类集合行为未变:空元素 → 一行空行;缺席 → 没有这个键
|
||||
assertEquals(listOf(emptyMap<String, String>()), present["CLDT"])
|
||||
|
||||
val absent = """
|
||||
<MSG>
|
||||
<META><SNDR>AODB</SNDR><SEQN>9</SEQN><DTTM>20021010090311</DTTM><TYPE>SCHD</TYPE><STYP>DNLD</STYP></META>
|
||||
<SCHD><RECS>1</RECS><FLTR><FLID>121</FLID></FLTR></SCHD>
|
||||
</MSG>
|
||||
""".trimIndent()
|
||||
val absentKeys = ((codec.decode(absent) as DecodeResult.Ok).message.body as ScheduleBody)
|
||||
.records.single().collections
|
||||
|
||||
assertFalse(absentKeys.containsKey("SRVT"))
|
||||
assertFalse(absentKeys.containsKey("VIPF"))
|
||||
assertFalse(absentKeys.containsKey("CLDT"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `malformed xml returns MALFORMED not CODEC_ERROR`() {
|
||||
val err = codec.decode("not-xml") as DecodeResult.Err
|
||||
|
||||
@@ -111,6 +111,30 @@ class FlightStateEngineTest {
|
||||
assertEquals(2, next.stateVersion)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unpersisted collections are carried but never merged until their detail tables exist`() {
|
||||
val current = FlightSnapshot(
|
||||
"121", day, FlightState.ACTIVE, 1,
|
||||
scalars = mapOf("FLNO" to "CA001"),
|
||||
collections = mapOf("GTDT" to listOf(mapOf("GATE" to "G1"))),
|
||||
)
|
||||
val next = FlightStateEngine.snapshotState(
|
||||
current,
|
||||
ScheduleRecord(
|
||||
"121",
|
||||
scalars = mapOf("FLNO" to "CA002"),
|
||||
collections = mapOf("SRVT" to listOf(mapOf("SRTC" to "SR01")), "VIPF" to emptyList()),
|
||||
),
|
||||
operationDay = day,
|
||||
keepDeleted = false,
|
||||
)
|
||||
|
||||
// [G-SRVT-VIPF]:wire/domain 保留这两个集合,但合并层既不落库,也不因"出现"清空任何明细
|
||||
assertFalse(next.collections.containsKey("SRVT"))
|
||||
assertFalse(next.collections.containsKey("VIPF"))
|
||||
assertEquals(listOf(mapOf("GATE" to "G1")), next.collections["GTDT"])
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `validation accepts valid snapshot and rejects RECS mismatch`() {
|
||||
val ok = FlightStateEngine.validateMessage(
|
||||
|
||||
@@ -62,6 +62,18 @@ class PipelineMetricsTest {
|
||||
org.junit.jupiter.api.Assertions.assertTrue(gauge("msgx.pipeline.job.heartbeat_age_seconds") >= 0.0)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `unpersisted SRVT VIPF counters are registered and reflect decoded hits`() {
|
||||
val counters = ctx.getBean(PipelineCounters::class.java)
|
||||
val srvtBefore = gauge("msgx.pipeline.codec.srvt_seen.total")
|
||||
val vipfBefore = gauge("msgx.pipeline.codec.vipf_seen.total")
|
||||
|
||||
counters.unpersistedCollectionSeenAdd(mapOf("SRVT" to 2, "VIPF" to 1))
|
||||
|
||||
assertEquals(srvtBefore + 2.0, gauge("msgx.pipeline.codec.srvt_seen.total"), 0.001)
|
||||
assertEquals(vipfBefore + 1.0, gauge("msgx.pipeline.codec.vipf_seen.total"), 0.001)
|
||||
}
|
||||
|
||||
private fun gauge(name: String): Double =
|
||||
requireNotNull(registry.find(name).gauge()) { "gauge not registered: $name" }.value()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user