test_walk_graph_config.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. """V3-M4A: WalkGraphStore 读三个游走配置 JSON + 校验 + 拍板值解包。"""
  2. from __future__ import annotations
  3. import json
  4. from pathlib import Path
  5. import pytest
  6. from content_agent.integrations.walk_graph_json import (
  7. EDGE_CATALOG_PATH,
  8. PROFILE_EDGE_STATUSES,
  9. WalkGraphStore,
  10. _validate_graph,
  11. _validate_profile,
  12. edge_budgets_for_platform,
  13. edge_permission,
  14. edge_supported,
  15. low_budget,
  16. )
  17. PROFILE_DIR = Path("tech_documents/数据接口与来源/platform_profiles")
  18. def test_graph_has_8_nodes_and_9_edges():
  19. # M8:删 query_next_page 边后 10→9;search_page 节点保留(孤儿不报错)。
  20. graph = WalkGraphStore().load_graph()
  21. assert len(graph["nodes"]) == 8
  22. assert len(graph["edges"]) == 9
  23. assert "query_next_page" not in {edge["edge_id"] for edge in graph["edges"]}
  24. assert {edge["gate"] for edge in graph["edges"]} == {"none", "decision_gated", "keep_only"}
  25. def test_graph_edges_match_platform_neutral_catalog():
  26. graph = WalkGraphStore().load_graph()
  27. catalog = json.loads(EDGE_CATALOG_PATH.read_text(encoding="utf-8"))
  28. assert {edge["edge_id"] for edge in graph["edges"]} == {
  29. row["edge_id"] for row in catalog["walk_edge_catalog"]
  30. }
  31. def test_policy_unwraps_pinned_values():
  32. policy = WalkGraphStore().load_policy()
  33. # 2026-06-24 拍板:总闸收敛到 10、删 max_reseed_rounds;deny / halve_min_1(max(1, budget//2))。
  34. assert policy["global"]["max_total_actions_per_run"] == 10
  35. assert policy["global"]["max_depth"] == 5
  36. assert "max_reseed_rounds" not in policy["global"]
  37. assert policy["edge_permissions"]["KEEP_CONTENT_FOR_REVIEW"]["video_to_hashtag"] == "deny"
  38. assert policy["budget_tiers"]["low_budget"] == "halve_min_1"
  39. assert low_budget(3) == 1
  40. assert low_budget(10) == 5
  41. def test_policy_pins_gemini_workers_without_call_cap():
  42. # Gemini 数量上限已取消;仅保留判定并发度。M8 删 max_reseed_rounds。
  43. policy_global = WalkGraphStore().load_policy()["global"]
  44. assert set(policy_global) == {
  45. "max_total_actions_per_run",
  46. "max_depth",
  47. "gemini_max_workers",
  48. }
  49. assert policy_global["gemini_max_workers"] == 5
  50. def test_policy_allows_smoke_env_overrides(monkeypatch):
  51. monkeypatch.setenv("CONTENT_AGENT_WALK_MAX_TOTAL_ACTIONS_PER_RUN", "4")
  52. monkeypatch.setenv("CONTENT_AGENT_WALK_MAX_DEPTH", "2")
  53. monkeypatch.setenv("CONTENT_AGENT_GEMINI_MAX_WORKERS", "3")
  54. policy_global = WalkGraphStore().load_policy()["global"]
  55. assert policy_global["max_total_actions_per_run"] == 4
  56. assert policy_global["max_depth"] == 2
  57. assert policy_global["gemini_max_workers"] == 3
  58. assert set(policy_global) == {
  59. "max_total_actions_per_run",
  60. "max_depth",
  61. "gemini_max_workers",
  62. }
  63. def test_policy_edge_budgets_match_decided_values():
  64. # 2026-06-24 拍板:标签 5、作者 5、每作者作品 5;query_next_page 已删。
  65. budgets = WalkGraphStore().load_policy()["edge_budgets_by_id"]
  66. assert "query_next_page" not in budgets
  67. assert budgets["author_to_works"]["max_total_actions"] == 5
  68. assert budgets["author_to_works"]["max_works_per_author"] == 5
  69. assert budgets["hashtag_to_query"]["max_total_actions"] == 5
  70. def test_platform_edge_budgets_migrate_default_and_kuaishou_author_budget():
  71. policy = WalkGraphStore().load_policy()
  72. rows = policy["platform_edge_budgets_by_platform_edge"]
  73. assert rows[("default", "__global__")]["max_total_actions"] == 10
  74. assert rows[("default", "__global__")]["max_depth"] == 5
  75. assert rows[("default", "author_to_works")]["max_total_actions"] == 5
  76. assert rows[("kuaishou", "author_to_works")]["max_works_per_author"] == 5
  77. kuaishou = edge_budgets_for_platform(policy, "kuaishou")
  78. assert kuaishou["author_to_works"]["max_total_actions"] == 5
  79. assert kuaishou["author_to_works"]["operator_display_status"] == "budget_blocked"
  80. def test_three_short_video_author_edges_match_m3_contract():
  81. store = WalkGraphStore()
  82. assert edge_supported(store.load_profile("douyin"), "author_to_works")
  83. assert edge_supported(store.load_profile("douyin"), "author_work_to_content")
  84. assert edge_supported(store.load_profile("kuaishou"), "author_to_works")
  85. assert edge_supported(store.load_profile("kuaishou"), "author_work_to_content")
  86. profile = store.load_profile("shipinhao")
  87. assert not edge_supported(profile, "author_to_works")
  88. assert not edge_supported(profile, "author_work_to_content")
  89. assert edge_supported(profile, "hashtag_to_query")
  90. # 终端边不在 profile(平台无关控制边)→ 恒 supported。
  91. assert edge_supported(profile, "decision_to_asset")
  92. def test_profile_edge_status_contract_accepts_four_states():
  93. assert PROFILE_EDGE_STATUSES == {"supported", "blocked", "no_interface", "planned"}
  94. edge_ids = {"author_to_works", "video_to_hashtag"}
  95. findings = _validate_profile(
  96. {
  97. "edges": {
  98. "author_to_works": {"status": "supported"},
  99. "video_to_hashtag": {"status": "no_interface"},
  100. }
  101. },
  102. edge_ids,
  103. )
  104. assert findings == []
  105. bad_status = _validate_profile({"edges": {"author_to_works": {"status": "weak"}}}, edge_ids)
  106. assert any(finding["check_id"] == "profile_edge_status" for finding in bad_status)
  107. bad_shape = _validate_profile({"edges": {"_all": "blocked"}}, edge_ids)
  108. assert any(finding["check_id"] == "profile_edge_ref" for finding in bad_shape)
  109. assert any(finding["check_id"] == "profile_edge_invalid" for finding in bad_shape)
  110. def test_edge_supported_only_allows_supported_status():
  111. assert edge_supported({"edges": {"edge": {"status": "supported"}}}, "edge")
  112. for status in ["blocked", "no_interface", "planned"]:
  113. assert not edge_supported({"edges": {"edge": {"status": status}}}, "edge")
  114. assert edge_supported({"edges": {}}, "decision_to_asset")
  115. def test_all_platform_profiles_load_and_blocked_profiles_are_explicit():
  116. store = WalkGraphStore()
  117. graph_edge_ids = {edge["edge_id"] for edge in store.load_graph()["edges"]}
  118. for path in sorted(PROFILE_DIR.glob("*.json")):
  119. profile = store.load_profile(path.stem)
  120. assert "_all" not in profile["edges"]
  121. if path.stem in {"toutiao", "zhihu"}:
  122. assert set(profile["edges"]) == graph_edge_ids
  123. assert {row["status"] for row in profile["edges"].values()} == {"blocked"}
  124. def test_edge_permission_defaults_to_deny():
  125. policy = WalkGraphStore().load_policy()
  126. assert edge_permission(policy, "ADD_TO_CONTENT_POOL", "author_to_works") == "allow"
  127. assert edge_permission(policy, "KEEP_CONTENT_FOR_REVIEW", "author_to_works") == "allow_low_budget"
  128. assert edge_permission(policy, "REJECT_CONTENT", "author_to_works") == "deny"
  129. assert edge_permission(policy, None, "author_to_works") == "deny"
  130. assert edge_permission(policy, "ADD_TO_CONTENT_POOL", "unknown_edge") == "deny"
  131. def test_unknown_graph_edge_fails_validation():
  132. graph = WalkGraphStore().load_graph()
  133. tampered = {**graph, "edges": [*graph["edges"], {**graph["edges"][0], "edge_id": "made_up_edge"}]}
  134. findings = _validate_graph(tampered, {edge["edge_id"] for edge in graph["edges"]})
  135. assert any(finding["check_id"] == "edge_not_in_catalog" for finding in findings)
  136. with pytest.raises(ValueError):
  137. store = WalkGraphStore()
  138. bad_policy = {**store.load_policy(), "edge_budgets": [{"edge_id": "made_up_edge"}]}
  139. from content_agent.integrations.walk_graph_json import _raise_on_fail, _validate_policy
  140. _raise_on_fail(_validate_policy(bad_policy, {e["edge_id"] for e in graph["edges"]}), "walk policy")