merge: W49 —— 非激活会话落新消息时节流静默重拉(3 秒窗口 + 结构性 trailing),让抽屉红点在不开抽屉时也能出现
This commit is contained in:
commit
5dc6408bb9
3 files changed
+556
-15
No files matched your search
@@ -0,0 +1,210 @@
|
||||
package top.yeij.cyrene.ui.sessions
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
/**
|
||||
* 非激活会话的新消息 ⇒ 节流地静默重拉(W49 修的 S6:「停在主会话时次级会话来了新消息,
|
||||
* 抽屉红点不出现,必须开一次抽屉才补上」)。
|
||||
*
|
||||
* 两块纯判据([persistedRoute] 定这一帧走哪条路,[silentRefreshStep] 定这一刻该不该拉、拉完窗口怎么走),
|
||||
* 钉住四条要一直成立的边界:
|
||||
* 1. 非激活会话的新消息 ⇒ **该拉**;
|
||||
* 2. 同一节流窗口内的后续消息 ⇒ **不拉**,但窗口到点**必定**还会再拉一次(trailing,红点不会停在旧值);
|
||||
* 3. 当前打开的那个会话的新消息 ⇒ **不拉**(那条路本来就有「看到即已读」,多拉一次列表就是白花两个
|
||||
* 请求);
|
||||
* 4. 换账号 / 换地址 ⇒ **不被窗口挡住**(该拉)。切会话与它同类,但那条路刻意没接进来 ——
|
||||
* 理由见 [silentRefreshStep] 与 SessionsViewModel.select。
|
||||
*
|
||||
* 时钟一律由测试喂(判据自己不读 `System.currentTimeMillis()`),所以这里不用 sleep、也没有 flaky。
|
||||
*/
|
||||
class InactiveSessionPullTest {
|
||||
|
||||
// —— 路由:哪一帧有资格触发拉列表 ——
|
||||
|
||||
@Test
|
||||
fun `a message in another session takes the pull route`() {
|
||||
assertEquals(
|
||||
PersistedRoute.InactiveSession,
|
||||
persistedRoute(sessionId = "web:other", activeSessionId = "web"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a message in the active session never pulls the list`() {
|
||||
// 「别加请求」:当前会话的未读由「看到即已读」处理掉了,抽屉里的未读不会因这一帧变多。
|
||||
assertEquals(
|
||||
PersistedRoute.ActiveSession,
|
||||
persistedRoute(sessionId = "web", activeSessionId = "web"),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ids are compared verbatim, not by prefix`() {
|
||||
// 主会话 `web` 与次级会话 `web:xxx` 是**两个**会话(前缀相同):判据必须是等值,不是前缀。
|
||||
assertEquals(
|
||||
PersistedRoute.InactiveSession,
|
||||
persistedRoute(sessionId = "web:1", activeSessionId = "web"),
|
||||
)
|
||||
assertEquals(
|
||||
PersistedRoute.InactiveSession,
|
||||
persistedRoute(sessionId = "web", activeSessionId = "web:1"),
|
||||
)
|
||||
}
|
||||
|
||||
// —— 节流:该拉的那一刻拉,窗口内不重复拉 ——
|
||||
|
||||
@Test
|
||||
fun `the first inactive message pulls right away`() {
|
||||
val step = silentRefreshStep(
|
||||
cause = SilentRefreshCause.InactiveSessionMessage,
|
||||
nowMillis = 1_000L,
|
||||
throttle = SilentRefreshThrottle(),
|
||||
)
|
||||
|
||||
assertEquals(SilentRefreshAction.PullNow, step.action)
|
||||
// 账本:窗口从这一拉开始,且没有悬着的补拉。
|
||||
assertEquals(1_000L, step.throttle.lastPullAt)
|
||||
assertFalse(step.throttle.trailingPending)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a second message inside the window does not pull, but arms the trailing pull`() {
|
||||
val armed = silentRefreshStep(
|
||||
cause = SilentRefreshCause.InactiveSessionMessage,
|
||||
nowMillis = 1_000L,
|
||||
throttle = SilentRefreshThrottle(lastPullAt = 0L),
|
||||
)
|
||||
|
||||
assertEquals(SilentRefreshAction.ScheduleTrailing, armed.action)
|
||||
// 等到窗口走完(0 + 窗口),不是「从这一刻再等一个窗口」。
|
||||
assertEquals(SILENT_REFRESH_WINDOW_MILLIS - 1_000L, armed.delayMillis)
|
||||
assertTrue(armed.throttle.trailingPending)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `more messages inside the same window do not arm a second trailing pull`() {
|
||||
val armed = SilentRefreshThrottle(lastPullAt = 0L, trailingPending = true)
|
||||
|
||||
val step = silentRefreshStep(
|
||||
cause = SilentRefreshCause.InactiveSessionMessage,
|
||||
nowMillis = 2_000L,
|
||||
throttle = armed,
|
||||
)
|
||||
|
||||
// 已经排上的那次补拉就是「窗口里所有成因的合并」,再排一次只会多一个请求。
|
||||
assertEquals(SilentRefreshAction.Ignore, step.action)
|
||||
assertEquals(armed, step.throttle)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `a message exactly one window later pulls again`() {
|
||||
// 边界口径:正好等于窗口**算窗口已过**(见 silentRefreshStep 的 KDoc)。
|
||||
val step = silentRefreshStep(
|
||||
cause = SilentRefreshCause.InactiveSessionMessage,
|
||||
nowMillis = SILENT_REFRESH_WINDOW_MILLIS,
|
||||
throttle = SilentRefreshThrottle(lastPullAt = 0L),
|
||||
)
|
||||
|
||||
assertEquals(SilentRefreshAction.PullNow, step.action)
|
||||
assertEquals(SILENT_REFRESH_WINDOW_MILLIS, step.throttle.lastPullAt)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `one millisecond before the window is over still does not pull`() {
|
||||
val step = silentRefreshStep(
|
||||
cause = SilentRefreshCause.InactiveSessionMessage,
|
||||
nowMillis = SILENT_REFRESH_WINDOW_MILLIS - 1L,
|
||||
throttle = SilentRefreshThrottle(lastPullAt = 0L),
|
||||
)
|
||||
|
||||
assertEquals(SilentRefreshAction.ScheduleTrailing, step.action)
|
||||
assertEquals(1L, step.delayMillis)
|
||||
}
|
||||
|
||||
// —— trailing:连发之后一定还会再拉一次 ——
|
||||
|
||||
@Test
|
||||
fun `a burst of five messages ends as two pulls`() {
|
||||
// 别人连发 5 条(第 1 条时就拉过了):总共**两次**请求 —— 第 1 条那次,以及窗口到点的补拉。
|
||||
// 只有上限没有下限的话,第 2..5 条会被窗口吃掉,红点停在「第 1 条」的旧值上。
|
||||
var ledger = throttleAfterPull(0L)
|
||||
|
||||
// 第 2 条:窗口内 ⇒ 不拉,但排上补拉。
|
||||
val second = silentRefreshStep(SilentRefreshCause.InactiveSessionMessage, 500L, ledger)
|
||||
assertEquals(SilentRefreshAction.ScheduleTrailing, second.action)
|
||||
ledger = second.throttle
|
||||
|
||||
// 第 3、4、5 条:同一窗口内 ⇒ 安静(补拉已经排上了)。
|
||||
for (at in listOf(900L, 1_500L, 2_800L)) {
|
||||
val step = silentRefreshStep(SilentRefreshCause.InactiveSessionMessage, at, ledger)
|
||||
assertEquals(SilentRefreshAction.Ignore, step.action)
|
||||
ledger = step.throttle
|
||||
}
|
||||
|
||||
// 补拉到点:把它拉掉(VM 到点后走的就是这一步 —— 账本按「真的拉过一次」推进)。
|
||||
val trailingAt = 500L + second.delayMillis
|
||||
assertEquals(SILENT_REFRESH_WINDOW_MILLIS, trailingAt)
|
||||
ledger = throttleAfterPull(trailingAt)
|
||||
assertEquals(SILENT_REFRESH_WINDOW_MILLIS, ledger.lastPullAt)
|
||||
assertFalse(ledger.trailingPending)
|
||||
|
||||
// 补拉**不是终点**:下一个窗口里的新消息照样能拉(不是「只补一次就哑了」)。
|
||||
val later = silentRefreshStep(
|
||||
SilentRefreshCause.InactiveSessionMessage,
|
||||
SILENT_REFRESH_WINDOW_MILLIS * 2,
|
||||
ledger,
|
||||
)
|
||||
assertEquals(SilentRefreshAction.PullNow, later.action)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `the trailing delay lands exactly on the window boundary`() {
|
||||
// 补拉要等的时间与判据用的是同一个常量:从「排上的那一刻」往后等,正好落在窗口结束那一点
|
||||
// (VM 到点就直接拉、不再回头问判据,所以这里钉的是那个时长本身没算错)。
|
||||
val armed = silentRefreshStep(
|
||||
cause = SilentRefreshCause.InactiveSessionMessage,
|
||||
nowMillis = 700L,
|
||||
throttle = SilentRefreshThrottle(lastPullAt = 0L),
|
||||
)
|
||||
|
||||
assertEquals(0L + SILENT_REFRESH_WINDOW_MILLIS, 700L + armed.delayMillis)
|
||||
}
|
||||
|
||||
// —— 身份变化不被窗口挡住 ——
|
||||
|
||||
@Test
|
||||
fun `a context change pulls even inside the window`() {
|
||||
val insideWindow = SilentRefreshThrottle(lastPullAt = 1_000L, trailingPending = true)
|
||||
|
||||
val step = silentRefreshStep(
|
||||
cause = SilentRefreshCause.ContextChanged,
|
||||
nowMillis = 1_500L,
|
||||
throttle = insideWindow,
|
||||
)
|
||||
|
||||
// 换账号 / 换地址那一拉与「别的会话又攒了几条」无关,不该被窗口挡住。
|
||||
assertEquals(SilentRefreshAction.PullNow, step.action)
|
||||
// 这一拉已经把窗口里那条消息要的刷新一并做掉 ⇒ 窗口重开,不再补一次。
|
||||
assertEquals(1_500L, step.throttle.lastPullAt)
|
||||
assertFalse(step.throttle.trailingPending)
|
||||
}
|
||||
|
||||
// —— 账本:只记真的拉过的那一次 ——
|
||||
|
||||
@Test
|
||||
fun `an empty ledger has no pull time`() {
|
||||
assertNull(SilentRefreshThrottle().lastPullAt)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `after a pull the window restarts and nothing is left pending`() {
|
||||
val ledger = throttleAfterPull(42L)
|
||||
|
||||
assertEquals(42L, ledger.lastPullAt)
|
||||
assertFalse(ledger.trailingPending)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user