Files
msgexchange-v2/src/test/kotlin/com/gzzn/omms/msgexchange/ingress/InboxServiceTest.kt
T
windyboyandCursor dc1c49af6e docs(acm2-75): 精简不变量与声明边界,去掉无依据条目
审改 specification 管道/航班域/投影与 CLM:作废与需求重复或依据不足的 INV/CLM,白话重写保留条款,并同步架构、实现与引用注释。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-16 17:26:49 +08:00

121 lines
5.2 KiB
Kotlin

package com.gzzn.omms.msgexchange.ingress
import com.gzzn.omms.msgexchange.MutableClock
import com.gzzn.omms.msgexchange.config.PipelineProps
import com.gzzn.omms.msgexchange.infra.persistence.CminmsgInboxRepository
import com.gzzn.omms.msgexchange.infra.persistence.MailboxMarkResult
import com.gzzn.omms.msgexchange.infra.persistence.MailboxRow
import com.gzzn.omms.msgexchange.infra.persistence.ProcStateRepository
import com.gzzn.omms.msgexchange.infra.stub.StubInbox
import com.gzzn.omms.msgexchange.infra.stub.StubInboxCursor
import com.gzzn.omms.msgexchange.infra.stub.StubPipelineTx
import com.gzzn.omms.msgexchange.infra.stub.StubProcState
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertNotNull
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Assertions.assertThrows
import org.junit.jupiter.api.Test
import java.time.Instant
/**
* 兼容入口的接收边界(`C-8`):`insertRaw` 返回 ID 即"已落信"。
* 此后的接收时间读取失败或 PG 入队失败都只留痕/计数,不改接收结果——否则客户端重试会在
* 信箱里多写一行;缺的 `PROC_STATE` 由收报轮询按 `US-01` 补建,且只建一条。
*/
class InboxServiceTest {
private val t0: Instant = Instant.parse("2026-09-06T02:00:00Z")
/** 可注入失败的信箱:覆盖"插入失败 / 接收时间读失败 / 接收时间为 NULL"三种情形。 */
private class FakeInbox(
private val rawFailure: Boolean = false,
private val receivedFailure: Boolean = false,
private val receivedNull: Boolean = false,
) : CminmsgInboxRepository {
override fun insertRaw(rawXml: String): Long =
if (rawFailure) throw IllegalStateException("mailbox-down") else 42L
override fun rawOf(msgId: Long): String? = null
override fun receivedAtOf(msgId: Long): Instant? = when {
receivedFailure -> throw IllegalStateException("read-time-down")
receivedNull -> null
else -> Instant.parse("2026-09-06T01:59:55Z")
}
override fun readRange(fromExclusive: Long, limit: Int): List<MailboxRow> = emptyList()
override fun maxId(): Long? = null
override fun minId(): Long? = null
override fun markProcessedIfUnmarked(msgId: Long, value: String): MailboxMarkResult = MailboxMarkResult.MISSING
}
/** PG 入队第一次失败,其余委托给内存仓储(模拟轮询随后补建)。 */
private class FlakyProcState(private val delegate: StubProcState) : ProcStateRepository by delegate {
private var failNextInsert = true
override fun insertIfAbsent(msgId: Long, receivedAt: Instant?, enqueuedAt: Instant?): Boolean {
if (failNextInsert) {
failNextInsert = false
throw IllegalStateException("pg-down")
}
return delegate.insertIfAbsent(msgId, receivedAt, enqueuedAt)
}
}
@Test
fun `receipt survives receive-time read failure and pg enqueue failure`() {
val proc = StubProcState()
val service = InboxService(FakeInbox(receivedFailure = true), FlakyProcState(proc), MutableClock(t0))
val receipt = service.accept("<MSG/>")
assertEquals(42L, receipt.msgId) // 已落信:ID 照常返回
assertEquals(t0, receipt.receivedAt) // 读失败 → 回退到注入时钟
assertEquals(1L, service.pgEnqueueFailures.get()) // 入队失败只计数,不冒泡
assertNull(proc.find(42L))
}
@Test
fun `missing receive time falls back to the injected clock`() {
val service = InboxService(FakeInbox(receivedNull = true), StubProcState(), MutableClock(t0))
val receipt = service.accept("<MSG/>")
assertEquals(t0, receipt.receivedAt)
assertEquals(0L, service.pgEnqueueFailures.get())
}
@Test
fun `a real mailbox insert failure still fails without a fake id`() {
val service = InboxService(FakeInbox(rawFailure = true), StubProcState(), MutableClock(t0))
assertThrows(IllegalStateException::class.java) { service.accept("<MSG/>") }
assertEquals(0L, service.pgEnqueueFailures.get())
}
@Test
fun `the poller backfills a compat receipt whose pg enqueue failed, exactly once`() {
val inbox = StubInbox().apply { clear() }
val proc = StubProcState().apply { clear() }
val cursor = StubInboxCursor().apply { clear() }
val clock = MutableClock(t0)
val service = InboxService(inbox, FlakyProcState(proc), clock)
val poller = InboxPoller(inbox, proc, cursor, StubPipelineTx(), PipelineProps(), clock)
val receipt = service.accept("<MSG/>") // 落信成功;PG 入队失败但不抛
assertEquals(1L, service.pgEnqueueFailures.get())
assertNull(proc.find(receipt.msgId))
assertEquals(1, poller.pollOnce(t0)) // 轮询补建
assertNotNull(proc.find(receipt.msgId))
assertEquals(1, proc.rows.size)
assertEquals(receipt.msgId, cursor.cursor.committedUpTo)
assertEquals(0, poller.pollOnce(t0)) // 已存在:不重复建行,也不重复计数
assertEquals(1, proc.rows.size)
}
}