feat(tools): FS7 v2 dual-read diff — detail tables vs legacy slots (P2-1)
Extract FlightSchdReadAssembler for detail-only, legacy-slot, and merged read paths. Extend FlightStoreDiffTool with diffDualRead strict field comparison. Add counterexample tests (G28/G33/G23, GTNO=3, CKDT dedupe, ABTM ABDG, dual DELY) per review-flight-state-2026-09-08.
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
package com.gzzn.omms.msgexchange.tools
|
||||
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.jdbc.FlightSchdReadAssembler
|
||||
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
|
||||
|
||||
/**
|
||||
* FS7 v2 双读对拍:明细表组装 vs legacy 槽位列组装。
|
||||
* 反例来自 review-flight-state-2026-09-08 §12.1 / SIS §3.34.3(G28/G33/G23)。
|
||||
*/
|
||||
class FlightStoreDualReadTest {
|
||||
|
||||
private val tool = FlightStoreDiffTool()
|
||||
private val flid = "TEST_DUAL_READ"
|
||||
|
||||
private fun detailView(vararg collections: Pair<String, String>) = mapOf(
|
||||
flid to linkedMapOf(
|
||||
"FLID" to flid,
|
||||
*collections,
|
||||
),
|
||||
)
|
||||
|
||||
private fun legacyView(vararg collections: Pair<String, String>) = mapOf(
|
||||
flid to linkedMapOf(
|
||||
"FLID" to flid,
|
||||
*collections,
|
||||
),
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `Dual read GREEN when detail and legacy slot views agree within slot capacity`() {
|
||||
val gtdt = """[{"GTNO":"1","GATE":"G28","PGOT":"07SEP260800"},{"GTNO":"2","GATE":"G33"}]"""
|
||||
val detail = detailView("GTDT" to gtdt)
|
||||
val legacy = legacyView("GTDT" to gtdt)
|
||||
|
||||
val report = tool.diffDualRead(detail, legacy)
|
||||
assertTrue(report.isGreen)
|
||||
assertEquals(1, report.matchedCount)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Dual read RED for SIS three-gate sample G28 G33 G23 - third gate must not be ignored`() {
|
||||
val detailGtdt = """[
|
||||
{"GTNO":"1","GATE":"G28"},
|
||||
{"GTNO":"2","GATE":"G33"},
|
||||
{"GTNO":"3","GATE":"G23"}
|
||||
]""".replace("\n", "").replace(" ", "")
|
||||
val legacyGtdt = """[{"GTNO":"1","GATE":"G28"},{"GTNO":"2","GATE":"G33"}]"""
|
||||
|
||||
val report = tool.diffDualRead(
|
||||
detailView("GTDT" to detailGtdt),
|
||||
legacyView("GTDT" to legacyGtdt),
|
||||
)
|
||||
assertFalse(report.isGreen)
|
||||
assertTrue(report.unexpectedDeviations.any { it.path == "GTDT" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Dual read RED for non-contiguous GTNO=3 - must not rewrite to GTNO=1`() {
|
||||
val detailGtdt = """[{"GTNO":"3","GATE":"G28"}]"""
|
||||
val legacyGtdt = """[{"GTNO":"1","GATE":"G28"}]"""
|
||||
|
||||
val report = tool.diffDualRead(
|
||||
detailView("GTDT" to detailGtdt),
|
||||
legacyView("GTDT" to legacyGtdt),
|
||||
)
|
||||
assertFalse(report.isGreen)
|
||||
assertTrue(report.unexpectedDeviations.any { it.path?.startsWith("GTDT") == true })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Dual read RED for duplicate CHKC different cabin classes - legacy dedupe loses row`() {
|
||||
val detailCkdt = """[
|
||||
{"CKNO":"1","CHKC":"01","CCLS":"F"},
|
||||
{"CKNO":"2","CHKC":"01","CCLS":"Y"}
|
||||
]""".replace("\n", "").replace(" ", "")
|
||||
val legacyCkdt = """[{"CKNO":"1","CHKC":"01","CCLS":"F"}]"""
|
||||
|
||||
val report = tool.diffDualRead(
|
||||
detailView("CKDT" to detailCkdt),
|
||||
legacyView("CKDT" to legacyCkdt),
|
||||
)
|
||||
assertFalse(report.isGreen)
|
||||
assertTrue(report.unexpectedDeviations.any { it.path == "CKDT" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Dual read RED for two bridge ops with ABDG - legacy milestone view drops bridge id`() {
|
||||
val detailAbtm = """[
|
||||
{"ASNO":"1","ABDG":"B12","ABOP":"A","AOTM":"07SEP260730"},
|
||||
{"ASNO":"2","ABDG":"B15","ABOP":"D","AOTM":"07SEP261200"}
|
||||
]""".replace("\n", "").replace(" ", "")
|
||||
val legacyAbtm = """[
|
||||
{"ASNO":"1","ABOP":"A","AOTM":"07SEP260730"},
|
||||
{"ASNO":"2","ABOP":"D","AOTM":"07SEP261200"}
|
||||
]""".replace("\n", "").replace(" ", "")
|
||||
|
||||
val report = tool.diffDualRead(
|
||||
detailView("ABTM" to detailAbtm),
|
||||
legacyView("ABTM" to legacyAbtm),
|
||||
)
|
||||
assertFalse(report.isGreen)
|
||||
assertTrue(report.unexpectedDeviations.any { it.path?.contains("ABDG") == true || it.path?.startsWith("ABTM") == true })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `Dual read RED for two DELY reasons - legacy keeps only first`() {
|
||||
val detailDely = """[
|
||||
{"DLNO":"1","CODE":"01","STRT":"07SEP260800","DURA":"30"},
|
||||
{"DLNO":"2","CODE":"02","STRT":"07SEP260900","DURA":"15"}
|
||||
]""".replace("\n", "").replace(" ", "")
|
||||
val legacyDely = """[{"CODE":"01","STRT":"07SEP260800","DURA":"30"}]"""
|
||||
|
||||
val report = tool.diffDualRead(
|
||||
detailView("DELY" to detailDely),
|
||||
legacyView("DELY" to legacyDely),
|
||||
)
|
||||
assertFalse(report.isGreen)
|
||||
assertTrue(report.unexpectedDeviations.any { it.path == "DELY" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `FlightSchdReadAssembler exposes lossy legacy slot view for three-gate SIS sample`() {
|
||||
val row = mapOf(
|
||||
"GATE1" to "G28",
|
||||
"GATE2" to "G33",
|
||||
)
|
||||
val detailCollections = mapOf(
|
||||
"GTDT" to """[
|
||||
{"GTNO":"1","GATE":"G28"},
|
||||
{"GTNO":"2","GATE":"G33"},
|
||||
{"GTNO":"3","GATE":"G23"}
|
||||
]""".replace("\n", "").replace(" ", ""),
|
||||
)
|
||||
|
||||
val detailView = FlightSchdReadAssembler.assembleDetailOnly(flid, row, detailCollections)
|
||||
val legacyView = FlightSchdReadAssembler.assembleLegacySlotsOnly(flid, row)
|
||||
|
||||
val report = tool.diffDualRead(mapOf(flid to detailView), mapOf(flid to legacyView))
|
||||
assertFalse(report.isGreen)
|
||||
assertTrue(legacyView["GTDT"]!!.contains("G23").not())
|
||||
assertTrue(detailView["GTDT"]!!.contains("G23"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `FlightSchdReadAssembler merged view follows detail table when present`() {
|
||||
val row = mapOf("GATE1" to "OLD")
|
||||
val detailCollections = mapOf(
|
||||
"GTDT" to """[{"GTNO":"1","GATE":"G28"}]""",
|
||||
)
|
||||
val merged = FlightSchdReadAssembler.assembleMerged(flid, row, detailCollections)
|
||||
assertEquals(detailCollections["GTDT"], merged["GTDT"])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user