feat(outbound): REQ_TRACK/COUTMSGS/schd-sync 与 RESP 守卫(ACM2-92/86)
出站请求状态机与编码;SCHD-RESP 须匹配未过期开放请求;EROR 结案匹配 SENT。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
package com.gzzn.omms.msgexchange.processing
|
||||
|
||||
import com.gzzn.omms.msgexchange.MutableClock
|
||||
import com.gzzn.omms.msgexchange.codec.JacksonXmlCodec
|
||||
import com.gzzn.omms.msgexchange.config.OperationDayProps
|
||||
import com.gzzn.omms.msgexchange.config.PipelineProps
|
||||
import com.gzzn.omms.msgexchange.domain.OutboundRequestKeys
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.CoutmsgOutboxRepository
|
||||
import com.gzzn.omms.msgexchange.infra.persistence.ReqTrackRepository
|
||||
import com.gzzn.omms.msgexchange.infra.stub.StubCoutmsgOutbox
|
||||
import com.gzzn.omms.msgexchange.infra.stub.StubReqTrack
|
||||
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.time.Duration
|
||||
import java.time.LocalDate
|
||||
|
||||
class OutboundRequestTest {
|
||||
|
||||
private val clock = MutableClock(MutableClock.BASE)
|
||||
private val opDay = OperationDayProps().apply { zone = "Asia/Shanghai"; cutoffHour = 0 }
|
||||
private val props = PipelineProps().apply { outbound.responseTimeout = Duration.ofMinutes(5) }
|
||||
|
||||
private fun service(req: StubReqTrack, outbox: StubCoutmsgOutbox) =
|
||||
OutboundRequestService(req, outbox, JacksonXmlCodec(), props, opDay, clock)
|
||||
|
||||
@Test
|
||||
fun `schd sync registers PENDING then confirms COUTMSGS as SENT`() {
|
||||
val req = StubReqTrack(clock)
|
||||
val outbox = StubCoutmsgOutbox()
|
||||
val svc = service(req, outbox)
|
||||
|
||||
val outcome = svc.registerRqfdSync()
|
||||
assertTrue(outcome is OutboundRequestService.RegisterOutcome.Registered)
|
||||
val open = req.findLatest(
|
||||
OutboundRequestKeys.RQFD_REQ_TYPE,
|
||||
LocalDate.of(2026, 9, 6),
|
||||
OutboundRequestKeys.SENDER,
|
||||
listOf(ReqTrackRepository.ReqState.SENT),
|
||||
)!!
|
||||
assertEquals(1L, open.coutmsgsId)
|
||||
assertTrue(outbox.messages.containsValue(outbox.messages.values.first()))
|
||||
assertTrue(outbox.messages.values.first().contains("<SNDR>OMMS</SNDR>"))
|
||||
assertTrue(outbox.messages.values.first().contains("<TYPE>RQFD</TYPE>"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `open slot rejects duplicate schd sync registration`() {
|
||||
val req = StubReqTrack(clock)
|
||||
val svc = service(req, StubCoutmsgOutbox())
|
||||
svc.registerRqfdSync()
|
||||
assertEquals(OutboundRequestService.RegisterOutcome.OpenExists, svc.registerRqfdSync())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `restart retries PENDING without coutmsgs after job dispatch`() {
|
||||
val req = StubReqTrack(clock)
|
||||
val outbox = StubCoutmsgOutbox()
|
||||
val day = LocalDate.of(2026, 9, 6)
|
||||
val reqId = req.insert(OutboundRequestKeys.RQFD_REQ_TYPE, day, OutboundRequestKeys.SENDER)
|
||||
assertEquals(ReqTrackRepository.ReqState.PENDING, req.rows[reqId]!!.state)
|
||||
|
||||
val svc = service(req, outbox)
|
||||
assertEquals(1, svc.dispatchPending())
|
||||
|
||||
assertEquals(ReqTrackRepository.ReqState.SENT, req.rows[reqId]!!.state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ambiguous write marks uncertain and does not blind resend`() {
|
||||
val req = StubReqTrack(clock)
|
||||
val outbox = StubCoutmsgOutbox().apply { nextInsertOutcome = CoutmsgOutboxRepository.InsertOutcome.AMBIGUOUS }
|
||||
val day = LocalDate.of(2026, 9, 6)
|
||||
val reqId = req.insert(OutboundRequestKeys.RQFD_REQ_TYPE, day, OutboundRequestKeys.SENDER)
|
||||
val svc = service(req, outbox)
|
||||
|
||||
assertFalse(svc.dispatchPending() > 0)
|
||||
assertTrue(req.rows[reqId]!!.writeUncertain)
|
||||
assertEquals(ReqTrackRepository.ReqState.PENDING, req.rows[reqId]!!.state)
|
||||
outbox.nextInsertOutcome = CoutmsgOutboxRepository.InsertOutcome.CONFIRMED
|
||||
assertEquals(0, svc.dispatchPending())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `timeout expires SENT request`() {
|
||||
val req = StubReqTrack(clock)
|
||||
val day = LocalDate.of(2026, 9, 6)
|
||||
val reqId = req.insert(OutboundRequestKeys.RQFD_REQ_TYPE, day, OutboundRequestKeys.SENDER)
|
||||
req.linkCoutmsgs(reqId, 10L, 99L)
|
||||
req.markSent(reqId, clock.instant())
|
||||
clock.advance(Duration.ofMinutes(10).toMillis())
|
||||
|
||||
val svc = service(req, StubCoutmsgOutbox())
|
||||
assertEquals(1, svc.expireTimedOut())
|
||||
assertEquals(ReqTrackRepository.ReqState.EXPIRED, req.rows[reqId]!!.state)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `EROR fails matching open outbound by seqn`() {
|
||||
val req = StubReqTrack(clock)
|
||||
val day = LocalDate.of(2026, 9, 6)
|
||||
val reqId = req.insert(OutboundRequestKeys.RQFD_REQ_TYPE, day, OutboundRequestKeys.SENDER)
|
||||
req.linkCoutmsgs(reqId, 1L, 42L)
|
||||
req.markSent(reqId, clock.instant())
|
||||
|
||||
val svc = service(req, StubCoutmsgOutbox())
|
||||
assertTrue(svc.failFromEror(42L, "RQFD", "NONE"))
|
||||
assertEquals(ReqTrackRepository.ReqState.FAILED, req.rows[reqId]!!.state)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user