feat(settings): 设置页「她忙时发消息」追加第三档「并行」——忙时不打断、另起一路(chat.parallel=true)
主人要求:在既有的「追加队列 / 打断」之下再追加一档「并行」。三档语义互补——追加队列进队列等 (默认档,逐字节不变)、打断先发 interrupt 再发这条、并行不打断、另起一路同时处理。 改了什么: - AppSettings:InterruptMode 加第三个值 Parallel(wireScope = null, wireParallel = true),并给枚举 补出第二根线 wireParallel(wireScope 只管 interrupt 的 scope,表达不了「要声明并行」这件事)。 既有两个值的 name 与落盘键 interrupt_mode 一个字未动——改名会让已存偏好读不出来并静默回落默认档。 落盘字符串 → 档位的映射抽成顶层纯函数 toInterruptMode(AppSettings 本体要 Android + DataStore, 单测里起不来),新增的第三个名字能正确读回,未知字符串仍回落 AppendQueue(既有行为未变)。 - 判据抽成纯函数 parallelForSend(mode, busy) 与 parallelWireValue(declare):前者与 interruptScopeForSend 共用同一个忙判据(ChatUiState.busyMode != null),只在会话正忙时为真。 规范 §2.2 没写空闲时要不要也声明——空闲时这条本来就是它自己的回合,声明与不声明在服务端是同一 件事,故取与打断同一个判据(最小惊讶:空闲时三档逐字节完全相同)。 - 两条互斥由枚举取值本身保证,不是靠约定:并行档 wireScope 是 null ⇒ 绝不发 interrupt;另外两档 wireParallel 是 false ⇒ 绝不带 parallel 键。默认档「追加队列」线上零差异由此结构性成立。 - 发送路径接线:ChatViewModel.deliver 只读一次偏好、同时喂给两个判据(读两次会在用户切档的瞬间 出现「既打断又并行」);ChatSocket.sendChat 与 ChatRequest 各加一个 parallel 参数,false 一律 翻成整键省略——显式 false 会被序列化成 parallel:false,那是「显式否认」而不是「未声明」, 默认档就不再逐字节不变。 - 设置页:新选项标题「并行」,summary 说清后果——不打断她正在说的这一句、两路各算一遍会多花一份 算力与 token(主人裁决:峰值 token 近翻倍可接受,但用户有权知道)。UI 仍是 Miuix,与同栏另外 两条逐字同形,勾选标记位置相同。 - 分享路径同样不套用这一档偏好(W26 的 allowInterrupt 因此更名为 applySendPreference,它现在管 的是整档偏好):分享是批量投递,逐条索要并行会被服务端 2 路额度把同一次分享劈成两种待遇,且每 条各算一遍;用户也没按过发送键说这条要并行。单张图时同样不声明,规则才能一句话说清。 为什么:主人 2026-10-05 的裁决(真两路并行)需要客户端有一个显式声明入口;W30 已备好 chat.parallel 的线上形状(默认 null = 整键省略),本次只把设置页这一档与发送路径接通,上行仍是纯增量——没选 「并行」的用户线上一个字节都不变。 验收:assembleDebug 通过;testDebugUnitTest 通过(1022 测试 / 0 失败 / 0 错误;基线 1004,新增 18: ParallelForSendTest 13 条 + InterruptScopeTest 1 条 + InterruptModeTest 4 条)。
This commit is contained in:
1 parent
82d3ca3953
commit
a776e9f608
11 files changed
+462
-92
No files matched your search
@@ -1,16 +1,24 @@
|
||||
package top.yeij.cyrene.core.prefs
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
import top.yeij.cyrene.data.ws.WsOutbound
|
||||
|
||||
/**
|
||||
* A2 偏好 → 线上取值 的映射(契约 v0.56 §3.1)。
|
||||
* 「她忙时发消息」档位 → 线上取值 的映射(契约 v0.56 §3.1 的 `interrupt.scope`;忙时并行规范
|
||||
* §2.2 的 `chat.parallel`)。
|
||||
*
|
||||
* 这一层只负责一件事:把用户选的档位翻成 `interrupt` 帧的 `scope`。默认档必须翻成
|
||||
* **整键省略**——服务端视同 `"all"`,帧因此与加入 `scope` 之前**逐字节相同**(兼容保证)。
|
||||
* 这一层负责三件事:
|
||||
* 1. 把用户选的档位翻成 `interrupt` 帧的 `scope`——**默认档与「并行」档都必须是「不发」**
|
||||
* (默认档在服务端视同 `"all"`,帧因此与加入 `scope` 之前逐字节相同;并行档则压根不该有这条帧,
|
||||
* 它与打断是互斥语义);
|
||||
* 2. 两个声明**没有哪一档同时给**(`wireScope` 与 `wireParallel` 永不同时成立);
|
||||
* 3. 落盘那一侧(`AppSettings` 的 `interrupt_mode` 键,按 `enum.name` 存)三档都能存能读、
|
||||
* 未知值仍回落默认档。`AppSettings` 本体要 Android + DataStore、单测里起不来,映射本身是顶层
|
||||
* 纯函数 [toInterruptMode]。
|
||||
*/
|
||||
class InterruptModeTest {
|
||||
|
||||
@@ -22,7 +30,7 @@ class InterruptModeTest {
|
||||
val mode = AppPrefs().interruptMode
|
||||
|
||||
assertEquals(InterruptMode.AppendQueue, mode)
|
||||
assertNull("默认档必须是「省略 scope」(服务端 = all)", mode.wireScope)
|
||||
assertNull("默认档必须是「不发打断帧」", mode.wireScope)
|
||||
assertEquals("""{"type":"interrupt","data":{"sessionId":"web"}}""", frameOf(mode))
|
||||
}
|
||||
|
||||
@@ -36,23 +44,63 @@ class InterruptModeTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the preference only ever produces the two documented wire values`() {
|
||||
// 服务端取值域是 {"", "all", "current"};客户端只发「省略」与 "current" 两种,
|
||||
// ""/"all" 已由省略表达——不自己补一串等价别名,少一种线上形状就少一类歧义。
|
||||
assertEquals(listOf(null, "current"), InterruptMode.entries.map { it.wireScope })
|
||||
fun `the parallel mode sends no interrupt frame at all`() {
|
||||
// 并行与打断互斥:并行档先打断就恰好砍掉它要并行的那一路。所以这里不是「省略 scope」
|
||||
// 而是「这条路根本不发这条帧」——`wireScope` 就是 `null`,帧形状与默认档那个「不发」一致。
|
||||
assertNull(InterruptMode.Parallel.wireScope)
|
||||
assertEquals("""{"type":"interrupt","data":{"sessionId":"web"}}""", frameOf(InterruptMode.Parallel))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the preference only ever produces the documented interrupt wire values`() {
|
||||
// 服务端取值域是 {"", "all", "current"};客户端只发「不发打断帧」与 "current" 两种,
|
||||
// ""/"all" 由「不发打断帧」表达——不自己补一串等价别名,少一种线上形状就少一类歧义。
|
||||
assertEquals(listOf(null, "current", null), InterruptMode.entries.map { it.wireScope })
|
||||
assertTrue(InterruptMode.entries.none { it.wireScope == "all" })
|
||||
assertTrue(InterruptMode.entries.none { it.wireScope == "" })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `both modes stay distinguishable on the wire`() {
|
||||
// 打成同一个形状就等于「两种模式被当成一种」——正是 A2 要修掉的那类缺陷。
|
||||
assertTrue(frameOf(InterruptMode.AppendQueue) != frameOf(InterruptMode.Interrupt))
|
||||
fun `no mode ever asks for both an interrupt and a parallel leg`() {
|
||||
// 一对互斥语义:打断 = 先取消她正在说的这一句;并行 = 不打断、另起一路同时说。
|
||||
// 两件事同时声明是自相矛盾的(先砍掉要并行的那一路),三档各占一种组合,没有第三种。
|
||||
val names = InterruptMode.entries.map { it.name }
|
||||
|
||||
assertTrue(
|
||||
"没有任何一档能同时给两个声明:$names",
|
||||
InterruptMode.entries.none { it.wireScope != null && it.wireParallel },
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `only the parallel mode ever declares a parallel leg`() {
|
||||
assertEquals(listOf(InterruptMode.Parallel), InterruptMode.entries.filter { it.wireParallel })
|
||||
assertFalse("默认档一个声明都不发", AppPrefs().interruptMode.wireParallel)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the persisted enum names are stable`() {
|
||||
// AppSettings 按 enum 名落盘:改名 = 把所有已存偏好静默重置回默认档。
|
||||
assertEquals(listOf("AppendQueue", "Interrupt"), InterruptMode.entries.map { it.name })
|
||||
// 第三档是**追加**上来的新名字;既有那两个名字一个字都不许动。
|
||||
assertEquals(listOf("AppendQueue", "Interrupt", "Parallel"), InterruptMode.entries.map { it.name })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `every mode survives a round trip through the persisted name`() {
|
||||
InterruptMode.entries.forEach { mode ->
|
||||
assertEquals("落盘名 ${mode.name} 读不回原来的档", mode, mode.name.toInterruptMode())
|
||||
}
|
||||
// 三个落盘名必须互不相同,否则有档位会被读成另一档。
|
||||
assertEquals(3, InterruptMode.entries.map { it.name }.toSet().size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an unknown persisted value still falls back to the default mode`() {
|
||||
// 缺省 / 旧版本遗留 / 被手改过 / 大小写不同 / 带了空格的值,一律解成默认档:回落方向必须是
|
||||
// 「最少动作」的那一档,**绝不**悄悄换成更激进的「打断」或「并行」。
|
||||
// 注意 "current" 是 interrupt 帧的 scope 取值、不是档位名——它也必须回落。
|
||||
listOf<String?>(null, "", "all", "current", "Parallelism", "parallel", "AppendQueue ", "3").forEach { raw ->
|
||||
assertEquals("落盘值 ${raw?.let { "「$it」" } ?: "空"} 应回落默认档", InterruptMode.AppendQueue, raw.toInterruptMode())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,9 +7,10 @@ import org.junit.Test
|
||||
/**
|
||||
* 忙着并行规范的上行两处**纯增量**(§2.2 `chat.parallel` / §2.3 `interrupt.turnId`)。
|
||||
*
|
||||
* 本层只负责一件事:**省略时整键不进线上、给值时原样发出**。本端今天一个字节都不发这两个字段
|
||||
* (真并行要用户显式声明,声明入口不在本次范围内),所以这一组测试真正钉住的是
|
||||
* 「老形状的那条帧仍然逐字节相同」——那是与老服务端兼容的全部依据。
|
||||
* 本层只负责一件事:**省略时整键不进线上、给值时原样发出**。`parallel` 现在有调用方了——
|
||||
* 设置页那栏选「并行」且会话正忙时由发送路径置 `true`(判据是纯函数 `parallelForSend`,线上取值
|
||||
* 经 `parallelWireValue` 翻,`false` 一律翻成「整键省略」);`interrupt.turnId` 本端仍一个字节都不发。
|
||||
* 所以这一组测试真正钉住的是「老形状的那条帧仍然逐字节相同」——那是与老服务端兼容的全部依据。
|
||||
*/
|
||||
class WsOutboundParallelTest {
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ import top.yeij.cyrene.core.prefs.InterruptMode
|
||||
* A2 客户端半边:**发这条消息之前要不要先打断**(契约 v0.56 §3.1 的 `interrupt.scope`)。
|
||||
*
|
||||
* 这个判据此前**根本不存在**——设置页那个开关没有任何发送路径读它,于是「打断」与「追加队列」
|
||||
* 行为完全一样,`ChatSocket.sendInterrupt` 一条帧都没发过。这里把它钉成纯函数,四条一次钉全:
|
||||
* 行为完全一样,`ChatSocket.sendInterrupt` 一条帧都没发过。这里把它钉成纯函数,六条一次钉全
|
||||
* (并行那一档的**另一半**——`chat.parallel`——见 [ParallelForSendTest]):
|
||||
*
|
||||
* | 档位 | 会话正忙 | 该发什么 |
|
||||
* |---|---|---|
|
||||
@@ -19,12 +20,16 @@ import top.yeij.cyrene.core.prefs.InterruptMode
|
||||
* | 追加队列(默认) | 空闲 | **什么都不发** |
|
||||
* | 打断 | 忙 | `interrupt{scope:"current"}` |
|
||||
* | 打断 | 空闲 | **什么都不发** |
|
||||
* | 并行 | 忙 | **什么都不发**(它与打断互斥;那一档声明的是 `chat.parallel`) |
|
||||
* | 并行 | 空闲 | **什么都不发** |
|
||||
*
|
||||
* 两条最容易写错的边界各占一列:
|
||||
* 三条最容易写错的边界各占一列:
|
||||
* - **默认档不许有任何行为变化**(第一行、第二行):线上请求必须与接入本判据之前逐字节相同;
|
||||
* - **打断但空闲不发帧**(第四行):命中空闲会话在服务端是 no-op,白发一条帧只是多一次往返、
|
||||
* 还会在 logcat 里制造「这里打断过」的假象——所以「正忙」必须是**服务端说的**
|
||||
* (`session_busy` ⇒ `ChatUiState.busyMode != null`),不是队列深度非零推出来的。
|
||||
* (`session_busy` ⇒ `ChatUiState.busyMode != null`),不是队列深度非零推出来的;
|
||||
* - **并行档整列都不发**(第五行、第六行):并行与打断互斥,先打断就恰好砍掉要并行的那一路。
|
||||
* 这不是「这一档碰巧没得发」,是 `Parallel.wireScope == null` 的结构性结论。
|
||||
*/
|
||||
class InterruptScopeTest {
|
||||
|
||||
@@ -52,8 +57,8 @@ class InterruptScopeTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the whole matrix of both modes against both busy states is pinned`() {
|
||||
// 2 档 × 2 忙闲 = 4 格,一次穷举钉死;将来任何人动这个判据,这里先红。
|
||||
fun `the whole matrix of all three modes against both busy states is pinned`() {
|
||||
// 3 档 × 2 忙闲 = 6 格,一次穷举钉死;将来任何人动这个判据,这里先红。
|
||||
val matrix = InterruptMode.entries.flatMap { mode ->
|
||||
listOf(true, false).map { busy ->
|
||||
"${mode.name}/busy=$busy" to interruptScopeForSend(mode, busy)
|
||||
@@ -66,6 +71,8 @@ class InterruptScopeTest {
|
||||
"AppendQueue/busy=false" to null,
|
||||
"Interrupt/busy=true" to "current",
|
||||
"Interrupt/busy=false" to null,
|
||||
"Parallel/busy=true" to null,
|
||||
"Parallel/busy=false" to null,
|
||||
),
|
||||
matrix,
|
||||
)
|
||||
@@ -85,7 +92,7 @@ class InterruptScopeTest {
|
||||
// 发送路径取偏好读失败时回落 `AppPrefs()` 的缺省值(见 ChatViewModel.currentInterruptMode 的 KDoc)。
|
||||
// 这里钉住那个缺省值本身是**追加队列** = 不发任何帧 —— 读不到偏好时回落的方向必须是
|
||||
// 「最少动作」的那一档,绝不能悄悄变成更激进的「打断」。
|
||||
// (落盘字符串 → 枚举的映射另有 AppSettings.toInterruptMode 与 InterruptModeTest 钉着。)
|
||||
// (落盘字符串 → 枚举的映射另有顶层纯函数 toInterruptMode 与 InterruptModeTest 钉着。)
|
||||
val fallback = AppPrefs().interruptMode
|
||||
|
||||
assertEquals(InterruptMode.AppendQueue, fallback)
|
||||
@@ -94,7 +101,7 @@ class InterruptScopeTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the two modes only differ while she is busy`() {
|
||||
fun `the interrupt mode only differs from the default while she is busy`() {
|
||||
// 空闲那一列两种档位必须**看起来一样**(都不发);差异只允许出现在「忙」这一列,
|
||||
// 否则默认档就不是零行为变化了。
|
||||
assertEquals(
|
||||
@@ -106,4 +113,17 @@ class InterruptScopeTest {
|
||||
interruptScopeForSend(InterruptMode.Interrupt, busy = true),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the parallel mode is indistinguishable from the default on this axis however busy she is`() {
|
||||
// 并行与打断互斥,所以这一档在**这条**轴上一个字节都不该多出来:忙闲两种情形都必须
|
||||
// 与默认档完全一致(它的声明在另一条轴上,见 ParallelForSendTest)。
|
||||
listOf(true, false).forEach { busy ->
|
||||
assertEquals(
|
||||
"busy=$busy 时并行档发出了打断帧——那会砍掉它自己要并行的那一路",
|
||||
interruptScopeForSend(InterruptMode.AppendQueue, busy),
|
||||
interruptScopeForSend(InterruptMode.Parallel, busy),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package top.yeij.cyrene.domain.chat
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
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.AppPrefs
|
||||
import top.yeij.cyrene.core.prefs.InterruptMode
|
||||
import top.yeij.cyrene.data.ws.WsOutbound
|
||||
|
||||
/**
|
||||
* 忙时并行的客户端半边:**发这条消息时要不要声明 `parallel:true`**(忙时并行规范 §2.2),
|
||||
* 也就是 [interruptScopeForSend] 的兄弟 [parallelForSend]([InterruptScopeTest] 钉的是另一条轴)。
|
||||
*
|
||||
* 设置页那栏「她忙时发消息」现在是三选一,两条铁律在这里穷举钉死:
|
||||
*
|
||||
* | 档位 | 会话正忙时这一条怎么发 | `interrupt` | `chat.parallel` |
|
||||
* |---|---|---|---|
|
||||
* | 追加队列(默认) | 排队等(今天的行为) | **不发** | **不带**(线上逐字节不变) |
|
||||
* | 打断 | 先取消在跑的那一轮,再发 | `scope:"current"` | **不带** |
|
||||
* | 并行 | **不打断**,本条另起一路 | **绝不发** | `true` |
|
||||
*
|
||||
* 顺手把「空闲时三档逐字节完全相同」钉住:规范 §2.2 只写了 `parallel:true` 是「请求把本条作为
|
||||
* 独立一路处理」,**没写**空闲时要不要照发。空闲时这条本来就是它自己的回合,声明与不声明在服务端
|
||||
* 是同一件事 ⇒ 本端取与打断**同一个判据**(只在忙时声明),空闲时切档不会改变一个字节。
|
||||
*/
|
||||
class ParallelForSendTest {
|
||||
|
||||
/** 加入 `parallel` / 加入本档位之前那条 `chat` 帧的逐字形状。 */
|
||||
private val legacyChat =
|
||||
"""{"type":"chat","data":{"sessionId":"web","text":"你好","deviceId":"d","clientMsgId":"c-1"}}"""
|
||||
|
||||
/** 同上,但带上了 `parallel:true`(只有「并行」档在忙时才是这个形状)。 */
|
||||
private val parallelChat =
|
||||
"""{"type":"chat","data":{"sessionId":"web","text":"你好","deviceId":"d","clientMsgId":"c-1","parallel":true}}"""
|
||||
|
||||
/** 「打断」档在忙时那条帧。 */
|
||||
private val interruptCurrent =
|
||||
"""{"type":"interrupt","data":{"sessionId":"web","scope":"current"}}"""
|
||||
|
||||
/**
|
||||
* 按偏好把**实际会铺到线上的帧序列**铺出来:`interrupt` 只在 scope 非 `null` 时才发,
|
||||
* `chat` 永远发一条、`parallel` 键按判定带——与 `ChatViewModel.deliver` 接上
|
||||
* `ChatSocket.sendChat` / `ChatRequest` 之后的行为同形(那边把这两步接在一起,这里把它摊平)。
|
||||
*/
|
||||
private fun wireOf(mode: InterruptMode, busy: Boolean): List<String> = buildList {
|
||||
interruptScopeForSend(mode, busy)?.let {
|
||||
add(WsOutbound.Interrupt(sessionId = "web", scope = it).toJson())
|
||||
}
|
||||
add(
|
||||
WsOutbound.Chat(
|
||||
text = "你好",
|
||||
sessionId = "web",
|
||||
deviceId = "d",
|
||||
clientMsgId = "c-1",
|
||||
parallel = parallelWireValue(parallelForSend(mode, busy)),
|
||||
).toJson(),
|
||||
)
|
||||
}
|
||||
|
||||
// —— 判据本身 ——
|
||||
|
||||
@Test
|
||||
fun `the default mode declares nothing even while she is busy`() {
|
||||
// 最重要的一条:默认档是绝大多数用户手上的状态,它必须零行为变化。
|
||||
assertFalse(parallelForSend(InterruptMode.AppendQueue, busy = true))
|
||||
assertEquals(AppPrefs().interruptMode, InterruptMode.AppendQueue)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the default mode declares nothing while she is idle either`() {
|
||||
assertFalse(parallelForSend(InterruptMode.AppendQueue, busy = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the parallel mode asks for its own leg while she is busy`() {
|
||||
assertTrue(parallelForSend(InterruptMode.Parallel, busy = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the parallel mode asks for nothing while she is idle`() {
|
||||
// 空闲时这条本来就是它自己的回合:声明了服务端也没第二路可并。
|
||||
assertFalse(parallelForSend(InterruptMode.Parallel, busy = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the interrupt mode never declares a parallel leg`() {
|
||||
// 「打断」与「并行」互斥:选了打断就是要她**先停**,同时再要一路并行是自相矛盾。
|
||||
assertFalse(parallelForSend(InterruptMode.Interrupt, busy = true))
|
||||
assertFalse(parallelForSend(InterruptMode.Interrupt, busy = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the whole matrix of all three modes against both busy states is pinned`() {
|
||||
// 3 档 × 2 忙闲 = 6 格,一次穷举钉死;将来任何人动这个判据,这里先红。
|
||||
val matrix = InterruptMode.entries.flatMap { mode ->
|
||||
listOf(true, false).map { busy -> "${mode.name}/busy=$busy" to parallelForSend(mode, busy) }
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
listOf(
|
||||
"AppendQueue/busy=true" to false,
|
||||
"AppendQueue/busy=false" to false,
|
||||
"Interrupt/busy=true" to false,
|
||||
"Interrupt/busy=false" to false,
|
||||
"Parallel/busy=true" to true,
|
||||
"Parallel/busy=false" to false,
|
||||
),
|
||||
matrix,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `only the busy parallel cell ever declares a leg`() {
|
||||
val declaring = InterruptMode.entries.filter { mode ->
|
||||
listOf(true, false).any { busy -> parallelForSend(mode, busy) }
|
||||
}
|
||||
|
||||
assertEquals(listOf(InterruptMode.Parallel), declaring)
|
||||
}
|
||||
|
||||
// —— 两条铁律:并行绝不打断;另外两档绝不并行 ——
|
||||
|
||||
@Test
|
||||
fun `the parallel mode never sends an interrupt frame however busy she is`() {
|
||||
listOf(true, false).forEach { busy ->
|
||||
assertNull("并行档发出打断帧就会砍掉它自己要并行的那一路", interruptScopeForSend(InterruptMode.Parallel, busy))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `no mode ever declares both an interrupt and a parallel leg`() {
|
||||
InterruptMode.entries.forEach { mode ->
|
||||
listOf(true, false).forEach { busy ->
|
||||
assertFalse(
|
||||
"${mode.name}/busy=$busy 同时要了打断与并行",
|
||||
interruptScopeForSend(mode, busy) != null && parallelForSend(mode, busy),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// —— 铺到线上的形状 ——
|
||||
|
||||
@Test
|
||||
fun `the whole wire shape of all three modes is pinned frame by frame`() {
|
||||
assertEquals(listOf(legacyChat), wireOf(InterruptMode.AppendQueue, busy = true))
|
||||
assertEquals(listOf(legacyChat), wireOf(InterruptMode.AppendQueue, busy = false))
|
||||
|
||||
assertEquals(listOf(interruptCurrent, legacyChat), wireOf(InterruptMode.Interrupt, busy = true))
|
||||
assertEquals(listOf(legacyChat), wireOf(InterruptMode.Interrupt, busy = false))
|
||||
|
||||
// 并行:**没有** interrupt 帧,只有一条多带 `parallel:true` 的 chat 帧。
|
||||
assertEquals(listOf(parallelChat), wireOf(InterruptMode.Parallel, busy = true))
|
||||
assertEquals(listOf(legacyChat), wireOf(InterruptMode.Parallel, busy = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `while she is idle all three modes put exactly the same bytes on the wire`() {
|
||||
// 空闲是绝大多数发送的情形;切档在这里必须**零差异**(三档的唯一差别是「她正忙时」)。
|
||||
val wires = InterruptMode.entries.map { wireOf(it, busy = false) }
|
||||
|
||||
assertEquals(3, wires.size)
|
||||
assertTrue("空闲时三档的帧不一致:$wires", wires.all { it == listOf(legacyChat) })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the default mode keeps today's bytes on the wire while she is busy`() {
|
||||
// 「一个字节都不变」的正面陈述:默认档忙时那条 chat 帧就是加入 `parallel` 之前的字面量。
|
||||
assertEquals(listOf(legacyChat), wireOf(AppPrefs().interruptMode, busy = true))
|
||||
}
|
||||
|
||||
// —— 判定 → 字段取值的那一步 ——
|
||||
|
||||
@Test
|
||||
fun `a false judgement must omit the key rather than send an explicit false`() {
|
||||
// `false` 与「省略」在服务端同义(都落回排队),但**线上形状不同**:带上 `"parallel":false`
|
||||
// 就不再是「与加入该字段之前逐字节相同」。只有 true 才带上该键。
|
||||
assertNull(parallelWireValue(false))
|
||||
assertEquals(true, parallelWireValue(true))
|
||||
assertEquals(legacyChat, wireOf(InterruptMode.AppendQueue, busy = true).last())
|
||||
assertFalse(wireOf(InterruptMode.AppendQueue, busy = true).last().contains("parallel"))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user