test: Android mode回显契约+ranking漂移修正(mode回显/null-mode/非法回落/顺序衰减)
CI / backend (push) Failing after 52s
CI / web (push) Successful in 29m15s
CI / android (push) Failing after 3m52s

This commit is contained in:
dsp
2026-09-12 13:43:52 +08:00
parent 8693762281
commit b5ac9b9ec8
3 changed files with 27 additions and 5 deletions
@@ -104,7 +104,9 @@ data class Video(
@Serializable
data class VideoFeedResponse(
@SerialName("next_cursor") val nextCursor: String? = null,
val results: List<Video> = emptyList()
val results: List<Video> = emptyList(),
/** 后端 feed 回显实际生效的流;非法 mode 会被回落并在此告知,客户端可据此发现参数写错 */
@SerialName("mode") val mode: String? = null
)
@Serializable
@@ -78,6 +78,14 @@ class ApiContractTest {
val parsed = json.decodeFromString<VideoFeedResponse>(raw)
assertNull(parsed.nextCursor)
assertTrue(parsed.results.isEmpty())
assertNull(parsed.mode)
}
@Test
fun `feed response echoes effective mode for illegal mode fallback`() {
val raw = """{"next_cursor": null, "mode": "recommend", "results": []}"""
val parsed = json.decodeFromString<VideoFeedResponse>(raw)
assertEquals("recommend", parsed.mode)
}
@Test
+16 -4
View File
@@ -310,6 +310,7 @@ async def test_cursor_survives_score_drift_between_pages(app, user_factory):
这是真实可复现的数据丢失,不是理论问题。
"""
from asgiref.sync import sync_to_async
from unittest.mock import patch
from apps.videos.models import Video
@@ -322,15 +323,26 @@ async def test_cursor_survives_score_drift_between_pages(app, user_factory):
for i, vid in enumerate(ids):
await sync_to_async(Video.objects.filter(pk=vid).update)(hot_score=float(100 - i))
first = await app.get("/api/v1/videos/feed?mode=recommend&page_size=2")
# 第一页请求会触发 maybe_recompute_hot_scores → 后台线程按 compute_hot_score
# 重算全表分数,覆盖上面手写的 100..95 分布。这里 mock 掉保证确定性
# (后台重算由 test_recommend 系用例覆盖)。
# NOTE: view 里是函数内 `from .services import maybe_recompute_hot_scores`,
# 必须 patch 定义处 apps.videos.services,且它是 async 函数,用 AsyncMock。
from unittest.mock import AsyncMock
with patch("apps.videos.services.maybe_recompute_hot_scores", new=AsyncMock(return_value=None)):
first = await app.get("/api/v1/videos/feed?mode=recommend&page_size=2")
body = first.json()
page1 = [v["id"] for v in body["results"]]
cursor = body["next_cursor"]
assert len(page1) == 2 and cursor
# 模拟翻页期间的分值漂移:把所有分数整体调小(时间衰减的效果)
await sync_to_async(Video.objects.filter(id__in=ids).update)(hot_score=1.0)
second = await app.get(f"/api/v1/videos/feed?mode=recommend&page_size=10&cursor={cursor}")
# 模拟翻页期间的分值漂移:整体衰减但保持相对顺序(真实时间衰减的效果)。
# 注意不能把所有行压成同一分 —— 那样锚点之前的行与锚点同分,
# 按 (score,id) 边界它们会被重新捞回,属于测试模拟失真,不是代码 bug。
for i, vid in enumerate(ids):
await sync_to_async(Video.objects.filter(pk=vid).update)(hot_score=float(50 - i))
with patch("apps.videos.services.maybe_recompute_hot_scores", new=AsyncMock(return_value=None)):
second = await app.get(f"/api/v1/videos/feed?mode=recommend&page_size=10&cursor={cursor}")
page2 = [v["id"] for v in second.json()["results"]]
assert not (set(page1) & set(page2)), f"分值漂移后翻页重复返回: 第一页{page1} 第二页{page2}"