test_pattern_recall_decision.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. import copy
  2. from content_agent.business_modules.content_discovery.content_discovery_builder import run as build_content
  3. from content_agent.business_modules.content_discovery.pattern_recall.recall_decision import run
  4. from content_agent.integrations.runtime_files import LocalRuntimeFileStore
  5. from tests.p1_helpers import real_source_payload
  6. from tests.p4_helpers import (
  7. FailingCategoryMatchClient,
  8. FailingDecodeClient,
  9. FakeCategoryMatchClient,
  10. FakeDecodeClient,
  11. fake_decode_bad_shape,
  12. fake_decode_pending,
  13. fake_decode_success,
  14. fake_match_paths_hit,
  15. fake_match_paths_no_hit,
  16. )
  17. def _build_state(tmp_path):
  18. runtime = LocalRuntimeFileStore(tmp_path / "runtime")
  19. runtime.prepare_run("run_001")
  20. source_context = real_source_payload()
  21. platform_results = [
  22. {
  23. "content_discovery_id": "content_001",
  24. "search_query_id": "q_001",
  25. "platform": "douyin",
  26. "platform_content_id": "7390000000000000000",
  27. "platform_content_format": "video",
  28. "description": "爱国情感类人物故事观察",
  29. "platform_author_id": "author_001",
  30. "author_display_name": "作者",
  31. "statistics": {"digg_count": 1},
  32. "tags": ["#人物故事"],
  33. "score": 72,
  34. "portrait_available": True,
  35. "age_50_plus_level": "medium",
  36. "risk_level": "low",
  37. "discovery_relation": "derived_from_pattern_demand",
  38. "discovery_start_source": "pattern_itemset",
  39. "previous_discovery_step": "search_query_direct",
  40. "play_url": "https://video.example/a.mp4",
  41. }
  42. ]
  43. built = build_content("run_001", "policy_001", platform_results, source_context, runtime)
  44. pattern_seed_pack = {
  45. "seed_terms": ["爱国情感", "人物故事"],
  46. "category_bindings": [
  47. {"category_path": "/理念/观念/个人观念/情感认同/国家民族认同/爱国情感"}
  48. ],
  49. }
  50. return runtime, source_context, pattern_seed_pack, built
  51. def test_recall_decision_matched_writes_evidence_and_updates_bundle(tmp_path):
  52. runtime, source_context, pattern_seed_pack, built = _build_state(tmp_path)
  53. result = run(
  54. "run_001",
  55. "policy_001",
  56. built["discovered_content_items"],
  57. built["content_media_records"],
  58. built["evidence_bundles"],
  59. source_context,
  60. pattern_seed_pack,
  61. runtime,
  62. FakeDecodeClient(fake_decode_success()),
  63. FakeCategoryMatchClient(),
  64. poll_interval_seconds=0,
  65. )
  66. evidence = result["pattern_recall_evidence"][0]
  67. bundle = result["evidence_bundles"][0]
  68. item = result["discovered_content_items"][0]
  69. assert evidence["recall_status"] == "matched"
  70. assert evidence["matched_terms"]
  71. assert evidence["matched_category_paths"]
  72. assert bundle["pattern_match_result"]["pattern_recall"] == "matched"
  73. assert bundle["pattern_match_result"]["score"] == 72
  74. assert item["pattern_match_result"]["pattern_recall_evidence_id"] == "recall_001"
  75. def test_recall_decision_pending_does_not_fail_or_match(tmp_path):
  76. runtime, source_context, pattern_seed_pack, built = _build_state(tmp_path)
  77. result = run(
  78. "run_001",
  79. "policy_001",
  80. built["discovered_content_items"],
  81. built["content_media_records"],
  82. built["evidence_bundles"],
  83. source_context,
  84. pattern_seed_pack,
  85. runtime,
  86. FakeDecodeClient(fake_decode_pending()),
  87. FakeCategoryMatchClient(),
  88. max_wait_seconds=0,
  89. poll_interval_seconds=0,
  90. )
  91. assert result["pattern_recall_evidence"][0]["recall_status"] == "pending"
  92. assert result["evidence_bundles"][0]["pattern_match_result"]["pattern_recall"] == (
  93. "pattern_recall_pending"
  94. )
  95. def test_recall_decision_bad_decode_is_rejected(tmp_path):
  96. runtime, source_context, pattern_seed_pack, built = _build_state(tmp_path)
  97. result = run(
  98. "run_001",
  99. "policy_001",
  100. built["discovered_content_items"],
  101. built["content_media_records"],
  102. built["evidence_bundles"],
  103. source_context,
  104. pattern_seed_pack,
  105. runtime,
  106. FakeDecodeClient(fake_decode_bad_shape()),
  107. FakeCategoryMatchClient(),
  108. poll_interval_seconds=0,
  109. )
  110. assert result["pattern_recall_evidence"][0]["recall_status"] == "rejected"
  111. def test_recall_decision_decode_client_error_is_content_failed(tmp_path):
  112. runtime, source_context, pattern_seed_pack, built = _build_state(tmp_path)
  113. result = run(
  114. "run_001",
  115. "policy_001",
  116. built["discovered_content_items"],
  117. built["content_media_records"],
  118. built["evidence_bundles"],
  119. source_context,
  120. pattern_seed_pack,
  121. runtime,
  122. FailingDecodeClient(),
  123. FakeCategoryMatchClient(),
  124. poll_interval_seconds=0,
  125. )
  126. evidence = result["pattern_recall_evidence"][0]
  127. assert evidence["recall_status"] == "failed"
  128. assert evidence["evidence_summary"]["failure_reason"] == "decode_client_error"
  129. assert result["evidence_bundles"][0]["pattern_match_result"]["pattern_recall"] == (
  130. "pattern_recall_failed"
  131. )
  132. def test_recall_decision_category_client_error_is_content_failed(tmp_path):
  133. runtime, source_context, pattern_seed_pack, built = _build_state(tmp_path)
  134. result = run(
  135. "run_001",
  136. "policy_001",
  137. built["discovered_content_items"],
  138. built["content_media_records"],
  139. built["evidence_bundles"],
  140. source_context,
  141. pattern_seed_pack,
  142. runtime,
  143. FakeDecodeClient(fake_decode_success()),
  144. FailingCategoryMatchClient(),
  145. poll_interval_seconds=0,
  146. )
  147. evidence = result["pattern_recall_evidence"][0]
  148. assert evidence["recall_status"] == "failed"
  149. assert evidence["evidence_summary"]["failure_reason"] == "category_match_client_error"
  150. def test_recall_decision_no_match_when_category_has_no_hit(tmp_path):
  151. runtime, source_context, pattern_seed_pack, built = _build_state(tmp_path)
  152. result = run(
  153. "run_001",
  154. "policy_001",
  155. built["discovered_content_items"],
  156. built["content_media_records"],
  157. built["evidence_bundles"],
  158. source_context,
  159. pattern_seed_pack,
  160. runtime,
  161. FakeDecodeClient(fake_decode_success(terms=["不相关主题"])),
  162. FakeCategoryMatchClient(fake_match_paths_no_hit()),
  163. poll_interval_seconds=0,
  164. )
  165. assert result["pattern_recall_evidence"][0]["recall_status"] == "no_match"
  166. def test_recall_decision_preserves_upstream_source_evidence(tmp_path):
  167. runtime, source_context, pattern_seed_pack, built = _build_state(tmp_path)
  168. before = copy.deepcopy(built["evidence_bundles"][0]["source_evidence"])
  169. result = run(
  170. "run_001",
  171. "policy_001",
  172. built["discovered_content_items"],
  173. built["content_media_records"],
  174. built["evidence_bundles"],
  175. source_context,
  176. pattern_seed_pack,
  177. runtime,
  178. FakeDecodeClient(fake_decode_success()),
  179. FakeCategoryMatchClient(
  180. fake_match_paths_hit(
  181. paths=[
  182. "/理念/观念/个人观念/情感认同/国家民族认同/爱国情感",
  183. "/理念/观念/个人观念/情感认同/国家民族认同/人物故事",
  184. ]
  185. )
  186. ),
  187. poll_interval_seconds=0,
  188. )
  189. after = result["evidence_bundles"][0]["source_evidence"]
  190. assert after == before
  191. evidence = result["pattern_recall_evidence"][0]
  192. assert len(evidence["matched_category_paths"]) == 2
  193. assert evidence["evidence_summary"]["primary_matched_category_path"]