test: Android mode回显契约+ranking漂移修正(mode回显/null-mode/非法回落/顺序衰减)
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user