from __future__ import annotations import json import shutil from copy import deepcopy from pathlib import Path import pytest from content_agent.errors import ContentAgentError, ErrorCode from content_agent.integrations.policy_json import ( JsonPolicyBundleStore, _build_rule_pack_by_entity, _select_dispatch, ) RULE_PACK_JSON = Path("product_documents/规则包/douyin_rule_packs.v1.json") EDGE_CATALOG_JSON = Path("tech_documents/数据接口与来源/walk_edge_catalog.json") def test_policy_bundle_uses_content_dispatch_and_exports_runtime_contracts(): bundle = JsonPolicyBundleStore().load_policy_bundle("V4") assert bundle["dispatch_id"] == "dispatch_content" assert bundle["runtime_stage"] == "V1.0" assert bundle["target_entity"] == "Content" assert bundle["content_format"] == "video" assert bundle["rule_pack_id"] == "douyin_content_discovery_rule_pack_v1" assert bundle["effect_status_mapping"] assert bundle["query_effect_aggregation"] assert bundle["runtime_status_contract"]["query_effect_status"] == [ "success", "pending", "failed", "rule_blocked", ] assert bundle["policy_bundle_hash"] def test_policy_bundle_fails_when_content_dispatch_is_missing(tmp_path): root = _copy_policy_files(tmp_path) path = root / "product_documents/规则包/douyin_rule_packs.v1.json" data = json.loads(path.read_text(encoding="utf-8")) for dispatch in data["rule_pack_dispatch"]: if dispatch["dispatch_id"] == "dispatch_content": dispatch["dispatch_enabled"] = False path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8") with pytest.raises(ValueError, match="dispatch not found for Content/video"): JsonPolicyBundleStore(root).load_policy_bundle("V4") def test_dispatch_conflict_raises_config_error_with_rule_pack_ids(tmp_path): root = _copy_policy_files(tmp_path) path = root / "product_documents/规则包/douyin_rule_packs.v1.json" data = json.loads(path.read_text(encoding="utf-8")) duplicate = dict(data["rule_pack_dispatch"][0]) duplicate["dispatch_id"] = "dispatch_content_duplicate" duplicate["priority"] = 2 data["rule_pack_dispatch"].append(duplicate) path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8") with pytest.raises(ContentAgentError) as exc_info: JsonPolicyBundleStore(root).load_policy_bundle("V4") error = exc_info.value assert error.error_code == ErrorCode.CONFIG_RULE_PACK_DISPATCH_CONFLICT assert "douyin_content_discovery_rule_pack_v1" in error.message assert error.detail["conflict_rule_pack_ids"] == [ "douyin_content_discovery_rule_pack_v1", "douyin_content_discovery_rule_pack_v1", ] def test_select_dispatch_still_returns_content_for_default_bundle(): rule_package = json.loads(RULE_PACK_JSON.read_text(encoding="utf-8")) dispatch = _select_dispatch(rule_package, "V4") assert dispatch["dispatch_id"] == "dispatch_content" assert dispatch["target_entity"] == "Content" def test_policy_bundle_accepts_explicit_douyin_platform(): default_bundle = JsonPolicyBundleStore().load_policy_bundle("V4") explicit_bundle = JsonPolicyBundleStore().load_policy_bundle("V4", platform="douyin") assert explicit_bundle["dispatch_id"] == default_bundle["dispatch_id"] assert explicit_bundle["rule_pack_id"] == default_bundle["rule_pack_id"] assert explicit_bundle["platform"] == "douyin" assert explicit_bundle["rule_pack_by_entity"] == default_bundle["rule_pack_by_entity"] @pytest.mark.parametrize( ("platform", "dispatch_id"), [ ("douyin", "dispatch_content"), ("kuaishou", "dispatch_content_kuaishou"), ("shipinhao", "dispatch_content_shipinhao"), ], ) def test_policy_bundle_loads_real_three_platform_dispatches(platform, dispatch_id): bundle = JsonPolicyBundleStore().load_policy_bundle("V4", platform=platform) assert bundle["dispatch_id"] == dispatch_id assert bundle["dispatch"]["platform"] == platform assert bundle["rule_pack_id"] == "douyin_content_discovery_rule_pack_v1" assert bundle["rule_pack_by_entity"]["Content"]["dispatch"]["platform"] == platform def test_select_dispatch_filters_by_platform(): rule_package = json.loads(RULE_PACK_JSON.read_text(encoding="utf-8")) dispatch = _select_dispatch(rule_package, "V4", platform="kuaishou") assert dispatch["dispatch_id"] == "dispatch_content_kuaishou" assert dispatch["platform"] == "kuaishou" def test_rule_pack_by_entity_filters_by_platform(): rule_package = json.loads(RULE_PACK_JSON.read_text(encoding="utf-8")) by_entity = _build_rule_pack_by_entity(rule_package, "V4", platform="kuaishou") assert set(by_entity) == {"Content"} assert by_entity["Content"]["dispatch"]["platform"] == "kuaishou" def test_policy_bundle_fails_for_unknown_platform_without_dispatch(): with pytest.raises(ValueError, match="dispatch not found for Content/video"): JsonPolicyBundleStore().load_policy_bundle("V4", platform="unknown") def test_dispatch_conflict_ignores_other_platforms(tmp_path): root = _copy_policy_files(tmp_path) path = root / "product_documents/规则包/douyin_rule_packs.v1.json" data = json.loads(path.read_text(encoding="utf-8")) data["rule_pack_dispatch"].append(_synthetic_platform_dispatch(data, "xiaohongshu")) path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8") bundle = JsonPolicyBundleStore(root).load_policy_bundle("V4", platform="douyin") assert bundle["dispatch_id"] == "dispatch_content" assert bundle["platform"] == "douyin" def _synthetic_author_dispatch(rule_package): # M4 砍包后无 future dispatch 行;合成一条 Author dispatch 验证选择器仍是实体参数化的。 author = deepcopy(next(d for d in rule_package["rule_pack_dispatch"] if d["dispatch_id"] == "dispatch_content")) author.update( dispatch_id="dispatch_author_test", target_entity="Author", content_format="not_applicable", rule_pack_id="author_test_rule_pack_v1", dispatch_enabled=True, runtime_stage="V1.0", strategy_version="V4", ) return author def _synthetic_platform_dispatch(rule_package, platform): dispatch = deepcopy(next(d for d in rule_package["rule_pack_dispatch"] if d["dispatch_id"] == "dispatch_content")) dispatch.update( dispatch_id=f"dispatch_content_{platform}_test", platform=platform, dispatch_enabled=True, runtime_stage="V1.0", strategy_version="V4", ) return dispatch def test_select_dispatch_can_select_non_content_when_enabled(): rule_package = deepcopy(json.loads(RULE_PACK_JSON.read_text(encoding="utf-8"))) author = _synthetic_author_dispatch(rule_package) rule_package["rule_pack_dispatch"].append(author) dispatch = _select_dispatch( rule_package, "V4", target_entity="Author", content_format=author["content_format"] ) assert dispatch["rule_pack_id"] == "author_test_rule_pack_v1" def test_load_policy_bundle_keeps_content_shim(): bundle = JsonPolicyBundleStore().load_policy_bundle("V4") assert bundle["target_entity"] == "Content" assert bundle["rule_pack_id"] == "douyin_content_discovery_rule_pack_v1" assert bundle["rule_pack"] is bundle["rule_pack_by_entity"]["Content"]["rule_pack"] def test_load_policy_bundle_exposes_rule_pack_by_entity(): bundle = JsonPolicyBundleStore().load_policy_bundle("V4") by_entity = bundle["rule_pack_by_entity"] assert set(by_entity) == {"Content"} assert by_entity["Content"]["dispatch"]["dispatch_id"] == "dispatch_content" assert by_entity["Content"]["rule_pack"]["rule_pack_id"] == bundle["rule_pack_id"] def test_enabled_author_dispatch_can_be_found_by_entity_without_replacing_content_shim(tmp_path): root = _copy_policy_files(tmp_path) path = root / "product_documents/规则包/douyin_rule_packs.v1.json" data = json.loads(path.read_text(encoding="utf-8")) data["rule_pack_dispatch"].append(_synthetic_author_dispatch(data)) author_pack = deepcopy(data["rule_packs"][0]) author_pack["rule_pack_id"] = "author_test_rule_pack_v1" data["rule_packs"].append(author_pack) path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8") bundle = JsonPolicyBundleStore(root).load_policy_bundle("V4") assert bundle["rule_pack_id"] == "douyin_content_discovery_rule_pack_v1" assert set(bundle["rule_pack_by_entity"]) == {"Content", "Author"} assert bundle["rule_pack_by_entity"]["Author"]["rule_pack"]["rule_pack_id"] == "author_test_rule_pack_v1" def test_policy_bundle_fails_when_dispatch_points_to_missing_rule_pack(tmp_path): root = _copy_policy_files(tmp_path) path = root / "product_documents/规则包/douyin_rule_packs.v1.json" data = json.loads(path.read_text(encoding="utf-8")) for dispatch in data["rule_pack_dispatch"]: if dispatch["dispatch_id"] == "dispatch_content": dispatch["rule_pack_id"] = "missing_rule_pack" path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8") with pytest.raises(ValueError, match="dispatch dispatch_content matched 0 enabled rule packs"): JsonPolicyBundleStore(root).load_policy_bundle("V4") def test_policy_bundle_fails_when_dispatch_points_to_disabled_rule_pack(tmp_path): root = _copy_policy_files(tmp_path) path = root / "product_documents/规则包/douyin_rule_packs.v1.json" data = json.loads(path.read_text(encoding="utf-8")) for rule_pack in data["rule_packs"]: if rule_pack["rule_pack_id"] == "douyin_content_discovery_rule_pack_v1": rule_pack["enabled"] = False path.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8") with pytest.raises(ValueError, match="dispatch dispatch_content matched 0 enabled rule packs"): JsonPolicyBundleStore(root).load_policy_bundle("V4") def _copy_policy_files(tmp_path: Path) -> Path: root = tmp_path / "repo" (root / "product_documents/规则包").mkdir(parents=True) shutil.copy( "product_documents/规则包/douyin_rule_packs.v1.json", root / "product_documents/规则包/douyin_rule_packs.v1.json", ) (root / "product_documents/抖音游走策略").mkdir(parents=True) shutil.copy( "product_documents/抖音游走策略/douyin_walk_strategy.v1.json", root / "product_documents/抖音游走策略/douyin_walk_strategy.v1.json", ) (root / EDGE_CATALOG_JSON).parent.mkdir(parents=True) shutil.copy(EDGE_CATALOG_JSON, root / EDGE_CATALOG_JSON) return root