test_p8_strategy_review.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  1. from content_agent.business_modules import learning_review
  2. from content_agent.integrations.runtime_files import LocalRuntimeFileStore
  3. def test_strategy_review_outputs_p8_contract_with_missing_feedback(tmp_path):
  4. runtime = LocalRuntimeFileStore(tmp_path / "runtime")
  5. run_id = "run_001"
  6. policy_run_id = "policy_001"
  7. runtime.prepare_run(run_id)
  8. _write_minimal_runtime(runtime, run_id, policy_run_id)
  9. review = learning_review.run(run_id, policy_run_id, runtime)
  10. assert review["data_window"]["scope"] == "single_run"
  11. assert review["metric_summary"]["search_query_count"] == 4
  12. assert review["metric_summary"]["pooled_content_count"] == 1
  13. assert review["query_review"]["effective_queries"][0]["search_query"] == "银发旅行"
  14. assert review["query_review"]["review_queries"][0]["search_query_effect_status"] == "pending"
  15. assert review["rule_review"]["decision_distribution"]["ADD_TO_CONTENT_POOL"] == 1
  16. assert review["walk_review"]["walk_action_count"] == 1
  17. assert review["asset_review"]["content_asset_count"] == 1
  18. assert review["asset_review"]["search_clue_assets"]["promoted_count"] == 1
  19. assert review["performance_feedback"]["performance_feedback_status"] == "missing"
  20. assert review["recommendations"]
  21. assert all(item["requires_human_approval"] is True for item in review["recommendations"])
  22. written = runtime.read_json(run_id, "strategy_review.json")
  23. assert written["raw_payload"]["recommendations"] == review["recommendations"]
  24. def test_strategy_review_filters_legacy_contract_values(tmp_path):
  25. runtime = LocalRuntimeFileStore(tmp_path / "runtime")
  26. run_id = "run_legacy"
  27. policy_run_id = "policy_legacy"
  28. runtime.prepare_run(run_id)
  29. _write_minimal_runtime(runtime, run_id, policy_run_id)
  30. runtime.append_jsonl(
  31. run_id,
  32. "search_clues.jsonl",
  33. [
  34. {
  35. "run_id": run_id,
  36. "policy_run_id": policy_run_id,
  37. "clue_id": "clue_old",
  38. "search_query_id": "q_old",
  39. "search_query": "旧口径",
  40. "search_query_effect_status": "weak_" + "effective",
  41. "result_count": 10,
  42. "pooled_content_count": 10,
  43. "review_content_count": 0,
  44. "pending_content_count": 0,
  45. "rejected_content_count": 0,
  46. "raw_payload": {},
  47. }
  48. ],
  49. )
  50. runtime.append_jsonl(
  51. run_id,
  52. "rule_decisions.jsonl",
  53. [
  54. {
  55. "run_id": run_id,
  56. "policy_run_id": policy_run_id,
  57. "decision_id": "decision_old",
  58. "decision_target_type": "content",
  59. "decision_target_id": "content_old",
  60. "decision_action": "HOLD_CONTENT_" + "PENDING",
  61. "decision_reason_code": "old_status",
  62. "search_query_effect_status": "block" + "ed",
  63. "raw_payload": {},
  64. }
  65. ],
  66. )
  67. review = learning_review.run(run_id, policy_run_id, runtime)
  68. assert "weak_" + "effective" not in {
  69. item["search_query_effect_status"]
  70. for bucket in review["query_review"].values()
  71. if isinstance(bucket, list)
  72. for item in bucket
  73. }
  74. assert "HOLD_CONTENT_" + "PENDING" not in review["rule_review"]["decision_distribution"]
  75. def _write_minimal_runtime(runtime, run_id: str, policy_run_id: str) -> None:
  76. runtime.write_json(
  77. run_id,
  78. "final_output.json",
  79. {
  80. "schema_version": "runtime_record.v1",
  81. "run_id": run_id,
  82. "policy_run_id": policy_run_id,
  83. "policy": {
  84. "strategy_version": "V1",
  85. "policy_bundle_id": "douyin_policy_bundle_v1",
  86. "rule_pack_id": "douyin_content_rules_v1",
  87. "rule_pack_version": "V1",
  88. },
  89. "walk_strategy": {"walk_strategy_version": "V1.0"},
  90. "content_assets": [
  91. {
  92. "content_asset_id": "asset_001",
  93. "platform": "douyin",
  94. "platform_content_id": "content_001",
  95. "source_path_record_ids": ["path_001"],
  96. "decision_ids": ["decision_001"],
  97. }
  98. ],
  99. "author_assets": [{"author_asset_id": "author_001"}],
  100. "summary": {
  101. "search_query_count": 4,
  102. "discovered_content_count": 3,
  103. "pooled_content_count": 1,
  104. "review_content_count": 1,
  105. "rejected_content_count": 1,
  106. },
  107. },
  108. )
  109. runtime.append_jsonl(
  110. run_id,
  111. "search_clues.jsonl",
  112. [
  113. _clue(run_id, policy_run_id, "clue_001", "q_001", "银发旅行", "success", 1),
  114. _clue(run_id, policy_run_id, "clue_002", "q_002", "怀旧故事", "pending", 0),
  115. _clue(run_id, policy_run_id, "clue_003", "q_003", "广场舞", "failed", 0),
  116. _clue(run_id, policy_run_id, "clue_004", "q_004", "保健品", "rule_blocked", 0),
  117. ],
  118. )
  119. runtime.append_jsonl(
  120. run_id,
  121. "rule_decisions.jsonl",
  122. [
  123. _decision(
  124. run_id,
  125. policy_run_id,
  126. "decision_001",
  127. "content_001",
  128. "ADD_TO_CONTENT_POOL",
  129. "passed",
  130. "success",
  131. search_query_id="q_001",
  132. ),
  133. _decision(run_id, policy_run_id, "decision_002", "content_002", "KEEP_CONTENT_FOR_REVIEW", "needs_more_evidence", "pending"),
  134. _decision(run_id, policy_run_id, "decision_003", "content_003", "REJECT_CONTENT", "missing_content_portrait", "failed"),
  135. ],
  136. )
  137. runtime.append_jsonl(
  138. run_id,
  139. "discovered_content_items.jsonl",
  140. [
  141. {
  142. "run_id": run_id,
  143. "policy_run_id": policy_run_id,
  144. "content_discovery_id": "discovery_001",
  145. "search_query_id": "q_001",
  146. "platform": "douyin",
  147. "platform_content_id": "content_001",
  148. "raw_payload": {},
  149. }
  150. ],
  151. )
  152. runtime.append_jsonl(
  153. run_id,
  154. "source_path_records.jsonl",
  155. [
  156. {
  157. "run_id": run_id,
  158. "policy_run_id": policy_run_id,
  159. "source_path_record_id": "path_001",
  160. "source_path_type": "decision_to_asset",
  161. "from_node_type": "rule_decision",
  162. "from_node_id": "decision_001",
  163. "to_node_type": "content_asset",
  164. "to_node_id": "asset_001",
  165. "decision_id": "decision_001",
  166. "raw_payload": {},
  167. }
  168. ],
  169. )
  170. runtime.append_jsonl(
  171. run_id,
  172. "walk_actions.jsonl",
  173. [
  174. {
  175. "run_id": run_id,
  176. "policy_run_id": policy_run_id,
  177. "walk_action_id": "wa_001",
  178. "edge_id": "query_next_page",
  179. "walk_action": "fetch_next_page",
  180. "walk_status": "success",
  181. "raw_payload": {},
  182. }
  183. ],
  184. )
  185. def _clue(run_id, policy_run_id, clue_id, query_id, query, status, pooled_count):
  186. return {
  187. "run_id": run_id,
  188. "policy_run_id": policy_run_id,
  189. "clue_id": clue_id,
  190. "search_query_id": query_id,
  191. "search_query": query,
  192. "search_query_effect_status": status,
  193. "result_count": 1,
  194. "pooled_content_count": pooled_count,
  195. "review_content_count": 1 if status == "pending" else 0,
  196. "pending_content_count": 1 if status == "pending" else 0,
  197. "rejected_content_count": 1 if status in {"failed", "rule_blocked"} else 0,
  198. "raw_payload": {},
  199. }
  200. def _decision(
  201. run_id,
  202. policy_run_id,
  203. decision_id,
  204. target_id,
  205. action,
  206. reason,
  207. effect_status,
  208. search_query_id=None,
  209. ):
  210. return {
  211. "run_id": run_id,
  212. "policy_run_id": policy_run_id,
  213. "decision_id": decision_id,
  214. "decision_target_type": "content",
  215. "decision_target_id": target_id,
  216. "decision_action": action,
  217. "decision_reason_code": reason,
  218. "search_query_effect_status": effect_status,
  219. "source_evidence": {"search_query_id": search_query_id} if search_query_id else {},
  220. "raw_payload": {},
  221. }