test_portrait_cache.py 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. """M9 修复:作者画像按作者缓存(画像是作者级,同 run 内同作者只拉一次)。"""
  2. from content_agent.business_modules.progressive_screening import _ProgressiveContext
  3. class CountingLimiter:
  4. def __init__(self):
  5. self.calls = 0
  6. def wait(self):
  7. self.calls += 1
  8. return 0.0
  9. class CountingPortraitClient:
  10. def __init__(self):
  11. self.calls = []
  12. def fetch_account_fans_portrait(self, account_id):
  13. self.calls.append(account_id)
  14. return {
  15. "fans": {
  16. "age": {
  17. "data": {
  18. "41-50": {"percentage": "31.73%", "preference": "210"},
  19. "50-": {"percentage": "29.76%", "preference": "130"},
  20. }
  21. }
  22. }
  23. }
  24. def _ctx(client, *, limiter=None, portrait_limiter=None):
  25. return _ProgressiveContext(
  26. run_id="r", policy_run_id="p", source_context={}, policy_bundle={},
  27. platform_client=client, runtime=None, gemini_video_client=None,
  28. limiter=limiter, portrait_limiter=portrait_limiter,
  29. archive_dispatcher=None, platform="douyin", portrait_client=client,
  30. )
  31. def _bundle(author, q=80, p=70):
  32. return {
  33. "pattern_match_result": {"query_relevance_score": q},
  34. "content_engagement_metrics": {"platform_performance": {"platform_performance_score": p}},
  35. "content": {"author": {"platform_author_id": author}},
  36. }
  37. def test_portrait_cached_by_author():
  38. client = CountingPortraitClient()
  39. ctx = _ctx(client)
  40. bundles = [_bundle("a1"), _bundle("a1"), _bundle("a2")]
  41. ctx._inject_fifty_plus(bundles)
  42. assert client.calls == ["a1", "a2"] # 同作者只拉一次
  43. assert all(b["content_audience_50plus"]["status"] == "ok" for b in bundles)
  44. def test_portrait_uses_separate_limiter_bucket():
  45. client = CountingPortraitClient()
  46. search_limiter = CountingLimiter()
  47. portrait_limiter = CountingLimiter()
  48. ctx = _ctx(client, limiter=search_limiter, portrait_limiter=portrait_limiter)
  49. ctx._inject_fifty_plus([_bundle("a1"), _bundle("a2")])
  50. assert search_limiter.calls == 0
  51. assert portrait_limiter.calls == 2
  52. def test_portrait_fetched_when_query_passes_regardless_of_platform():
  53. # 修复:门只看 query 相关性,平台分再低也拉画像、算适老性(原来 platform<65 会跳过)。
  54. client = CountingPortraitClient()
  55. ctx = _ctx(client)
  56. bundles = [_bundle("a1", q=70, p=20)] # 平台仅 20
  57. ctx._inject_fifty_plus(bundles)
  58. assert client.calls == ["a1"]
  59. assert bundles[0]["content_audience_50plus"]["status"] == "ok"
  60. def test_portrait_skipped_when_query_below_gate():
  61. client = CountingPortraitClient()
  62. ctx = _ctx(client)
  63. bundles = [_bundle("a1", q=60, p=90)] # 相关性 60 < 65 → 不拉
  64. ctx._inject_fifty_plus(bundles)
  65. assert client.calls == []
  66. assert bundles[0]["content_audience_50plus"]["status"] == "not_attempted"
  67. class FlakyPortraitClient:
  68. def __init__(self, fail_times):
  69. self.fail_times = fail_times
  70. self.calls = 0
  71. def fetch_account_fans_portrait(self, account_id):
  72. self.calls += 1
  73. if self.calls <= self.fail_times:
  74. raise RuntimeError("热点宝 429")
  75. return {"fans": {"age": {"data": {"50-": {"percentage": "40%", "preference": "150"}}}}}
  76. def test_portrait_retry_recovers_transient_failure():
  77. client = FlakyPortraitClient(fail_times=1) # 第一次失败、第二次成功
  78. ctx = _ctx(client)
  79. bundles = [_bundle("a1")]
  80. ctx._inject_fifty_plus(bundles)
  81. assert client.calls == 2 # 重试了一次
  82. assert bundles[0]["content_audience_50plus"]["status"] == "ok"
  83. def test_portrait_unavailable_records_failure_reason():
  84. client = FlakyPortraitClient(fail_times=9) # 一直失败
  85. ctx = _ctx(client)
  86. bundles = [_bundle("a1")]
  87. ctx._inject_fifty_plus(bundles)
  88. block = bundles[0]["content_audience_50plus"]
  89. assert block["status"] == "unavailable"
  90. assert "热点宝 429" in block["failure_reason"] # 原因被记下,不再吞掉