test: Android mode回显契约+ranking漂移修正(mode回显/null-mode/非法回落/顺序衰减)
This commit is contained in:
@@ -104,7 +104,9 @@ data class Video(
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class VideoFeedResponse(
|
data class VideoFeedResponse(
|
||||||
@SerialName("next_cursor") val nextCursor: String? = null,
|
@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
|
@Serializable
|
||||||
|
|||||||
@@ -78,6 +78,14 @@ class ApiContractTest {
|
|||||||
val parsed = json.decodeFromString<VideoFeedResponse>(raw)
|
val parsed = json.decodeFromString<VideoFeedResponse>(raw)
|
||||||
assertNull(parsed.nextCursor)
|
assertNull(parsed.nextCursor)
|
||||||
assertTrue(parsed.results.isEmpty())
|
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
|
@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 asgiref.sync import sync_to_async
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
from apps.videos.models import Video
|
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):
|
for i, vid in enumerate(ids):
|
||||||
await sync_to_async(Video.objects.filter(pk=vid).update)(hot_score=float(100 - i))
|
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()
|
body = first.json()
|
||||||
page1 = [v["id"] for v in body["results"]]
|
page1 = [v["id"] for v in body["results"]]
|
||||||
cursor = body["next_cursor"]
|
cursor = body["next_cursor"]
|
||||||
assert len(page1) == 2 and 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"]]
|
page2 = [v["id"] for v in second.json()["results"]]
|
||||||
|
|
||||||
assert not (set(page1) & set(page2)), f"分值漂移后翻页重复返回: 第一页{page1} 第二页{page2}"
|
assert not (set(page1) & set(page2)), f"分值漂移后翻页重复返回: 第一页{page1} 第二页{page2}"
|
||||||
|
|||||||
Reference in New Issue
Block a user