feat(chat): W43 超限告知改入口中性措辞,补发反复失败就静默
① 超限告知(ShareRules.truncatedImagesNotice)改成**入口中性**措辞 原句以「分享的图片」开头,那是一句只对其中一条路成立的限定:W42 起这个函数同时被图片入口 用着(图片按钮已改多选),而图片入口里根本没有「分享」这件事。所以句子不再指认动作的来处, 只说两条路都成立的三件事:**有上限(4 张)、超出的没发、发出去的是前 N 张**。新句逐字: 「一次最多 4 张,超出的没发,只发送前 N 张」,N 就是真发出去的张数。 刻意不写「失败」「错误」:被上限截掉不是一次错误,是上限本身,那族措辞会让用户以为丢了东西。 两条路仍然共用同一句(只有一个出处),没有为此分裂成两份文案。 ② 补发「多次重试失败就静默」(主人 2026-10-06 定案) W40/W41 之后未连接时普通发送会进补发队列并给一句柔和告知,连接恢复后自动补发;主人新加的 规矩是**不要一直刷屏**:同一条(或同一批)反复重试都失败时,停止再弹告知,静静地等。 - 判据抽成纯函数 queuedSendNoticeAllowed(toldThisRound, failedResends):**静默闸优先**,其次是 既有的「一轮从空到空只说一次」(W40 那条一字未改)。它只回答该不该说,从不碰队列。 - 阈值 QUEUED_RESEND_SILENCE_THRESHOLD = 3。理由:不许取 1~2(那等于根本没告知);第 1 次失败 就是「刚进队列」那一刻,必须说;第 2、3 次仍允许开口(连接抖动后还是没连上,用户需要重新 知道);第 3 次仍未成功说明已在反复失败里,再说只是把同一件事刷第二遍,从此停下。 - 记账口径:本会话的一个**补发批次**(= 队列从空到非空的那一段),与告知本身的记账同口径, 不新造第二个概念;按单条记账会漏掉「每条只失败一两次、整批却一直没发出去」这种要静默的情形。 落在 ChatViewModel 的实例字段上:VM 是按会话建的,所以天然按会话,也不引入全局单例——别的 会话的反复失败不该把本会话的告知掐掉。 - 计什么:只计「补发没能上路、内容仍留在队列里」那种失败(它是唯一会反复发生的一种);已被 服务端裁决 / 已回滚的那两类是一次性了结(内容退回输入栏并给了明确原因),不计数。 - 清零时机:**补发真的成功一次**就归零(WS 帧交出去、或回落 REST 被服务端收下)⇒「静默之后 连接恢复并补发成功」之后,下一次离线**又能重新告知**,不会永久闭嘴。 - 静默不等于丢弃:这一个数只影响说不说,队列内容与补发的下一步一个字节都没动,连接一好照旧 按序补发(W38/W40/W41 那条硬要求照旧成立);气泡上「排队中 · 还没连上」的小标也一直挂着。 既有的两句队列告知(排队中那一句 / 离线那一句)一字未改。 测试:ShareNoticeTest 与 ImageSendPlanTest 的超限断言改成新事实(不删断言);新增 QueuedSendSilenceTest 钉住那条规则的四个边(第一次失败告知、阈值前仍告知、阈值后静默、成功后 清零)以及「阈值不许取 1~2」「静默不动队列」两条性质。 assembleDebug 成功;testDebugUnitTest 1150 测试 / 0 失败 / 0 错误(基线 1143,本批新增 7)。
This commit is contained in:
1 parent
f3ee2e0d7b
commit
5556573986
6 files changed
+222
-16
No files matched your search
@@ -97,9 +97,10 @@ class ImageSendPlanTest {
|
||||
assertEquals(ShareRules.MAX_IMAGES, plan.messages.size)
|
||||
assertEquals(images(4), plan.imagesToSend)
|
||||
assertEquals(1, plan.truncatedImages)
|
||||
// 「只发送前 N 张」里的 N 就是真发出去的那几张(告知那句文案拿它拼)。
|
||||
// 「只发送前 N 张」里的 N 就是真发出去的那几张(告知那句文案拿它拼)。W43 起这句措辞改
|
||||
// **入口中性**(与分享路径仍是同一句,只是不含「分享」这个只对一条路成立的限定)。
|
||||
assertEquals(
|
||||
"分享的图片超过 ${ShareRules.MAX_IMAGES} 张,只发送前 ${ShareRules.MAX_IMAGES} 张",
|
||||
"一次最多 ${ShareRules.MAX_IMAGES} 张,超出的没发,只发送前 ${ShareRules.MAX_IMAGES} 张",
|
||||
ShareRules.truncatedImagesNotice(plan.imagesToSend.size, plan.truncatedImages),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package top.yeij.cyrene.ui.chat
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* 「同一条(或同一批)反复补发失败就**静默**」的判据([queuedSendNoticeAllowed])——
|
||||
* 主人 2026-10-06 定案。
|
||||
*
|
||||
* ## 背景
|
||||
* W40/W41 之后,未连接时普通发送会进补发队列([ChatUiState.queuedForConnection]),并给**一句**
|
||||
* 柔和的告知([SEND_QUEUED_NOTICE]:还没连上、已排队、连上后自动发出);连接恢复后自动补发。
|
||||
* 主人新加的规矩是**不要一直刷屏**:同一条(或同一批)反复重试都失败时,**停止再弹告知**,静静地等。
|
||||
*
|
||||
* ## 本测试钉住四件事(就是那条规则的四个边)
|
||||
* 1. 第一次失败 ⇒ 告知;
|
||||
* 2. 到阈值之前(1、2 次)⇒ 仍然允许告知;
|
||||
* 3. 到阈值([QUEUED_RESEND_SILENCE_THRESHOLD] = 3)之后 ⇒ **静默**;
|
||||
* 4. **静默之后连接恢复并补发成功 ⇒ 计数清零**,下一次离线又能重新告知(不能永久闭嘴)。
|
||||
*
|
||||
* 另外钉住两条性质:阈值不许取 1 或 2(那等于根本没告知);静默**只是不说**——队列内容与补发的
|
||||
* 下一步一个字节都不受影响(静默 ≠ 丢弃)。
|
||||
*/
|
||||
class QueuedSendSilenceTest {
|
||||
|
||||
// —— 一、那条规则的四个边 ——
|
||||
|
||||
@Test
|
||||
fun `the first failure is always told`() {
|
||||
// 刚进队列那一刻(还没失败过):这一句必须说,它是「已经排上队了、不用重打一遍」的全部
|
||||
// 信息量所在。
|
||||
assertTrue(queuedSendNoticeAllowed(toldThisRound = false, failedResends = 0))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the retries before the threshold may still speak`() {
|
||||
(1 until QUEUED_RESEND_SILENCE_THRESHOLD).forEach { failures ->
|
||||
assertTrue(
|
||||
"第 $failures 次失败还在阈值以内,不该静默",
|
||||
queuedSendNoticeAllowed(toldThisRound = false, failedResends = failures),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `from the threshold on it goes quiet`() {
|
||||
(QUEUED_RESEND_SILENCE_THRESHOLD..QUEUED_RESEND_SILENCE_THRESHOLD + 3).forEach { failures ->
|
||||
assertFalse(
|
||||
"第 $failures 次失败之后不该再说",
|
||||
queuedSendNoticeAllowed(toldThisRound = false, failedResends = failures),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
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
|
||||
assertTrue(queuedSendNoticeAllowed(toldThisRound = false, failedResends = afterSuccess))
|
||||
// 再连着失败又要静默一次:静默不是一次性的,也不是永久闭嘴。
|
||||
assertFalse(queuedSendNoticeAllowed(toldThisRound = false, failedResends = QUEUED_RESEND_SILENCE_THRESHOLD))
|
||||
}
|
||||
|
||||
// —— 二、阈值本身与既有的「一轮只说一次」 ——
|
||||
|
||||
@Test
|
||||
fun `the threshold is not one or two - silencing there would be no notice at all`() {
|
||||
assertTrue(
|
||||
"阈值取 1 或 2 时,用户还没被告知就已经听不到了",
|
||||
QUEUED_RESEND_SILENCE_THRESHOLD >= 3,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the round dedup and the silence gate both have to allow it`() {
|
||||
// 静默闸**优先**:就算这一轮还没说过,到了阈值也一个字都不说。
|
||||
assertFalse(queuedSendNoticeAllowed(toldThisRound = true, failedResends = QUEUED_RESEND_SILENCE_THRESHOLD))
|
||||
// 没到阈值、但这一轮已经说过 ⇒ 照旧不说(W40 的「一轮从空到空只说一次」,一字不改)。
|
||||
assertFalse(queuedSendNoticeAllowed(toldThisRound = true, failedResends = 0))
|
||||
// 两个闸都放行才说。
|
||||
assertTrue(queuedSendNoticeAllowed(toldThisRound = false, failedResends = 0))
|
||||
}
|
||||
|
||||
// —— 三、静默不等于丢弃 ——
|
||||
|
||||
@Test
|
||||
fun `silence never touches the queue - what is queued stays queued`() {
|
||||
// 判据只回答「还说不说」:静默那一刻队列一动不动,补发的下一步照样是「发这条」。
|
||||
assertFalse(queuedSendNoticeAllowed(toldThisRound = false, failedResends = QUEUED_RESEND_SILENCE_THRESHOLD))
|
||||
|
||||
val state = ChatUiState(
|
||||
pending = listOf(PendingMessage(localId = "local-a", text = "还没连上时发的那条")),
|
||||
).withQueuedSend("local-a", parallel = false)
|
||||
|
||||
assertEquals(1, state.queuedForConnection.size)
|
||||
assertEquals("local-a", (state.nextQueuedSend() as QueuedSendStep.Send).localId)
|
||||
// 补发用的还是原来那个键(= 上行幂等键 `clientMsgId`):静默期里补发过的那些,服务端
|
||||
// 照旧按同一个键判重,不会落成第二条消息。
|
||||
assertEquals("local-a", state.queuedForConnection.single().localId)
|
||||
}
|
||||
}
|
||||
@@ -60,11 +60,23 @@ class ShareNoticeTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the existing wording for a truncated share is unchanged`() {
|
||||
assertEquals(
|
||||
"分享的图片超过 ${ShareRules.MAX_IMAGES} 张,只发送前 1 张",
|
||||
shareNoticeFor(SharedContent(images = listOf("file:///cache/share-1.png"), truncatedImages = 3)),
|
||||
)
|
||||
fun `a truncated share says the cap, the dropped ones, and what went out`() {
|
||||
// W43 起这句改**入口中性**(主人 2026-10-06 拍板):同一个函数也被图片入口用着,而图片
|
||||
// 入口里没有「分享」这件事 ⇒ 句子不再指认动作的来处,只说两条路都成立的三件事。
|
||||
val said = shareNoticeFor(
|
||||
SharedContent(images = listOf("file:///cache/share-1.png"), truncatedImages = 3),
|
||||
)!!
|
||||
|
||||
assertEquals("一次最多 ${ShareRules.MAX_IMAGES} 张,超出的没发,只发送前 1 张", said)
|
||||
// 三件事逐条钉住:有上限 / 超出的没发 / 发出去的是前 N 张。
|
||||
assertTrue(said.contains("最多 ${ShareRules.MAX_IMAGES} 张"))
|
||||
assertTrue(said.contains("超出的没发"))
|
||||
assertTrue(said.contains("只发送前 1 张"))
|
||||
// 被上限截掉**不是错误**,是上限本身 ⇒ 那一族措辞不许出现。
|
||||
assertFalse(said.contains("失败"))
|
||||
assertFalse(said.contains("错误"))
|
||||
// 入口中性:两条路里都不该再出现只对其中一条成立的「分享」这个限定。
|
||||
assertFalse(said.contains("分享"))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in new issue
Block a user