fix(flight): 历史清理判据对齐 US-14 AC2 五类已结束条件(ACM2-91)
- HistoryRules.isEnded 共享求值器:计划超 3 天/取消超 1h/离港/到港;JDBC 与 stub 同源不漂移 - 删除无依据的 idle/deleted/terminal 三个窗口(reference.md 四行冲突参数重写为 AC2 口径) - 备降条件依赖 FDIV(DDES/DDIR),落地(Q3、ACM2-100)前显式不命中,参数先行登记 - 修「当日」取系统时钟的缺陷,改由传入 now 推导(测试抓出) - implementation.md 12.5 与配置、yml 同批对齐
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
package com.gzzn.omms.msgexchange.domain.flight
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
|
||||
/**
|
||||
* `US-14` AC2 五类已结束条件的共享求值边界(`HistoryRules.isEnded`):
|
||||
* JDBC 候选查询与内存 stub 都走这一个实现,这里守住每一类的开/关边界。
|
||||
* 时间字段解析不出(缺席)一律不命中:宁漏删不误删。
|
||||
*/
|
||||
class HistoryRulesIsEndedTest {
|
||||
|
||||
private val zone: ZoneId = ZoneId.of("Asia/Shanghai")
|
||||
/** 2026-09-09T00:00Z = 上海本地 2026-09-09T08:00。 */
|
||||
private val now: Instant = Instant.parse("2026-09-09T00:00:00Z")
|
||||
private val rules = HistoryRules(plannedAgeDays = 3, cancelledHours = 1, divertedHours = 1, arrivedHours = 1)
|
||||
|
||||
private fun ended(
|
||||
plannedAt: Instant? = null,
|
||||
cancelledAt: Instant? = null,
|
||||
arrivalAt: Instant? = null,
|
||||
departureAt: Instant? = null,
|
||||
): Boolean = rules.isEnded(plannedAt, cancelledAt, arrivalAt, departureAt, zone, now)
|
||||
|
||||
@Test
|
||||
fun `condition 1 planned time older than three days`() {
|
||||
assertTrue(ended(plannedAt = now.minusSeconds(3 * 86400 + 1))) // 超过 3 天 → 已结束
|
||||
assertFalse(ended(plannedAt = now.minusSeconds(3 * 86400 - 1))) // 恰好不满 3 天 → 未结束
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `condition 2 cancelled more than one hour ago`() {
|
||||
assertTrue(ended(cancelledAt = now.minusSeconds(3600 + 1)))
|
||||
assertFalse(ended(cancelledAt = now.minusSeconds(3600 - 1)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `condition 4 departed yesterday-or-earlier plan and actual departure not after now`() {
|
||||
// SODT 2026-09-08T10:00 上海(前一天),NEAT = 当前时刻 → 已结束
|
||||
assertTrue(ended(plannedAt = Instant.parse("2026-09-08T02:00:00Z"), departureAt = now))
|
||||
// 实际起飞晚于当前(还没起飞)→ 未结束
|
||||
assertFalse(ended(plannedAt = Instant.parse("2026-09-08T02:00:00Z"), departureAt = now.plusSeconds(60)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `condition 4 and 5 require planned date before today`() {
|
||||
// SODT 2026-09-09T07:00 上海(当日),即使到港/离港时间都满足也不算结束
|
||||
assertFalse(ended(plannedAt = Instant.parse("2026-09-08T23:00:00Z"), departureAt = now.minusSeconds(3600)))
|
||||
assertFalse(ended(plannedAt = Instant.parse("2026-09-08T23:00:00Z"), arrivalAt = now.minusSeconds(7200)))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `condition 5 arrived more than one hour ago`() {
|
||||
val yesterday = Instant.parse("2026-09-08T02:00:00Z")
|
||||
assertTrue(ended(plannedAt = yesterday, arrivalAt = now.minusSeconds(7200))) // 2 小时前到港
|
||||
assertFalse(ended(plannedAt = yesterday, arrivalAt = now.minusSeconds(1800))) // 30 分钟前到港
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `absent time fields never qualify`() {
|
||||
assertFalse(ended())
|
||||
assertFalse(ended(plannedAt = now, cancelledAt = now, arrivalAt = now, departureAt = now))
|
||||
}
|
||||
}
|
||||
@@ -24,15 +24,16 @@ class HistorySweepJobTest {
|
||||
|
||||
private val now: Instant = Instant.parse("2026-09-09T00:00:00Z")
|
||||
|
||||
private fun seededFlight(flid: String, deleted: Boolean, idleDays: Long): StubFlightState {
|
||||
/** 播种一条已结束(US-14 AC2 条件 1:SODT 早于当前超过 3 天)的航班。 */
|
||||
private fun seededFlight(flid: String, deleted: Boolean): StubFlightState {
|
||||
val f = StubFlightState()
|
||||
f.persistFullState(
|
||||
com.gzzn.omms.msgexchange.domain.flight.FlightSnapshot(
|
||||
flid, java.time.LocalDate.of(2026, 9, 1), if (deleted) FlightState.DELETED else FlightState.ACTIVE,
|
||||
1, mapOf("FLNO" to "CA001"), emptyMap(),
|
||||
1, mapOf("FLNO" to "CA001", "SODT" to "01Sep260000"), emptyMap(),
|
||||
),
|
||||
msgId = 1,
|
||||
now = now.minusSeconds(idleDays * 86400),
|
||||
now = now.minusSeconds(30 * 86400),
|
||||
)
|
||||
return f
|
||||
}
|
||||
@@ -51,7 +52,7 @@ class HistorySweepJobTest {
|
||||
|
||||
@Test
|
||||
fun `history store not connected must delete zero rows`() {
|
||||
val f = seededFlight("F1", deleted = true, idleDays = 30)
|
||||
val f = seededFlight("F1", deleted = true)
|
||||
val events = StubMsgEvents()
|
||||
val job = HistorySweepJob(f, events, HistoryProps().apply { historyStoreEnabled = false }, OperationDayProps(), StubPipelineTx(), StubPipelineLock())
|
||||
|
||||
@@ -64,10 +65,11 @@ class HistorySweepJobTest {
|
||||
|
||||
@Test
|
||||
fun `archive success precedes physical purge and confirmed rows only`() {
|
||||
val f = seededFlight("F1", deleted = true, idleDays = 30)
|
||||
val f = seededFlight("F1", deleted = true)
|
||||
f.persistFullState(
|
||||
com.gzzn.omms.msgexchange.domain.flight.FlightSnapshot(
|
||||
"F2", java.time.LocalDate.of(2026, 9, 1), FlightState.DELETED, 1, mapOf("FLNO" to "CA002"), emptyMap(),
|
||||
"F2", java.time.LocalDate.of(2026, 9, 1), FlightState.DELETED, 1,
|
||||
mapOf("FLNO" to "CA002", "SODT" to "01Sep260000"), emptyMap(),
|
||||
),
|
||||
msgId = 2, now = now.minusSeconds(30 * 86400),
|
||||
)
|
||||
@@ -89,7 +91,7 @@ class HistorySweepJobTest {
|
||||
|
||||
@Test
|
||||
fun `never-fdel lifecycle purge emits msg deletion notice before deletion`() {
|
||||
val f = seededFlight("F3", deleted = false, idleDays = 30) // 还在用,但已经静默超过兜底期限,命中清理条件
|
||||
val f = seededFlight("F3", deleted = false) // 还在用,但计划时间早于当前超过 3 天(US-14 AC2 条件 1),命中清理条件
|
||||
val events = StubMsgEvents()
|
||||
val store = RecordingHistoryStore()
|
||||
val job = HistorySweepJob(
|
||||
@@ -107,7 +109,7 @@ class HistorySweepJobTest {
|
||||
|
||||
@Test
|
||||
fun `a flight updated after archival is not purged and emits no deletion notice`() {
|
||||
val f = seededFlight("F4", deleted = false, idleDays = 30)
|
||||
val f = seededFlight("F4", deleted = false)
|
||||
val events = StubMsgEvents()
|
||||
val store = object : HistorySweepJob.HistoryStore {
|
||||
override fun archive(candidates: List<HistoryCandidate>): Set<String> {
|
||||
@@ -131,7 +133,7 @@ class HistorySweepJobTest {
|
||||
|
||||
@Test
|
||||
fun `snap log purge runs independently of the history store switch`() {
|
||||
val f = seededFlight("F1", deleted = true, idleDays = 30)
|
||||
val f = seededFlight("F1", deleted = true)
|
||||
var cutoff: Instant? = null
|
||||
val purge = com.gzzn.omms.msgexchange.infra.persistence.SnapshotLogPurge { instant -> cutoff = instant; 7 }
|
||||
val job = HistorySweepJob(
|
||||
|
||||
@@ -41,9 +41,13 @@ class HistorySweepPurgePgTest {
|
||||
val events = JdbcMsgEventRepository(ds, clock)
|
||||
val flid = "PG-" + UUID.randomUUID().toString().take(8)
|
||||
flights.persistFullState(
|
||||
FlightSnapshot(flid, LocalDate.of(2026, 8, 1), FlightState.ACTIVE, 1, emptyMap(), emptyMap()),
|
||||
FlightSnapshot(
|
||||
flid, LocalDate.of(2026, 8, 1), FlightState.ACTIVE, 1,
|
||||
mapOf("SODT" to "01Aug260000"), // 计划时间早于当前超过 3 天 → US-14 AC2 条件 1 命中
|
||||
emptyMap(),
|
||||
),
|
||||
msgId = 1,
|
||||
now = t0.minusSeconds(30L * 86400), // 静默超过 idle-hours,命中生命周期清退
|
||||
now = t0.minusSeconds(30L * 86400),
|
||||
)
|
||||
|
||||
// 删除阶段注入失败:删除通知已登记,删除抛错 → 整个事务必须回滚。
|
||||
|
||||
Reference in New Issue
Block a user