test_policy_dispatch.py 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177
  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 JsonPolicyBundleStore, _select_dispatch
  9. RULE_PACK_JSON = Path("product_documents/规则包/douyin_rule_packs.v1.json")
  10. def test_policy_bundle_uses_content_dispatch_and_exports_runtime_contracts():
  11. bundle = JsonPolicyBundleStore().load_policy_bundle("V1")
  12. assert bundle["dispatch_id"] == "dispatch_content"
  13. assert bundle["runtime_stage"] == "V1.0"
  14. assert bundle["target_entity"] == "Content"
  15. assert bundle["content_format"] == "video"
  16. assert bundle["rule_pack_id"] == "douyin_content_discovery_rule_pack_v1"
  17. assert bundle["effect_status_mapping"]
  18. assert bundle["query_effect_aggregation"]
  19. assert bundle["runtime_status_contract"]["query_effect_status"] == [
  20. "success",
  21. "pending",
  22. "failed",
  23. "rule_blocked",
  24. ]
  25. assert bundle["policy_bundle_hash"]
  26. def test_policy_bundle_fails_when_content_dispatch_is_missing(tmp_path):
  27. root = _copy_policy_files(tmp_path)
  28. path = root / "product_documents/规则包/douyin_rule_packs.v1.json"
  29. data = json.loads(path.read_text(encoding="utf-8"))
  30. for dispatch in data["rule_pack_dispatch"]:
  31. if dispatch["dispatch_id"] == "dispatch_content":
  32. dispatch["dispatch_enabled"] = False
  33. path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
  34. with pytest.raises(ValueError, match="dispatch not found for Content/video"):
  35. JsonPolicyBundleStore(root).load_policy_bundle("V1")
  36. def test_dispatch_conflict_raises_config_error_with_rule_pack_ids(tmp_path):
  37. root = _copy_policy_files(tmp_path)
  38. path = root / "product_documents/规则包/douyin_rule_packs.v1.json"
  39. data = json.loads(path.read_text(encoding="utf-8"))
  40. duplicate = dict(data["rule_pack_dispatch"][0])
  41. duplicate["dispatch_id"] = "dispatch_content_duplicate"
  42. duplicate["priority"] = 2
  43. data["rule_pack_dispatch"].append(duplicate)
  44. path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
  45. with pytest.raises(ContentAgentError) as exc_info:
  46. JsonPolicyBundleStore(root).load_policy_bundle("V1")
  47. error = exc_info.value
  48. assert error.error_code == ErrorCode.CONFIG_RULE_PACK_DISPATCH_CONFLICT
  49. assert "douyin_content_discovery_rule_pack_v1" in error.message
  50. assert error.detail["conflict_rule_pack_ids"] == [
  51. "douyin_content_discovery_rule_pack_v1",
  52. "douyin_content_discovery_rule_pack_v1",
  53. ]
  54. def test_select_dispatch_still_returns_content_for_default_bundle():
  55. rule_package = json.loads(RULE_PACK_JSON.read_text(encoding="utf-8"))
  56. dispatch = _select_dispatch(rule_package, "V1")
  57. assert dispatch["dispatch_id"] == "dispatch_content"
  58. assert dispatch["target_entity"] == "Content"
  59. def _synthetic_author_dispatch(rule_package):
  60. # M4 砍包后无 future dispatch 行;合成一条 Author dispatch 验证选择器仍是实体参数化的。
  61. author = deepcopy(next(d for d in rule_package["rule_pack_dispatch"] if d["dispatch_id"] == "dispatch_content"))
  62. author.update(
  63. dispatch_id="dispatch_author_test",
  64. target_entity="Author",
  65. content_format="not_applicable",
  66. rule_pack_id="author_test_rule_pack_v1",
  67. dispatch_enabled=True,
  68. runtime_stage="V1.0",
  69. strategy_version="V1",
  70. )
  71. return author
  72. def test_select_dispatch_can_select_non_content_when_enabled():
  73. rule_package = deepcopy(json.loads(RULE_PACK_JSON.read_text(encoding="utf-8")))
  74. author = _synthetic_author_dispatch(rule_package)
  75. rule_package["rule_pack_dispatch"].append(author)
  76. dispatch = _select_dispatch(
  77. rule_package, "V1", target_entity="Author", content_format=author["content_format"]
  78. )
  79. assert dispatch["rule_pack_id"] == "author_test_rule_pack_v1"
  80. def test_load_policy_bundle_keeps_content_shim():
  81. bundle = JsonPolicyBundleStore().load_policy_bundle("V1")
  82. assert bundle["target_entity"] == "Content"
  83. assert bundle["rule_pack_id"] == "douyin_content_discovery_rule_pack_v1"
  84. assert bundle["rule_pack"] is bundle["rule_pack_by_entity"]["Content"]["rule_pack"]
  85. def test_load_policy_bundle_exposes_rule_pack_by_entity():
  86. bundle = JsonPolicyBundleStore().load_policy_bundle("V1")
  87. by_entity = bundle["rule_pack_by_entity"]
  88. assert set(by_entity) == {"Content"}
  89. assert by_entity["Content"]["dispatch"]["dispatch_id"] == "dispatch_content"
  90. assert by_entity["Content"]["rule_pack"]["rule_pack_id"] == bundle["rule_pack_id"]
  91. def test_enabled_author_dispatch_can_be_found_by_entity_without_replacing_content_shim(tmp_path):
  92. root = _copy_policy_files(tmp_path)
  93. path = root / "product_documents/规则包/douyin_rule_packs.v1.json"
  94. data = json.loads(path.read_text(encoding="utf-8"))
  95. data["rule_pack_dispatch"].append(_synthetic_author_dispatch(data))
  96. author_pack = deepcopy(data["rule_packs"][0])
  97. author_pack["rule_pack_id"] = "author_test_rule_pack_v1"
  98. data["rule_packs"].append(author_pack)
  99. path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
  100. bundle = JsonPolicyBundleStore(root).load_policy_bundle("V1")
  101. assert bundle["rule_pack_id"] == "douyin_content_discovery_rule_pack_v1"
  102. assert set(bundle["rule_pack_by_entity"]) == {"Content", "Author"}
  103. assert bundle["rule_pack_by_entity"]["Author"]["rule_pack"]["rule_pack_id"] == "author_test_rule_pack_v1"
  104. def test_policy_bundle_fails_when_dispatch_points_to_missing_rule_pack(tmp_path):
  105. root = _copy_policy_files(tmp_path)
  106. path = root / "product_documents/规则包/douyin_rule_packs.v1.json"
  107. data = json.loads(path.read_text(encoding="utf-8"))
  108. for dispatch in data["rule_pack_dispatch"]:
  109. if dispatch["dispatch_id"] == "dispatch_content":
  110. dispatch["rule_pack_id"] = "missing_rule_pack"
  111. path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
  112. with pytest.raises(ValueError, match="dispatch dispatch_content matched 0 enabled rule packs"):
  113. JsonPolicyBundleStore(root).load_policy_bundle("V1")
  114. def test_policy_bundle_fails_when_dispatch_points_to_disabled_rule_pack(tmp_path):
  115. root = _copy_policy_files(tmp_path)
  116. path = root / "product_documents/规则包/douyin_rule_packs.v1.json"
  117. data = json.loads(path.read_text(encoding="utf-8"))
  118. for rule_pack in data["rule_packs"]:
  119. if rule_pack["rule_pack_id"] == "douyin_content_discovery_rule_pack_v1":
  120. rule_pack["enabled"] = False
  121. path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
  122. with pytest.raises(ValueError, match="dispatch dispatch_content matched 0 enabled rule packs"):
  123. JsonPolicyBundleStore(root).load_policy_bundle("V1")
  124. def _copy_policy_files(tmp_path: Path) -> Path:
  125. root = tmp_path / "repo"
  126. (root / "product_documents/规则包").mkdir(parents=True)
  127. shutil.copy(
  128. "product_documents/规则包/douyin_rule_packs.v1.json",
  129. root / "product_documents/规则包/douyin_rule_packs.v1.json",
  130. )
  131. (root / "product_documents/抖音游走策略").mkdir(parents=True)
  132. shutil.copy(
  133. "product_documents/抖音游走策略/douyin_walk_strategy.v1.json",
  134. root / "product_documents/抖音游走策略/douyin_walk_strategy.v1.json",
  135. )
  136. return root