feat(processing): 快照流程锁内复核收敛 + 异常显式清除语义 + 语义矩阵定稿 (ACM2-29 P2-5/M3)

- SnapshotFlow 收敛到 v2 §5 事务顺序:锁外仅解析校验;取得 PIPELINE_LOCK 后
  锁内复核快照身份(last_message_id 重放短路)→ 锁内读代与当前航班态 →
  计算 nextState → 写入 → CAS → 事件 → SUCCEEDED。锁内 CAS 失败属数据异常,
  一律回滚 FAILED(INFRA),删除「版本号相同即视为重放」的推断路径(评审 F7/F8 收口)
- M3/F4:异常对象(FDIV/FRET/FLAB)显式清除语义——null 字面量/空对象/空串
  翻译为 Clear 命令;FlightNextState.clearedKeys 携带本次清除键;增量路径按
  映射把前缀标量列置 NULL;库内清除后读视图无该键(线格式保持键缺失,显式
  清除表达留待阶段 2 按下游契约定)
- StubFlightSchd 增量分支改为 nextState 权威整体替换(引擎已合并当前态),
  天然承载 Clear;flattenScalarsOnly 去除冗余循环(L2)
- docs/flight-state-semantics.md:16 类结构语义矩阵定稿(P0-A 交付物落库)
- 新测试:引擎 0 标记/异常清除/混排 fail-fast 单测;JDBC 异常清除回环用例

验证:MSGX_PG_PORT=5433 真实 PG ./gradlew test --rerun-tasks
105 用例 0 失败 0 跳过
This commit is contained in:
windyboy
2026-09-08 15:44:48 +08:00
parent 48a27b26dd
commit 20aafb9ab0
8 changed files with 223 additions and 46 deletions
@@ -142,4 +142,40 @@ class FlightStateEngineTest {
val next = FlightStateEngine.apply(current, commands, "m2", bumpVersion = true)
assertEquals(emptyList<Map<String, String>>(), next.collections["GTDT"])
}
@Test
fun `exception null payload translates to explicit clear and apply records cleared key`() {
val current = FlightNextState(
"F1",
mapOf("FLNO" to "CA100", "FRET" to """{"REID":"R1","RSN":"diverted"}"""),
emptyMap(),
3L,
"m1",
)
val commands = FlightStateEngine.commandsFromFields("F1", mapOf("FRET" to "null"), snapshotReplace = false)
assertEquals(ScalarCommand.Clear, commands.scalars["FRET"])
val next = FlightStateEngine.apply(current, commands, "m2", bumpVersion = true)
assertFalse(next.scalars.containsKey("FRET"))
assertTrue(next.clearedKeys.contains("FRET"))
assertEquals("CA100", next.scalars["FLNO"])
// 非空载荷仍为 Set,且清空后的下一跳 Set 恢复键(clearedKeys 不跨消息携带)
val restored = FlightStateEngine.apply(
next,
FlightStateEngine.commandsFromFields("F1", mapOf("FRET" to """{"REID":"R2"}"""), snapshotReplace = false),
"m3",
bumpVersion = true,
)
assertEquals("""{"REID":"R2"}""", restored.scalars["FRET"])
assertFalse(restored.clearedKeys.contains("FRET"))
}
@Test
fun `exception clear accepts null literal empty object and empty string`() {
for (payload in listOf("null", "{}", "")) {
val commands = FlightStateEngine.commandsFromFields("F1", mapOf("FDIV" to payload), snapshotReplace = false)
assertEquals(ScalarCommand.Clear, commands.scalars["FDIV"], "payload=[$payload] must be Clear")
}
}
}
@@ -693,6 +693,51 @@ class FlightSchdJdbcPgTest {
assertEquals(Triple("Z1", null, 1), after)
}
@Test
fun `PG dialect - exception object explicit clear writes null columns and omits wire key`() {
val day = "2026-09-07"
val flid = "TEST_EXC_CLEAR"
val engine = com.gzzn.omms.msgexchange.domain.flight.FlightStateEngine
// 1. 快照写入返航异常对象 → 主表前缀标量列
repo.persistNextStates(
day,
listOf(
engine.apply(
null,
engine.commandsFromFields(flid, mapOf("FRET" to """{"REID":"CA108","RSN":"diverted"}"""), snapshotReplace = true),
"msg-exc-1", bumpVersion = true,
),
),
snapshotReplace = true,
)
val stored1 = ds.queryOne(
"SELECT fret_reid, fret_rsn FROM flight_schd WHERE flid = ?",
{ ps -> ps.setString(1, flid) },
) { rs -> listOf(rs.getString("fret_reid"), rs.getString("fret_rsn")) }
assertEquals(listOf("CA108", "diverted"), stored1)
// 2. 增量显式清除(FRET="null")→ 列置 NULL,读视图无 FRET 键(评审 F4 修复闭环)
repo.persistNextStates(
null,
listOf(
engine.apply(
repo.findNextStateByFlid(flid),
engine.commandsFromFields(flid, mapOf("FRET" to "null"), snapshotReplace = false),
"msg-exc-2", bumpVersion = true,
),
),
snapshotReplace = false,
)
val stored2 = ds.queryOne(
"SELECT fret_reid, fret_rsn FROM flight_schd WHERE flid = ?",
{ ps -> ps.setString(1, flid) },
) { rs -> listOf(rs.getString("fret_reid"), rs.getString("fret_rsn")) }
assertEquals(listOf(null, null), stored2)
assertNull(repo.findByFlid(flid)!!["FRET"])
assertNull(repo.findNextStateByFlid(flid)!!.scalars["FRET"])
}
private fun assertLegacyCollectionColumnsAllNull(flid: String) {
val columnList = com.gzzn.omms.msgexchange.support.LEGACY_COLLECTION_STORAGE_COLUMNS.joinToString(", ")
ds.queryOne(