test_policy_dispatch.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. from __future__ import annotations
  2. import json
  3. import shutil
  4. from copy import deepcopy
  5. from pathlib import Path
  6. import pytest
  7. from content_agent.errors import ContentAgentError, ErrorCode
  8. from content_agent.integrations.policy_json import (
  9. JsonPolicyBundleStore,
  10. _build_rule_pack_by_entity,
  11. _select_dispatch,
  12. )
  13. RULE_PACK_JSON = Path("product_documents/规则包/douyin_rule_packs.v1.json")
  14. EDGE_CATALOG_JSON = Path("tech_documents/数据接口与来源/walk_edge_catalog.json")
  15. def test_policy_bundle_uses_content_dispatch_and_exports_runtime_contracts():
  16. bundle = JsonPolicyBundleStore().load_policy_bundle("V4")
  17. assert bundle["dispatch_id"] == "dispatch_content"
  18. assert bundle["runtime_stage"] == "V1.0"
  19. assert bundle["target_entity"] == "Content"
  20. assert bundle["content_format"] == "video"
  21. assert bundle["rule_pack_id"] == "douyin_content_discovery_rule_pack_v1"
  22. assert bundle["effect_status_mapping"]
  23. assert bundle["query_effect_aggregation"]
  24. assert bundle["runtime_status_contract"]["query_effect_status"] == [
  25. "success",
  26. "pending",
  27. "failed",
  28. "rule_blocked",
  29. ]
  30. assert bundle["policy_bundle_hash"]
  31. def test_policy_bundle_fails_when_content_dispatch_is_missing(tmp_path):
  32. root = _copy_policy_files(tmp_path)
  33. path = root / "product_documents/规则包/douyin_rule_packs.v1.json"
  34. data = json.loads(path.read_text(encoding="utf-8"))
  35. for dispatch in data["rule_pack_dispatch"]:
  36. if dispatch["dispatch_id"] == "dispatch_content":
  37. dispatch["dispatch_enabled"] = False
  38. path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
  39. with pytest.raises(ValueError, match="dispatch not found for Content/video"):
  40. JsonPolicyBundleStore(root).load_policy_bundle("V4")
  41. def test_dispatch_conflict_raises_config_error_with_rule_pack_ids(tmp_path):
  42. root = _copy_policy_files(tmp_path)
  43. path = root / "product_documents/规则包/douyin_rule_packs.v1.json"
  44. data = json.loads(path.read_text(encoding="utf-8"))
  45. duplicate = dict(data["rule_pack_dispatch"][0])
  46. duplicate["dispatch_id"] = "dispatch_content_duplicate"
  47. duplicate["priority"] = 2
  48. data["rule_pack_dispatch"].append(duplicate)
  49. path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
  50. with pytest.raises(ContentAgentError) as exc_info:
  51. JsonPolicyBundleStore(root).load_policy_bundle("V4")
  52. error = exc_info.value
  53. assert error.error_code == ErrorCode.CONFIG_RULE_PACK_DISPATCH_CONFLICT
  54. assert "douyin_content_discovery_rule_pack_v1" in error.message
  55. assert error.detail["conflict_rule_pack_ids"] == [
  56. "douyin_content_discovery_rule_pack_v1",
  57. "douyin_content_discovery_rule_pack_v1",
  58. ]
  59. def test_select_dispatch_still_returns_content_for_default_bundle():
  60. rule_package = json.loads(RULE_PACK_JSON.read_text(encoding="utf-8"))
  61. dispatch = _select_dispatch(rule_package, "V4")
  62. assert dispatch["dispatch_id"] == "dispatch_content"
  63. assert dispatch["target_entity"] == "Content"
  64. def test_policy_bundle_accepts_explicit_douyin_platform():
  65. default_bundle = JsonPolicyBundleStore().load_policy_bundle("V4")
  66. explicit_bundle = JsonPolicyBundleStore().load_policy_bundle("V4", platform="douyin")
  67. assert explicit_bundle["dispatch_id"] == default_bundle["dispatch_id"]
  68. assert explicit_bundle["rule_pack_id"] == default_bundle["rule_pack_id"]
  69. assert explicit_bundle["platform"] == "douyin"
  70. assert explicit_bundle["rule_pack_by_entity"] == default_bundle["rule_pack_by_entity"]
  71. @pytest.mark.parametrize(
  72. ("platform", "dispatch_id"),
  73. [
  74. ("douyin", "dispatch_content"),
  75. ("kuaishou", "dispatch_content_kuaishou"),
  76. ("shipinhao", "dispatch_content_shipinhao"),
  77. ],
  78. )
  79. def test_policy_bundle_loads_real_three_platform_dispatches(platform, dispatch_id):
  80. bundle = JsonPolicyBundleStore().load_policy_bundle("V4", platform=platform)
  81. assert bundle["dispatch_id"] == dispatch_id
  82. assert bundle["dispatch"]["platform"] == platform
  83. assert bundle["rule_pack_id"] == "douyin_content_discovery_rule_pack_v1"
  84. assert bundle["rule_pack_by_entity"]["Content"]["dispatch"]["platform"] == platform
  85. def test_select_dispatch_filters_by_platform():
  86. rule_package = json.loads(RULE_PACK_JSON.read_text(encoding="utf-8"))
  87. dispatch = _select_dispatch(rule_package, "V4", platform="kuaishou")
  88. assert dispatch["dispatch_id"] == "dispatch_content_kuaishou"
  89. assert dispatch["platform"] == "kuaishou"
  90. def test_rule_pack_by_entity_filters_by_platform():
  91. rule_package = json.loads(RULE_PACK_JSON.read_text(encoding="utf-8"))
  92. by_entity = _build_rule_pack_by_entity(rule_package, "V4", platform="kuaishou")
  93. assert set(by_entity) == {"Content"}
  94. assert by_entity["Content"]["dispatch"]["platform"] == "kuaishou"
  95. def test_policy_bundle_fails_for_unknown_platform_without_dispatch():
  96. with pytest.raises(ValueError, match="dispatch not found for Content/video"):
  97. JsonPolicyBundleStore().load_policy_bundle("V4", platform="unknown")
  98. def test_dispatch_conflict_ignores_other_platforms(tmp_path):
  99. root = _copy_policy_files(tmp_path)
  100. path = root / "product_documents/规则包/douyin_rule_packs.v1.json"
  101. data = json.loads(path.read_text(encoding="utf-8"))
  102. data["rule_pack_dispatch"].append(_synthetic_platform_dispatch(data, "xiaohongshu"))
  103. path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
  104. bundle = JsonPolicyBundleStore(root).load_policy_bundle("V4", platform="douyin")
  105. assert bundle["dispatch_id"] == "dispatch_content"
  106. assert bundle["platform"] == "douyin"
  107. def _synthetic_author_dispatch(rule_package):
  108. # M4 砍包后无 future dispatch 行;合成一条 Author dispatch 验证选择器仍是实体参数化的。
  109. author = deepcopy(next(d for d in rule_package["rule_pack_dispatch"] if d["dispatch_id"] == "dispatch_content"))
  110. author.update(
  111. dispatch_id="dispatch_author_test",
  112. target_entity="Author",
  113. content_format="not_applicable",
  114. rule_pack_id="author_test_rule_pack_v1",
  115. dispatch_enabled=True,
  116. runtime_stage="V1.0",
  117. strategy_version="V4",
  118. )
  119. return author
  120. def _synthetic_platform_dispatch(rule_package, platform):
  121. dispatch = deepcopy(next(d for d in rule_package["rule_pack_dispatch"] if d["dispatch_id"] == "dispatch_content"))
  122. dispatch.update(
  123. dispatch_id=f"dispatch_content_{platform}_test",
  124. platform=platform,
  125. dispatch_enabled=True,
  126. runtime_stage="V1.0",
  127. strategy_version="V4",
  128. )
  129. return dispatch
  130. def test_select_dispatch_can_select_non_content_when_enabled():
  131. rule_package = deepcopy(json.loads(RULE_PACK_JSON.read_text(encoding="utf-8")))
  132. author = _synthetic_author_dispatch(rule_package)
  133. rule_package["rule_pack_dispatch"].append(author)
  134. dispatch = _select_dispatch(
  135. rule_package, "V4", target_entity="Author", content_format=author["content_format"]
  136. )
  137. assert dispatch["rule_pack_id"] == "author_test_rule_pack_v1"
  138. def test_load_policy_bundle_keeps_content_shim():
  139. bundle = JsonPolicyBundleStore().load_policy_bundle("V4")
  140. assert bundle["target_entity"] == "Content"
  141. assert bundle["rule_pack_id"] == "douyin_content_discovery_rule_pack_v1"
  142. assert bundle["rule_pack"] is bundle["rule_pack_by_entity"]["Content"]["rule_pack"]
  143. def test_load_policy_bundle_exposes_rule_pack_by_entity():
  144. bundle = JsonPolicyBundleStore().load_policy_bundle("V4")
  145. by_entity = bundle["rule_pack_by_entity"]
  146. assert set(by_entity) == {"Content"}
  147. assert by_entity["Content"]["dispatch"]["dispatch_id"] == "dispatch_content"
  148. assert by_entity["Content"]["rule_pack"]["rule_pack_id"] == bundle["rule_pack_id"]
  149. def test_enabled_author_dispatch_can_be_found_by_entity_without_replacing_content_shim(tmp_path):
  150. root = _copy_policy_files(tmp_path)
  151. path = root / "product_documents/规则包/douyin_rule_packs.v1.json"
  152. data = json.loads(path.read_text(encoding="utf-8"))
  153. data["rule_pack_dispatch"].append(_synthetic_author_dispatch(data))
  154. author_pack = deepcopy(data["rule_packs"][0])
  155. author_pack["rule_pack_id"] = "author_test_rule_pack_v1"
  156. data["rule_packs"].append(author_pack)
  157. path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
  158. bundle = JsonPolicyBundleStore(root).load_policy_bundle("V4")
  159. assert bundle["rule_pack_id"] == "douyin_content_discovery_rule_pack_v1"
  160. assert set(bundle["rule_pack_by_entity"]) == {"Content", "Author"}
  161. assert bundle["rule_pack_by_entity"]["Author"]["rule_pack"]["rule_pack_id"] == "author_test_rule_pack_v1"
  162. def test_policy_bundle_fails_when_dispatch_points_to_missing_rule_pack(tmp_path):
  163. root = _copy_policy_files(tmp_path)
  164. path = root / "product_documents/规则包/douyin_rule_packs.v1.json"
  165. data = json.loads(path.read_text(encoding="utf-8"))
  166. for dispatch in data["rule_pack_dispatch"]:
  167. if dispatch["dispatch_id"] == "dispatch_content":
  168. dispatch["rule_pack_id"] = "missing_rule_pack"
  169. path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
  170. with pytest.raises(ValueError, match="dispatch dispatch_content matched 0 enabled rule packs"):
  171. JsonPolicyBundleStore(root).load_policy_bundle("V4")
  172. def test_policy_bundle_fails_when_dispatch_points_to_disabled_rule_pack(tmp_path):
  173. root = _copy_policy_files(tmp_path)
  174. path = root / "product_documents/规则包/douyin_rule_packs.v1.json"
  175. data = json.loads(path.read_text(encoding="utf-8"))
  176. for rule_pack in data["rule_packs"]:
  177. if rule_pack["rule_pack_id"] == "douyin_content_discovery_rule_pack_v1":
  178. rule_pack["enabled"] = False
  179. path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
  180. with pytest.raises(ValueError, match="dispatch dispatch_content matched 0 enabled rule packs"):
  181. JsonPolicyBundleStore(root).load_policy_bundle("V4")
  182. def _copy_policy_files(tmp_path: Path) -> Path:
  183. root = tmp_path / "repo"
  184. (root / "product_documents/规则包").mkdir(parents=True)
  185. shutil.copy(
  186. "product_documents/规则包/douyin_rule_packs.v1.json",
  187. root / "product_documents/规则包/douyin_rule_packs.v1.json",
  188. )
  189. (root / "product_documents/抖音游走策略").mkdir(parents=True)
  190. shutil.copy(
  191. "product_documents/抖音游走策略/douyin_walk_strategy.v1.json",
  192. root / "product_documents/抖音游走策略/douyin_walk_strategy.v1.json",
  193. )
  194. (root / EDGE_CATALOG_JSON).parent.mkdir(parents=True)
  195. shutil.copy(EDGE_CATALOG_JSON, root / EDGE_CATALOG_JSON)
  196. return root