merge: W33 —— parallel_limit 提示加「本端确在并行档」的闸 + 查证并发额度横幅(结论:代码无缺,是验证条件问题)
This commit is contained in:
commit
1f6c16c590
2 files changed
+66
-14
No files matched your search
@@ -157,6 +157,9 @@ const val PARALLEL_LIMIT_REASON = "parallel_limit"
|
||||
* 措辞刻意不吓人:这条消息服务端**收下了**、也照常会回,只是没能另起一路。
|
||||
* 三件事都要说清——(1) 没排上并行、(2) 这条并没丢(落回普通排队)、(3) 什么时候轮到它
|
||||
* (前面那句做完就轮到)。少了第 (2)(3) 句,用户会把「降级」读成「失败」。
|
||||
*
|
||||
* 本端**不在「并行」档**时这一句根本不出现(判据见 [parallelLimitNoticeFor])——那不是静默,
|
||||
* 理由写在那边的判据表里。
|
||||
*/
|
||||
const val PARALLEL_LIMIT_NOTICE = "她正忙,这条没能另起一路,已按普通排队排上;她做完手上这句就轮到它"
|
||||
|
||||
@@ -164,17 +167,30 @@ const val PARALLEL_LIMIT_NOTICE = "她正忙,这条没能另起一路,已按
|
||||
* `pending_ack.reason` ⇒ 该对用户说的那句降级告知(**纯函数**,`null` = 没什么要说的)。
|
||||
*
|
||||
* ## 判据(穷举)
|
||||
* - [PARALLEL_LIMIT_REASON] ⇒ [PARALLEL_LIMIT_NOTICE]:声明了并行但服务端额度已满,落回排队。
|
||||
* - [PARALLEL_LIMIT_REASON] **且本端当前就在** [InterruptMode.Parallel] 档 ⇒ [PARALLEL_LIMIT_NOTICE]:
|
||||
* 本端声明了并行、服务端额度已满,这一条落回排队。
|
||||
* - [PARALLEL_LIMIT_REASON] 但本端**不在**并行档 ⇒ `null`。`pending_ack` 是**广播帧**(每条已
|
||||
* 认证连接都收得到),别的端在同一会话声明并行又落回排队时本端**也会**收到这一帧——可那件事
|
||||
* 与本端无关:用户没选并行时,他这条消息**本来就是**普通排队、他预期的就是排队 ⇒
|
||||
* 落回排队**不是意外** ⇒ 不说**并不构成静默**。反过来,在那个时刻说一句「这条没能另起一路」
|
||||
* 会让用户以为是自己刚发的那条出了岔子;**误报会教用户不信任这条提示**,代价比少说一句大。
|
||||
* - 其它任何字符串 ⇒ `null`:**开放集合**(服务端明写「已知取值 `"parallel_limit"`」,将来可能
|
||||
* 再加),本端**不为不认识的 reason 编文案**——说不出的话就不说。这不是静默:
|
||||
* `reason` 本身在解析层**原样保留**(`WsEvent.PendingAck.reason`,未知值不丢也不改写),
|
||||
* 而「这条在排队」这件事照旧由既有的排队态与队列深度如实呈现。
|
||||
* - `null` / 空白 ⇒ `null`:「整键缺席」= 服务端没给理由(绝大部分情况),本来就没话说。
|
||||
*
|
||||
* 档位由调用方**现读**了喂进来(`ChatViewModel.currentInterruptMode` 那条纪律:偏好是用户在设置页
|
||||
* 当场切的东西,缓存住就留一个静默不生效的窗口);本函数只做判断,不读偏好、不碰协程。
|
||||
*
|
||||
* 不做 trim 之外的归一:reason 是**机器可读码**,大小写与拼写都以服务端为准,本端不猜同义词。
|
||||
*/
|
||||
fun parallelLimitNoticeFor(reason: String?): String? =
|
||||
if (reason?.trim() == PARALLEL_LIMIT_REASON) PARALLEL_LIMIT_NOTICE else null
|
||||
fun parallelLimitNoticeFor(reason: String?, mode: InterruptMode): String? =
|
||||
if (mode == InterruptMode.Parallel && reason?.trim() == PARALLEL_LIMIT_REASON) {
|
||||
PARALLEL_LIMIT_NOTICE
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
/**
|
||||
* 一帧「队列满」该对本地乐观气泡做什么(纯函数的结果,见 [queueFullOutcome])。
|
||||
@@ -1120,6 +1136,9 @@ class ChatViewModel(
|
||||
*
|
||||
* 它描述的是**刚刚发生的那一次发送**([WsEvent.PendingAck] 到达的那一刻),所以到点自己收走;
|
||||
* 它**不是错误**,不给关闭按钮、也不进 `error` 通道。
|
||||
*
|
||||
* 要不要说的判据不在这里,在 [parallelLimitNoticeFor]:本端不在「并行」档时那一句根本不出现
|
||||
* (这一帧是广播,落在别的端上的并行降级与本端无关)。
|
||||
*/
|
||||
private fun showParallelLimitNotice(text: String) {
|
||||
_state.update { it.copy(parallelLimitNotice = text) }
|
||||
@@ -1852,7 +1871,16 @@ class ChatViewModel(
|
||||
// 让它落回排队,界面上与普通排队**毫无区别**。用户选了「并行」却看不出区别就是
|
||||
// 静默,所以这里如实说一句。判据与文案都是纯函数 [parallelLimitNoticeFor]
|
||||
// (未知 reason = `null`,不编文案);这条**不是错误**,配色用告知档。
|
||||
parallelLimitNoticeFor(event.reason)?.let(::showParallelLimitNotice)
|
||||
//
|
||||
// **闸**:这一帧是**广播**,别的端在同一会话声明并行又落回排队时本端也收得到
|
||||
// ⇒ 只有本端当前确实在「并行」档才说(判据表与理由见 [parallelLimitNoticeFor])。
|
||||
// 档位必须**现读**(同 [currentInterruptMode] 那条纪律),而 `onEvent` 不是
|
||||
// suspend 函数 ⇒ 这一句得起一个协程(与 [ensureMessageKnown] 同一形态):它只读
|
||||
// 一次偏好,而 `pending_ack` 是低频的状态帧,不值一个常驻订阅。
|
||||
viewModelScope.launch {
|
||||
parallelLimitNoticeFor(event.reason, currentInterruptMode())
|
||||
?.let(::showParallelLimitNotice)
|
||||
}
|
||||
}
|
||||
|
||||
is WsEvent.PendingDrain -> if (event.sessionId == sessionId) {
|
||||
|
||||
@@ -5,14 +5,16 @@ import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import top.yeij.cyrene.core.prefs.InterruptMode
|
||||
|
||||
/**
|
||||
* 「并行没排上」的如实告知与顶部横幅的**两档配色**(契约 v0.65 §2.6)。
|
||||
*
|
||||
* 两件事各测各的:
|
||||
*
|
||||
* 1. [parallelLimitNoticeFor] 的判据(`pending_ack.reason` ⇒ 文案或 `null`):已知值一句话、
|
||||
* 未知值**不编**、缺席与空白没话说。开关 ON ≠ 一定起第二路,这一句就是那个差别的全部告知。
|
||||
* 1. [parallelLimitNoticeFor] 的判据(`pending_ack.reason` + **本端档位** ⇒ 文案或 `null`):
|
||||
* 已知值一句话、未知值**不编**、缺席与空白没话说,而**非并行档一律不说**——`pending_ack` 是
|
||||
* 广播帧,别的端的并行降级落在本端就是一条误报。
|
||||
* 2. [ChatUiState.errorIsQueueFullNotice] 那条**边界**:忙时被拒(降级告知)为 `true`,
|
||||
* 真错误为 `false`。形状仍是既有的顶部横幅,变的只是颜色——所以判据必须可测,且必须
|
||||
* 只对忙时被拒那一档成立。配色本身(哪一档用哪个色)在 `ChatScreen` 里,不进单测。
|
||||
@@ -21,32 +23,54 @@ class ParallelLimitNoticeTest {
|
||||
|
||||
private fun pending(localId: String) = PendingMessage(localId = localId, text = "这条")
|
||||
|
||||
// —— 一、reason ⇒ 文案(纯函数,穷举三档) ——
|
||||
// —— 一、reason + 档位 ⇒ 文案(纯函数,穷举) ——
|
||||
|
||||
@Test
|
||||
fun `the known reason says parallel did not fit and the message queued normally`() {
|
||||
assertEquals(PARALLEL_LIMIT_NOTICE, parallelLimitNoticeFor("parallel_limit"))
|
||||
assertEquals(PARALLEL_LIMIT_NOTICE, parallelLimitNoticeFor("parallel_limit", InterruptMode.Parallel))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the known reason is tolerant of surrounding whitespace`() {
|
||||
// 机器码两侧的空格只可能是传输噪声:认它比因为一个空格而**静默**更安全。
|
||||
assertEquals(PARALLEL_LIMIT_NOTICE, parallelLimitNoticeFor(" parallel_limit "))
|
||||
assertEquals(PARALLEL_LIMIT_NOTICE, parallelLimitNoticeFor(" parallel_limit ", InterruptMode.Parallel))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an unknown reason invents nothing`() {
|
||||
// 开放集合:将来服务端可能再加取值。不为不认识的 reason 编文案——说不出的话就不说,
|
||||
// 而「这条在排队」仍由既有的排队态与队列深度如实呈现(reason 本身也没被丢掉)。
|
||||
assertNull(parallelLimitNoticeFor("some_future_reason"))
|
||||
assertNull(parallelLimitNoticeFor("PARALLEL_LIMIT"))
|
||||
assertNull(parallelLimitNoticeFor("some_future_reason", InterruptMode.Parallel))
|
||||
assertNull(parallelLimitNoticeFor("PARALLEL_LIMIT", InterruptMode.Parallel))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an absent or blank reason has nothing to say`() {
|
||||
assertNull(parallelLimitNoticeFor(null))
|
||||
assertNull(parallelLimitNoticeFor(""))
|
||||
assertNull(parallelLimitNoticeFor(" "))
|
||||
assertNull(parallelLimitNoticeFor(null, InterruptMode.Parallel))
|
||||
assertNull(parallelLimitNoticeFor("", InterruptMode.Parallel))
|
||||
assertNull(parallelLimitNoticeFor(" ", InterruptMode.Parallel))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the default append-queue mode says nothing about a parallel limit`() {
|
||||
// 用户没选并行 ⇒ 他这条消息本来就是普通排队、他预期的就是排队 ⇒ 落回排队不是意外
|
||||
// ⇒ 不说并不构成静默。而那一帧是**广播**(别的端声明并行也会让本端收到),
|
||||
// 在默认档上说一句就是纯误报——误报会教用户不信任这条提示。
|
||||
assertNull(parallelLimitNoticeFor("parallel_limit", InterruptMode.AppendQueue))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the interrupt mode says nothing about a parallel limit`() {
|
||||
// 打断档同理(它与并行互斥):这一条本来就轮不到「另起一路」这件事。
|
||||
assertNull(parallelLimitNoticeFor("parallel_limit", InterruptMode.Interrupt))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `only the parallel mode turns the reason into a notice`() {
|
||||
// 三档一起摆出来:判据是一位档位开关,不是一个「不等于某档」的近似。
|
||||
val said = InterruptMode.entries.map { parallelLimitNoticeFor("parallel_limit", it) }
|
||||
|
||||
assertEquals(listOf(null, null, PARALLEL_LIMIT_NOTICE), said)
|
||||
}
|
||||
|
||||
// —— 二、横幅温度:降级告知柔和、真错误照旧红 ——
|
||||
|
||||
Reference in new issue
Block a user