test_portrait_cache.py 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  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_does_not_use_search_or_portrait_limiter():
  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 == 0
  52. assert client.calls == ["a1", "a2"]
  53. def test_portrait_fetched_when_query_passes_regardless_of_platform():
  54. # 修复:门只看 query 相关性,平台分再低也拉画像、算适老性(原来 platform<65 会跳过)。
  55. client = CountingPortraitClient()
  56. ctx = _ctx(client)
  57. bundles = [_bundle("a1", q=70, p=20)] # 平台仅 20
  58. ctx._inject_fifty_plus(bundles)
  59. assert client.calls == ["a1"]
  60. assert bundles[0]["content_audience_50plus"]["status"] == "ok"
  61. def test_portrait_skipped_when_query_below_gate():
  62. client = CountingPortraitClient()
  63. ctx = _ctx(client)
  64. bundles = [_bundle("a1", q=60, p=90)] # 相关性 60 < 65 → 不拉
  65. ctx._inject_fifty_plus(bundles)
  66. assert client.calls == []
  67. assert bundles[0]["content_audience_50plus"]["status"] == "not_attempted"
  68. class FlakyPortraitClient:
  69. def __init__(self, fail_times):
  70. self.fail_times = fail_times
  71. self.calls = 0
  72. def fetch_account_fans_portrait(self, account_id):
  73. self.calls += 1
  74. if self.calls <= self.fail_times:
  75. raise RuntimeError("热点宝 429")
  76. return {"fans": {"age": {"data": {"50-": {"percentage": "40%", "preference": "150"}}}}}
  77. def test_portrait_retry_recovers_transient_failure():
  78. client = FlakyPortraitClient(fail_times=1) # 第一次失败、第二次成功
  79. ctx = _ctx(client)
  80. bundles = [_bundle("a1")]
  81. ctx._inject_fifty_plus(bundles)
  82. assert client.calls == 2 # 重试了一次
  83. assert bundles[0]["content_audience_50plus"]["status"] == "ok"
  84. def test_portrait_unavailable_records_failure_reason():
  85. client = FlakyPortraitClient(fail_times=9) # 一直失败
  86. ctx = _ctx(client)
  87. bundles = [_bundle("a1")]
  88. ctx._inject_fifty_plus(bundles)
  89. block = bundles[0]["content_audience_50plus"]
  90. assert block["status"] == "unavailable"
  91. assert "热点宝 429" in block["failure_reason"] # 原因被记下,不再吞掉