test: GTDT e2e, v2 write-path guards, and PG test env support
Add JacksonXmlCodec/GtdtHandler/Pipeline tests, PgTestSupport for MSGX_PG_*, and update smoke/poller expectations for real decode semantics.
This commit is contained in:
@@ -0,0 +1,82 @@
|
||||
package com.gzzn.omms.msgexchange.processing
|
||||
|
||||
import com.gzzn.omms.msgexchange.MutableClock
|
||||
import com.gzzn.omms.msgexchange.codec.DecodeResult
|
||||
import com.gzzn.omms.msgexchange.codec.JacksonXmlCodec
|
||||
import com.gzzn.omms.msgexchange.config.PipelineProps
|
||||
import com.gzzn.omms.msgexchange.domain.ProcState
|
||||
import com.gzzn.omms.msgexchange.domain.ProcStatus
|
||||
import com.gzzn.omms.msgexchange.domain.flight.FlightStateEngine
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.CminmsgInboxRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.PipelineTransactionManager
|
||||
import com.gzzn.omms.msgexchange.infra.retry.FailureScheduler
|
||||
import com.gzzn.omms.msgexchange.infra.retry.ProcFailure
|
||||
import com.gzzn.omms.msgexchange.infra.stub.StubFlightSchd
|
||||
import com.gzzn.omms.msgexchange.infra.stub.StubMsgEvents
|
||||
import com.gzzn.omms.msgexchange.infra.stub.StubProcState
|
||||
import com.gzzn.omms.msgexchange.infra.stub.StubReqTrack
|
||||
import com.gzzn.omms.msgexchange.processing.handlers.GtdtHandler
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/** P2-0-1:GTDT 报文端到端 SUCCEEDED + 三门读回(真实 XmlCodec + GtdtHandler)。 */
|
||||
class GtdtPipelineTest {
|
||||
|
||||
@Test
|
||||
fun `GTDT XML end-to-end SUCCEEDED with three gates persisted`() {
|
||||
val raw = """
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<MSG>
|
||||
<META>
|
||||
<SNDR>AODB</SNDR><SEQN>9001</SEQN><DTTM>20260908120000</DTTM>
|
||||
<TYPE>FLOP</TYPE><STYP>GTDT</STYP>
|
||||
</META>
|
||||
<FLOP>
|
||||
<FLID>FL_GTDT_E2E</FLID>
|
||||
<GTDT GTNO="1"><GATE>A1</GATE><GTYP>D</GTYP></GTDT>
|
||||
<GTDT GTNO="3"><GATE>B2</GATE><GTYP>I</GTYP></GTDT>
|
||||
<GTDT GTNO="7"><GATE>C3</GATE><GTYP>D</GTYP></GTDT>
|
||||
</FLOP>
|
||||
</MSG>
|
||||
""".trimIndent()
|
||||
|
||||
val procState = StubProcState()
|
||||
val flightSchd = StubFlightSchd()
|
||||
val msgEvents = StubMsgEvents()
|
||||
val reqTrack = StubReqTrack()
|
||||
val txManager = object : PipelineTransactionManager {
|
||||
override fun <T> inTransaction(block: () -> T): T = block()
|
||||
}
|
||||
val inbox = object : CminmsgInboxRepository {
|
||||
override fun insertRaw(rawXml: String) = 1L
|
||||
override fun rawOf(cminmsgsId: Long) = raw
|
||||
override fun pollUnprocessed(afterId: Long, limit: Int) = emptyList<Long>()
|
||||
override fun backfillOnSuccess(cminmsgsId: Long, sndr: String, type: String, styp: String, seqn: Long) = Unit
|
||||
}
|
||||
val clock = MutableClock(MutableClock.BASE)
|
||||
val procFailure = ProcFailure(procState, FailureScheduler(PipelineProps(), clock))
|
||||
val codec = JacksonXmlCodec()
|
||||
val handler = GtdtHandler()
|
||||
val snapshot = SnapshotFlow(procState, flightSchd, msgEvents, reqTrack, procFailure, txManager, inbox)
|
||||
val processor = MessageProcessor(
|
||||
inbox, procState, msgEvents,
|
||||
CodecHolder(codec),
|
||||
HandlerHolder(HandlerRegistry(listOf(handler))),
|
||||
flightSchd, snapshot, procFailure, PipelineProps(), txManager,
|
||||
)
|
||||
|
||||
val headId = 42L
|
||||
procState.insert(headId, ProcStatus.PENDING)
|
||||
processor.processOne(ProcState(headId, ProcStatus.PENDING))
|
||||
|
||||
assertEquals(ProcStatus.SUCCEEDED, procState.snapshotOf(headId)!!.state)
|
||||
val next = flightSchd.findNextStateByFlid("FL_GTDT_E2E")!!
|
||||
assertEquals(3, next.collections["GTDT"]!!.size)
|
||||
assertEquals("3", next.collections["GTDT"]!![1]["GTNO"])
|
||||
assertEquals("C3", next.collections["GTDT"]!![2]["GATE"])
|
||||
assertEquals(1L, next.stateVersion)
|
||||
|
||||
val roundtrip = FlightStateEngine.fromFlightFields("FL_GTDT_E2E", flightSchd.findByFlid("FL_GTDT_E2E")!!)
|
||||
assertEquals(3, roundtrip.collections["GTDT"]!!.size)
|
||||
}
|
||||
}
|
||||
@@ -175,6 +175,22 @@ class MessageProcessorTest {
|
||||
return true
|
||||
}
|
||||
override fun deleteGenBefore(cutoffDay: String): Int = 0
|
||||
override fun findNextStateByFlid(flid: String): com.gzzn.omms.msgexchange.domain.flight.FlightNextState? =
|
||||
flights[flid]?.let { com.gzzn.omms.msgexchange.domain.flight.FlightStateEngine.fromFlightFields(flid, it) }
|
||||
override fun persistNextStates(
|
||||
day: String?,
|
||||
states: List<com.gzzn.omms.msgexchange.domain.flight.FlightNextState>,
|
||||
snapshotReplace: Boolean,
|
||||
now: Instant,
|
||||
) {
|
||||
states.forEach { state ->
|
||||
val scalarFields = linkedMapOf("FLID" to state.flid)
|
||||
state.scalars.forEach { (k, v) -> scalarFields[k] = v }
|
||||
flights[state.flid] = scalarFields + state.collections.mapValues { (_, items) ->
|
||||
com.fasterxml.jackson.databind.ObjectMapper().writeValueAsString(items)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class FakeTxManager : PipelineTransactionManager {
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.gzzn.omms.msgexchange.processing
|
||||
|
||||
import com.gzzn.omms.msgexchange.domain.DecodedMessage
|
||||
import com.gzzn.omms.msgexchange.domain.MetaFields
|
||||
import com.gzzn.omms.msgexchange.domain.MsgKind
|
||||
import com.gzzn.omms.msgexchange.domain.ProcState
|
||||
import com.gzzn.omms.msgexchange.domain.ProcStatus
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.CminmsgInboxRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.FlightSchdRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.MsgEventRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.PipelineTransactionManager
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.ProcStateRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.ReqTrackRepository
|
||||
import com.gzzn.omms.msgexchange.infra.retry.FailureScheduler
|
||||
import com.gzzn.omms.msgexchange.infra.retry.ProcFailure
|
||||
import com.gzzn.omms.msgexchange.config.PipelineProps
|
||||
import com.gzzn.omms.msgexchange.MutableClock
|
||||
import com.gzzn.omms.msgexchange.infra.stub.StubFlightSchd
|
||||
import com.gzzn.omms.msgexchange.infra.stub.StubMsgEvents
|
||||
import com.gzzn.omms.msgexchange.infra.stub.StubPipelineTransactionManager
|
||||
import com.gzzn.omms.msgexchange.infra.stub.StubProcState
|
||||
import com.gzzn.omms.msgexchange.infra.stub.StubReqTrack
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
/** v2 §2.6 / §5:提交后回填失败不得回滚 PROC_STATE。 */
|
||||
class SnapshotFlowBackfillTest {
|
||||
|
||||
private class FailingInbox : CminmsgInboxRepository {
|
||||
override fun insertRaw(rawXml: String): Long = 1L
|
||||
override fun rawOf(cminmsgsId: Long): String? = null
|
||||
override fun pollUnprocessed(afterId: Long, limit: Int): List<Long> = emptyList()
|
||||
override fun backfillOnSuccess(cminmsgsId: Long, sndr: String, type: String, styp: String, seqn: Long) {
|
||||
throw RuntimeException("mysql-down")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `backfill failure after commit leaves PROC_STATE SUCCEEDED`() {
|
||||
val flightSchd = StubFlightSchd()
|
||||
val procState = StubProcState()
|
||||
val msgEvents = StubMsgEvents()
|
||||
val reqTrack = StubReqTrack()
|
||||
val txManager = StubPipelineTransactionManager()
|
||||
val inbox = FailingInbox()
|
||||
val clock = MutableClock(MutableClock.BASE)
|
||||
val procFailure = ProcFailure(procState, FailureScheduler(PipelineProps(), clock))
|
||||
|
||||
val headId = 901L
|
||||
procState.insert(headId, ProcStatus.PENDING)
|
||||
val day = "2026-09-07"
|
||||
val meta = MetaFields("AODB", "SCHD", "DNLD", 9L, 20260907090000L)
|
||||
val decoded = DecodedMessage(meta, MsgKind.Schd(MsgKind.SchdSubtype.DNLD), "<SCHD/>")
|
||||
SnapshotFlow.StageResult.parser = {
|
||||
SnapshotFlow.StageResult.Ok(day, listOf("FL_X" to mapOf("FLID" to "FL_X")))
|
||||
}
|
||||
|
||||
val flow = SnapshotFlow(procState, flightSchd, msgEvents, reqTrack, procFailure, txManager, inbox)
|
||||
flow.publishSnapshot(ProcState(headId, ProcStatus.PENDING), decoded)
|
||||
|
||||
assertEquals(ProcStatus.SUCCEEDED, procState.snapshotOf(headId)!!.state)
|
||||
assertTrue(flightSchd.findByFlid("FL_X") != null)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `CAS replay requires matching lastMessageId not version alone`() {
|
||||
val base = StubFlightSchd()
|
||||
val procState = StubProcState()
|
||||
val msgEvents = StubMsgEvents()
|
||||
val reqTrack = StubReqTrack()
|
||||
val txManager = StubPipelineTransactionManager()
|
||||
val inbox = object : CminmsgInboxRepository {
|
||||
override fun insertRaw(rawXml: String) = 1L
|
||||
override fun rawOf(cminmsgsId: Long) = null
|
||||
override fun pollUnprocessed(afterId: Long, limit: Int): List<Long> = emptyList()
|
||||
override fun backfillOnSuccess(cminmsgsId: Long, sndr: String, type: String, styp: String, seqn: Long) = Unit
|
||||
}
|
||||
val clock = MutableClock(MutableClock.BASE)
|
||||
val procFailure = ProcFailure(procState, FailureScheduler(PipelineProps(), clock))
|
||||
|
||||
val headId = 902L
|
||||
val day = "2026-09-07"
|
||||
procState.insert(headId, ProcStatus.PENDING)
|
||||
|
||||
val conflictingGen = FlightSchdRepository.GenMeta(day, 2L, setOf("OTHER"), lastMessageId = "999")
|
||||
base.putGenIfVersion(day, 0L, FlightSchdRepository.GenMeta(day, 1L, setOf("OTHER"), lastMessageId = "999"))
|
||||
val mockFlightSchd = object : FlightSchdRepository by base {
|
||||
override fun putGenIfVersion(
|
||||
day: String,
|
||||
expected: Long,
|
||||
newGen: FlightSchdRepository.GenMeta,
|
||||
now: java.time.Instant,
|
||||
): Boolean {
|
||||
base.putGenIfVersion(day, 1L, conflictingGen)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
SnapshotFlow.StageResult.parser = {
|
||||
SnapshotFlow.StageResult.Ok(day, listOf("FL_Y" to mapOf("FLID" to "FL_Y")))
|
||||
}
|
||||
val meta = MetaFields("AODB", "SCHD", "DNLD", 10L, 20260907100000L)
|
||||
val decoded = DecodedMessage(meta, MsgKind.Schd(MsgKind.SchdSubtype.DNLD), "<SCHD/>")
|
||||
val flow = SnapshotFlow(procState, mockFlightSchd, msgEvents, reqTrack, procFailure, txManager, inbox)
|
||||
flow.publishSnapshot(ProcState(headId, ProcStatus.PENDING), decoded)
|
||||
|
||||
assertEquals(ProcStatus.FAILED, procState.snapshotOf(headId)!!.state)
|
||||
}
|
||||
}
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
package com.gzzn.omms.msgexchange.processing
|
||||
|
||||
import com.gzzn.omms.msgexchange.MutableClock
|
||||
import com.gzzn.omms.msgexchange.config.PipelineProps
|
||||
import com.gzzn.omms.msgexchange.domain.DecodedMessage
|
||||
import com.gzzn.omms.msgexchange.domain.Decision
|
||||
import com.gzzn.omms.msgexchange.domain.FlightChange
|
||||
import com.gzzn.omms.msgexchange.domain.MetaFields
|
||||
import com.gzzn.omms.msgexchange.domain.MsgKind
|
||||
import com.gzzn.omms.msgexchange.domain.ProcState
|
||||
import com.gzzn.omms.msgexchange.domain.ProcStatus
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.CminmsgInboxRepository
|
||||
import com.gzzn.omms.msgexchange.infra.retry.FailureScheduler
|
||||
import com.gzzn.omms.msgexchange.infra.retry.ProcFailure
|
||||
import com.gzzn.omms.msgexchange.infra.stub.StubMsgEvents
|
||||
import com.gzzn.omms.msgexchange.infra.stub.StubPipelineTransactionManager
|
||||
import com.gzzn.omms.msgexchange.infra.stub.StubProcState
|
||||
import com.gzzn.omms.msgexchange.infra.stub.StubReqTrack
|
||||
import com.gzzn.omms.msgexchange.codec.JacksonXmlCodec
|
||||
import com.gzzn.omms.msgexchange.processing.handlers.GtdtHandler
|
||||
import com.gzzn.omms.msgexchange.support.GuardedFlightSchd
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.nio.file.Paths
|
||||
import kotlin.io.path.readText
|
||||
|
||||
class V2PipelineWritePathContractTest {
|
||||
|
||||
@Test
|
||||
fun `SnapshotFlow and MessageProcessor sources must not call legacy upsert helpers`() {
|
||||
val processingDir = Paths.get("src/main/kotlin/com/gzzn/omms/msgexchange/processing")
|
||||
val snapshotSource = processingDir.resolve("SnapshotFlow.kt").readText()
|
||||
val pumpSource = processingDir.resolve("Pump.kt").readText()
|
||||
|
||||
assertFalse(snapshotSource.contains("upsertSnapshotBatch"))
|
||||
assertFalse(snapshotSource.contains("upsertIncremental"))
|
||||
assertTrue(snapshotSource.contains("persistNextStates"))
|
||||
|
||||
assertFalse(pumpSource.contains("upsertSnapshotBatch"))
|
||||
assertFalse(pumpSource.contains("upsertIncremental"))
|
||||
assertTrue(pumpSource.contains("persistNextStates"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `SnapshotFlow DNLD path calls persistNextStates not legacy upsert helpers`() {
|
||||
val flightSchd = GuardedFlightSchd()
|
||||
val procState = StubProcState()
|
||||
val msgEvents = StubMsgEvents()
|
||||
val reqTrack = StubReqTrack()
|
||||
val txManager = StubPipelineTransactionManager()
|
||||
val inbox = object : CminmsgInboxRepository {
|
||||
override fun insertRaw(rawXml: String) = 1L
|
||||
override fun rawOf(cminmsgsId: Long) = null
|
||||
override fun pollUnprocessed(afterId: Long, limit: Int): List<Long> = emptyList()
|
||||
override fun backfillOnSuccess(cminmsgsId: Long, sndr: String, type: String, styp: String, seqn: Long) = Unit
|
||||
}
|
||||
val procFailure = ProcFailure(procState, FailureScheduler(PipelineProps(), MutableClock(MutableClock.BASE)))
|
||||
|
||||
val headId = 1001L
|
||||
procState.insert(headId, ProcStatus.PENDING)
|
||||
val day = "2026-09-07"
|
||||
SnapshotFlow.StageResult.parser = {
|
||||
SnapshotFlow.StageResult.Ok(day, listOf("FL_DNLD" to mapOf("FLID" to "FL_DNLD", "FLNO" to "CA100", "GTDT" to """[{"GTNO":"1","GATE":"A1"}]""")))
|
||||
}
|
||||
val decoded = DecodedMessage(MetaFields("AODB", "SCHD", "DNLD", 11L, 20260907110000L), MsgKind.Schd(MsgKind.SchdSubtype.DNLD), "<SCHD/>")
|
||||
SnapshotFlow(procState, flightSchd, msgEvents, reqTrack, procFailure, txManager, inbox)
|
||||
.publishSnapshot(ProcState(headId, ProcStatus.PENDING), decoded)
|
||||
|
||||
assertEquals(ProcStatus.SUCCEEDED, procState.snapshotOf(headId)!!.state)
|
||||
assertEquals(1, flightSchd.persistNextStatesCalls)
|
||||
assertEquals(0, flightSchd.legacySnapshotCalls)
|
||||
assertEquals(0, flightSchd.legacyIncrementalCalls)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `MessageProcessor FLOP path calls persistNextStates not legacy upsert helpers`() {
|
||||
val flightSchd = GuardedFlightSchd()
|
||||
val procState = StubProcState()
|
||||
val msgEvents = StubMsgEvents()
|
||||
val reqTrack = StubReqTrack()
|
||||
val txManager = StubPipelineTransactionManager()
|
||||
val raws = mutableMapOf<Long, String>()
|
||||
val inbox = object : CminmsgInboxRepository {
|
||||
override fun insertRaw(rawXml: String) = 1L
|
||||
override fun rawOf(cminmsgsId: Long) = raws[cminmsgsId]
|
||||
override fun pollUnprocessed(afterId: Long, limit: Int): List<Long> = emptyList()
|
||||
override fun backfillOnSuccess(cminmsgsId: Long, sndr: String, type: String, styp: String, seqn: Long) = Unit
|
||||
}
|
||||
val procFailure = ProcFailure(procState, FailureScheduler(PipelineProps(), MutableClock(MutableClock.BASE)))
|
||||
val handler = GtdtHandler()
|
||||
val snapshot = SnapshotFlow(procState, flightSchd, msgEvents, reqTrack, procFailure, txManager, inbox)
|
||||
val processor = MessageProcessor(
|
||||
inbox, procState, msgEvents,
|
||||
CodecHolder(JacksonXmlCodec()),
|
||||
HandlerHolder(HandlerRegistry(listOf(handler))),
|
||||
flightSchd, snapshot, procFailure, PipelineProps(), txManager,
|
||||
)
|
||||
val headId = 1002L
|
||||
procState.insert(headId, ProcStatus.PENDING)
|
||||
raws[headId] = """
|
||||
<MSG>
|
||||
<META><SNDR>AODB</SNDR><SEQN>1</SEQN><DTTM>20260907120000</DTTM><TYPE>FLOP</TYPE><STYP>GTDT</STYP></META>
|
||||
<FLOP><FLID>FL_FLOP</FLID><GTDT GTNO="2"><GATE>B9</GATE></GTDT></FLOP>
|
||||
</MSG>
|
||||
""".trimIndent()
|
||||
processor.processOne(ProcState(headId, ProcStatus.PENDING))
|
||||
|
||||
assertEquals(ProcStatus.SUCCEEDED, procState.snapshotOf(headId)!!.state)
|
||||
assertEquals(1, flightSchd.persistNextStatesCalls)
|
||||
assertEquals(0, flightSchd.legacySnapshotCalls)
|
||||
assertEquals(0, flightSchd.legacyIncrementalCalls)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.gzzn.omms.msgexchange.processing.handlers
|
||||
|
||||
import com.gzzn.omms.msgexchange.codec.FlopPayload
|
||||
import com.gzzn.omms.msgexchange.domain.DecodedMessage
|
||||
import com.gzzn.omms.msgexchange.domain.MetaFields
|
||||
import com.gzzn.omms.msgexchange.domain.MsgKind
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class GtdtHandlerTest {
|
||||
|
||||
private val handler = GtdtHandler()
|
||||
|
||||
@Test
|
||||
fun `decide replaces GTDT collection for three gates`() {
|
||||
val msg = DecodedMessage(
|
||||
meta = MetaFields("AODB", "FLOP", "GTDT", 1L, 20260908120000L),
|
||||
kind = MsgKind.Flop("GTDT"),
|
||||
rawXml = "<MSG/>",
|
||||
body = FlopPayload(
|
||||
flid = "121112312",
|
||||
scalars = mapOf("FFID" to "CA-CA101-A-12DEC031345-D"),
|
||||
collections = mapOf(
|
||||
"GTDT" to listOf(
|
||||
mapOf("GTNO" to "1", "GATE" to "G28", "GTYP" to "D"),
|
||||
mapOf("GTNO" to "2", "GATE" to "G33", "GTYP" to "I"),
|
||||
mapOf("GTNO" to "3", "GATE" to "G23", "GTYP" to "D"),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val decision = handler.decide(emptyMap(), msg)
|
||||
assertEquals(1, decision.flightChanges.size)
|
||||
assertEquals("121112312", decision.flightChanges[0].flid)
|
||||
val gtdtJson = decision.flightChanges[0].fields["GTDT"]!!
|
||||
assertEquals(true, gtdtJson.contains("\"G28\""))
|
||||
assertEquals(1, decision.schdPush.size)
|
||||
assertEquals("121112312", decision.schdPush[0].flid)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user