test_case_replay.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
  1. """Real + synthetic case replay tests (V2-M0D).
  2. - real_id45: the harvested production baseline (demand_content.id=45). V4-M3 受控变化:
  3. 画像门槛整体退役,改由 Gemini query relevance + 平台可观测表现 50/50 打分。
  4. - syn_pool / syn_review: synthetic corpora (authored with high/low engagement)
  5. exercise the ADD / KEEP paths via the same V4 scorecard.
  6. Snapshots lock the deterministic replay output; regenerate with UPDATE_SNAPSHOTS=1.
  7. """
  8. from __future__ import annotations
  9. import copy
  10. import json
  11. from collections import Counter
  12. from pathlib import Path
  13. from typing import Any
  14. from tests.replay_harness import CASES_DIR, replay_case
  15. _SUMMARY_KEYS = [
  16. "pooled_content_count",
  17. "review_content_count",
  18. "rejected_content_count",
  19. "pending_content_count",
  20. ]
  21. def _decision_counts(artifacts) -> dict[str, int]:
  22. return dict(Counter(d.get("decision_action") for d in artifacts.decisions))
  23. def _build_synthetic_corpus(cases_dir: Path, case_id: str, items: list[dict[str, Any]]) -> None:
  24. """Author a minimal corpus: real (validated) source_context + given items."""
  25. source_context = json.loads(
  26. (CASES_DIR / "real_id45" / "input" / "source_context.json").read_text(encoding="utf-8")
  27. )
  28. dest = cases_dir / case_id / "input"
  29. dest.mkdir(parents=True, exist_ok=True)
  30. (dest / "source_context.json").write_text(
  31. json.dumps(source_context, ensure_ascii=False, indent=2), encoding="utf-8"
  32. )
  33. (dest / "discovered_content_items.jsonl").write_text(
  34. json.dumps(items, ensure_ascii=False, indent=2), encoding="utf-8"
  35. )
  36. def _synthetic_item(
  37. content_id: str,
  38. *,
  39. digg: int,
  40. comment: int = 800,
  41. share: int = 600,
  42. collect: int = 5000,
  43. ) -> dict[str, Any]:
  44. return {
  45. "content_discovery_id": f"syn_{content_id}",
  46. "search_query_id": "q_001",
  47. "platform": "douyin",
  48. "platform_content_id": content_id,
  49. "platform_content_format": "video",
  50. "description": "中医养生合成内容",
  51. "platform_author_id": "syn_author",
  52. "author_display_name": "养生作者",
  53. "statistics": {
  54. "digg_count": digg,
  55. "comment_count": comment,
  56. "share_count": share,
  57. "collect_count": collect,
  58. },
  59. "tags": ["#中医养生"],
  60. "score": 85,
  61. "risk_level": "low",
  62. "availability": "available",
  63. "discovery_start_source": "pattern_itemset",
  64. "previous_discovery_step": "search_query_direct",
  65. "content_metadata_source": "synthetic",
  66. }
  67. def test_replay_id45_baseline_gemini_score(tmp_path):
  68. artifacts = replay_case("real_id45", runtime_root=tmp_path / "rt")
  69. assert artifacts.state["status"] == "success"
  70. # M11 re-baseline:平台分改"量级+收缩比例"后,7406990358799732018(中等比例)平台分 60.5、
  71. # 总分 70.25 ≥ 70 → 由原"待复看"升为"入池"(allow_walk=False);7577667864522907506 比例极低
  72. # (官方大号式)平台分 25.1 → 仍淘汰。结果:3 入池 / 0 复看 / 1 淘汰。
  73. assert artifacts.summary["pooled_content_count"] == 3
  74. assert artifacts.summary["review_content_count"] == 0
  75. assert artifacts.summary["rejected_content_count"] == 1
  76. assert artifacts.summary["pending_content_count"] == 0
  77. assert _decision_counts(artifacts) == {
  78. "ADD_TO_CONTENT_POOL": 3,
  79. "REJECT_CONTENT": 1,
  80. }
  81. assert {d.get("decision_reason_code") for d in artifacts.decisions} == {
  82. "v4_query_and_platform_pass",
  83. "v4_query_or_score_below_threshold",
  84. }
  85. def test_replay_synthetic_pool_case(tmp_path):
  86. # M11:平台分改"收缩后比例"后,光大点赞不入池;高共鸣占比(转/赞0.3、藏/赞0.5、评/赞0.08)
  87. # 才是该入池的画像 → 给强比例,平台分 ~88、总分 ~84 ≥ 70 入池。
  88. _build_synthetic_corpus(
  89. tmp_path / "cases",
  90. "syn_pool",
  91. [
  92. _synthetic_item(
  93. "9000000000000000001",
  94. digg=100_000,
  95. comment=8_000,
  96. share=30_000,
  97. collect=50_000,
  98. )
  99. ],
  100. )
  101. artifacts = replay_case("syn_pool", runtime_root=tmp_path / "rt", cases_dir=tmp_path / "cases")
  102. assert artifacts.state["status"] == "success"
  103. assert artifacts.summary["pooled_content_count"] >= 1
  104. assert artifacts.summary["rejected_content_count"] == 0
  105. def test_replay_synthetic_review_case(tmp_path):
  106. _build_synthetic_corpus(
  107. tmp_path / "cases",
  108. "syn_review",
  109. [_synthetic_item("9000000000000000002", digg=50_000)],
  110. )
  111. artifacts = replay_case("syn_review", runtime_root=tmp_path / "rt", cases_dir=tmp_path / "cases")
  112. assert artifacts.state["status"] == "success"
  113. assert artifacts.summary["review_content_count"] >= 1
  114. assert artifacts.summary["pooled_content_count"] == 0
  115. def test_replay_id45_walk_obeys_decisions_after_m4(tmp_path):
  116. artifacts = replay_case("real_id45", runtime_root=tmp_path / "rt")
  117. walk_actions = artifacts.files["walk_actions.jsonl"]
  118. # M8:query_next_page 边已删,翻页归渐进筛选;游走只剩 tag/author。tag 预算 3→5。
  119. assert not [row for row in walk_actions if row["edge_id"] == "query_next_page"]
  120. tag_actions = [row for row in walk_actions if row["edge_id"] == "hashtag_to_query"]
  121. executed_tags = [row for row in tag_actions if row["walk_status"] == "success"]
  122. skipped_tags = [row for row in tag_actions if row["walk_status"] == "skipped"]
  123. assert executed_tags
  124. assert all(row["budget_tier"] == "normal" for row in executed_tags)
  125. assert sorted(row["reason_code"] for row in skipped_tags) == [
  126. "v4_allow_walk_denied",
  127. "v4_allow_walk_denied",
  128. ]
  129. assert len(executed_tags) == 5
  130. author_actions = [row for row in walk_actions if row["edge_id"] == "author_to_works"]
  131. assert author_actions
  132. assert all(row["walk_status"] == "success" for row in author_actions)
  133. assert all(row["budget_tier"] == "normal" for row in author_actions)
  134. # M11 re-baseline:新平台分下原"待复看"的 7406990358799732018 升为"入池"(allow_walk=False),
  135. # 已无 KEEP 内容触发预算降级 → budget_downgrade 边为空。
  136. downgrades = [row for row in walk_actions if row["edge_id"] == "budget_downgrade"]
  137. assert len(downgrades) == 0
  138. for row in walk_actions:
  139. execution = row["raw_payload"]["rule_pack_execution"]
  140. assert execution["executed"] is True
  141. assert execution["executed_rule_pack_id"] == "douyin_content_discovery_rule_pack_v1"