merge: W2 — A2 interrupt scope(传输层 + 设置项)
This commit is contained in:
commit
b44e8d96b9
6 files changed
+233
-4
No files matched your search
@@ -0,0 +1,58 @@
|
||||
package top.yeij.cyrene.core.prefs
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
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)。
|
||||
*
|
||||
* 这一层只负责一件事:把用户选的档位翻成 `interrupt` 帧的 `scope`。默认档必须翻成
|
||||
* **整键省略**——服务端视同 `"all"`,帧因此与加入 `scope` 之前**逐字节相同**(兼容保证)。
|
||||
*/
|
||||
class InterruptModeTest {
|
||||
|
||||
private fun frameOf(mode: InterruptMode): String =
|
||||
WsOutbound.Interrupt(sessionId = "web", scope = mode.wireScope).toJson()
|
||||
|
||||
@Test
|
||||
fun `the default mode serialises to today's interrupt frame byte for byte`() {
|
||||
val mode = AppPrefs().interruptMode
|
||||
|
||||
assertEquals(InterruptMode.AppendQueue, mode)
|
||||
assertNull("默认档必须是「省略 scope」(服务端 = all)", mode.wireScope)
|
||||
assertEquals("""{"type":"interrupt","data":{"sessionId":"web"}}""", frameOf(mode))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the interrupt mode maps to scope current so the queue survives`() {
|
||||
assertEquals("current", InterruptMode.Interrupt.wireScope)
|
||||
assertEquals(
|
||||
"""{"type":"interrupt","data":{"sessionId":"web","scope":"current"}}""",
|
||||
frameOf(InterruptMode.Interrupt),
|
||||
)
|
||||
}
|
||||
|
||||
@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 })
|
||||
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))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the persisted enum names are stable`() {
|
||||
// AppSettings 按 enum 名落盘:改名 = 把所有已存偏好静默重置回默认档。
|
||||
assertEquals(listOf("AppendQueue", "Interrupt"), InterruptMode.entries.map { it.name })
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package top.yeij.cyrene.data.ws
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* A2:`interrupt` 的可选 `scope`(契约 v0.56 §3.1,取值域 `{"", "all", "current"}`)。
|
||||
*
|
||||
* 两条要点:
|
||||
* - **省略 `scope` 的帧必须与今天逐字节相同**——这是与老服务端 / 老行为的兼容保证;
|
||||
* - 本层**不校验**取值(服务端开放取值域由服务端裁决):除了省略,其余一律原样透传,
|
||||
* 既不自作主张补默认、也不静默改写成别名——服务端对非法值是**定向回 `bad_request`**。
|
||||
*/
|
||||
class WsOutboundInterruptTest {
|
||||
|
||||
/** 加入 `scope` 之前那条帧的逐字形状。 */
|
||||
private val legacyFrame = """{"type":"interrupt","data":{"sessionId":"web"}}"""
|
||||
|
||||
@Test
|
||||
fun `an interrupt without a scope is today's frame byte for byte`() {
|
||||
assertEquals(legacyFrame, WsOutbound.Interrupt(sessionId = "web").toJson())
|
||||
assertEquals(legacyFrame, WsOutbound.Interrupt(sessionId = "web", scope = null).toJson())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `omitting the scope omits the key itself rather than sending null`() {
|
||||
val json = WsOutbound.Interrupt(sessionId = "web").toJson()
|
||||
|
||||
// `"scope":null` 与「整键省略」在服务端不是一回事:后者才是「未声明」。
|
||||
assertFalse("省略时不该出现 scope 键:$json", json.contains("scope"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `scope current is carried verbatim next to the session`() {
|
||||
assertEquals(
|
||||
"""{"type":"interrupt","data":{"sessionId":"web","scope":"current"}}""",
|
||||
WsOutbound.Interrupt(sessionId = "web", scope = "current").toJson(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `scope all and an empty scope are transmitted as-is`() {
|
||||
// 三者(省略 / "" / "all")在服务端同义,但**同义不等于客户端该改写**:
|
||||
// 调用方给了什么就发什么,服务端才是解释者。
|
||||
assertEquals(
|
||||
"""{"type":"interrupt","data":{"sessionId":"web","scope":"all"}}""",
|
||||
WsOutbound.Interrupt(sessionId = "web", scope = "all").toJson(),
|
||||
)
|
||||
assertEquals(
|
||||
"""{"type":"interrupt","data":{"sessionId":"web","scope":""}}""",
|
||||
WsOutbound.Interrupt(sessionId = "web", scope = "").toJson(),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `an unknown scope is sent verbatim and never silently downgraded to the default`() {
|
||||
// 服务端大小写敏感:`"Current"` 是非法值,会定向回 bad_request 且**不打断任何会话**。
|
||||
// 客户端若在此处「帮它纠正」成 "current",用户看到的就是一次没解释的打断。
|
||||
val json = WsOutbound.Interrupt(sessionId = "web", scope = "Current").toJson()
|
||||
|
||||
assertEquals(
|
||||
"""{"type":"interrupt","data":{"sessionId":"web","scope":"Current"}}""",
|
||||
json,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the session id stays first so older servers see the same key order`() {
|
||||
assertEquals(
|
||||
"""{"type":"interrupt","data":{"sessionId":"web","scope":"current"}}""",
|
||||
WsOutbound.Interrupt("web", "current").toJson(),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user