feat(chat): 静默告知改为按事故复位,队列排空即清零

W43 的静默规则只在「补发真的成功一次」时把 queuedResendFailures 清零。若一段坏网络的
队列以回滚或被收编收场(一次都没成功发出去),这一格就永远挂着 ⇒ 之后连接恢复、再断
一次、用户又发消息时,那句解释再也说不出口。那不是减少噪音,是把解释永久关掉(主人
2026-10-06 裁决:哪个体验好用哪个)。

改成按事故复位:一个「事故」= 队列从空到非空、再回到空,结束就复位,不管它是怎么结束
的——发出去了、回滚了(内容退回输入栏)、还是被收编了(气泡换成权威消息)。清零点因此
有两个:补发成功一次(原样保留),以及队列排空(既有钩子 forgetQueuedNoticeIfDrained,
回滚那条路也走这里,它是队列清空的唯一两个入口之一)。

这样不会在长期断网时刷屏:阈值仍是 3;「一批从空到非空只说一次」仍由 queuedNoticeTold
管着;纯网络故障下消息会堆积在队列里 ⇒ 队列不清空 ⇒ 不算新事故、计数不复位 ⇒ 不会每条
消息都弹一次横幅。而事故一旦结束,下一次断线又能重新解释。

判据提为纯函数 queuedResendFailuresAfterDelivery(VM 与测试共用,跟
queuedSendNoticeAllowed 同一个写法),单测钉住:队列排空即复位(哪怕这一批一条都没成功
发出去)、队列还有货则计数保留、补发成功照旧清零、一个事故里失败 0/1/2 次仍允许告知而
到阈值静默、下一个事故重新告知。静默不动队列:那条既有用例(判定那一刻队列仍在、下一步
仍是发这条)一字未改。

只动告知判据与它的记账。队列与出队逻辑、sendRouteFor / 三档路由 / W38 分享落地 / W45
直接试 REST / W46 的 CyreneRoute 日志全部未碰。

验证:gradlew.bat clean assembleDebug 与 gradlew.bat testDebugUnitTest,
failure / error 均为 0。
This commit is contained in:
AskaEth committed 2026-10-05 20:25:00 +08:00
1 parent 798c6ee907
commit ce50bd2c24
3 files changed
+150 -30

No files matched your search

@@ -6,19 +6,23 @@ import org.junit.Assert.assertTrue
import org.junit.Test
/**
* 「同一条(或同一批)反复补发失败就**静默**」的判据([queuedSendNoticeAllowed])——
* 主人 2026-10-06 定案。
* 「同一条(或同一批)反复补发失败就**静默**」的判据([queuedSendNoticeAllowed]),以及静默计数
* 在什么时候复位([queuedResendFailuresAfterDelivery])——主人 2026-10-06 定案。
*
* ## 背景
* W40/W41 之后,未连接时普通发送会进补发队列([ChatUiState.queuedForConnection]),并给**一句**
* 柔和的告知([SEND_QUEUED_NOTICE]:还没连上、已排队、连上后自动发出);连接恢复后自动补发。
* 主人新加的规矩是**不要一直刷屏**:同一条(或同一批)反复重试都失败时,**停止再弹告知**,静静地等。
* 2026-10-06 主人又裁决了一件跟它配套的事:静默按**事故**复位——一个「事故」= 队列从空到非空、
* 再回到空;结束就复位,**不要求**它是以一次成功的补发结束的。
*
* ## 本测试钉住四件事(就是那条规则的四个边)
* ## 本测试钉住五件事(那条规则的五个边)
* 1. 第一次失败 ⇒ 告知;
* 2. 到阈值之前(1、2 次)⇒ 仍然允许告知;
* 3. 到阈值([QUEUED_RESEND_SILENCE_THRESHOLD] = 3)之后 ⇒ **静默**;
* 4. **静默之后连接恢复并补发成功 ⇒ 计数清零**,下一次离线又能重新告知(不能永久闭嘴)。
* 4. **补发成功 ⇒ 计数清零**(W43 的旧清零点,一字不改);
* 5. **队列排空 ⇒ 计数清零**,哪怕这一次(乃至这一整批)**一次都没成功发出过**
* —— 静默按事故复位,不会永久闭嘴。
*
* 另外钉住两条性质:阈值不许取 1 或 2(那等于根本没告知);静默**只是不说**——队列内容与补发的
* 下一步一个字节都不受影响(静默 ≠ 丢弃)。
@@ -58,12 +62,71 @@ class QueuedSendSilenceTest {
fun `a successful resend clears the count so the next offline tells again`() {
// 补发成功时 `ChatViewModel.dropQueuedSend(sent = true)` 把计数归零 ⇒ 又回到「第一次失败」。
assertFalse(queuedSendNoticeAllowed(toldThisRound = false, failedResends = QUEUED_RESEND_SILENCE_THRESHOLD))
val afterSuccess = 0
val afterSuccess = queuedResendFailuresAfterDelivery(
QUEUED_RESEND_SILENCE_THRESHOLD,
sent = true,
drained = false,
)
assertEquals("补发成功这一个清零点照旧:一批还没发完也清零", 0, afterSuccess)
assertTrue(queuedSendNoticeAllowed(toldThisRound = false, failedResends = afterSuccess))
// 再连着失败又要静默一次:静默不是一次性的,也不是永久闭嘴。
assertFalse(queuedSendNoticeAllowed(toldThisRound = false, failedResends = QUEUED_RESEND_SILENCE_THRESHOLD))
}
// —— 一之二、事故结束(队列排空)就复位(W47 主人 2026-10-06 裁决) ——
@Test
fun `a drained batch resets the count even when nothing ever got sent`() {
// W43 的旧口径只在「补发成功」时清零。主人改成按事故复位:一个「事故」= 队列从空到非空、
// 再回到空,**它怎么结束都算**——包括一段坏网络以回滚 / 被收编收场、一次都没发出去。
// 旧口径下这种事故会把静默永远挂着:之后连接恢复、再断一次,用户永远得不到解释。
val failures = QUEUED_RESEND_SILENCE_THRESHOLD
assertFalse(queuedSendNoticeAllowed(toldThisRound = false, failedResends = failures))
val afterDrain = queuedResendFailuresAfterDelivery(failures, sent = false, drained = true)
assertEquals("队列排空就复位,不管这一批有没有成功发出过一条", 0, afterDrain)
assertTrue(
"下一个事故要能重新解释,不能永久闭嘴",
queuedSendNoticeAllowed(toldThisRound = false, failedResends = afterDrain),
)
}
@Test
fun `the count is kept while the queue is still non empty`() {
// 事故还没结束(队列里还有货、这一次又没成功)⇒ 计数原样留着,**同一个事故内**照旧静默。
// 这正是长期断网不刷屏的原因:消息堆在队列里 ⇒ 队列不清空 ⇒ 不算新事故 ⇒ 不重新告知。
val failures = QUEUED_RESEND_SILENCE_THRESHOLD
val kept = queuedResendFailuresAfterDelivery(failures, sent = false, drained = false)
assertEquals(failures, kept)
assertFalse(queuedSendNoticeAllowed(toldThisRound = false, failedResends = kept))
}
@Test
fun `one incident tells once at most and the next one starts over`() {
// 走一遍完整的事故:进队列那一刻(失败 0 次)说一次,之后每次补发又没上路都 ++,
// 到阈值就静默;队列排空(事故结束)后,下一个事故的第一次失败又能说出来。
var failures = 0
assertTrue("事故里的第一句必须说", queuedSendNoticeAllowed(toldThisRound = false, failedResends = failures))
while (failures < QUEUED_RESEND_SILENCE_THRESHOLD) {
assertTrue(
"第 ${failures + 1} 次失败之前仍然允许告知",
queuedSendNoticeAllowed(toldThisRound = false, failedResends = failures),
)
failures++
}
assertFalse("到阈值就停", queuedSendNoticeAllowed(toldThisRound = false, failedResends = failures))
failures = queuedResendFailuresAfterDelivery(failures, sent = false, drained = true)
assertEquals(0, failures)
assertTrue(
"下一个事故重新解释",
queuedSendNoticeAllowed(toldThisRound = false, failedResends = failures),
)
}
// —— 二、阈值本身与既有的「一轮只说一次」 ——
@Test