test_decode_payloads.py 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. from __future__ import annotations
  2. import json
  3. import pytest
  4. from core.models import Post
  5. from decode_content.payloads import IngestPayloadValidationError, build_payload, build_payloads, validate_ingest_payload
  6. def _post() -> Post:
  7. return Post(
  8. id="xhs_1",
  9. platform="xiaohongshu",
  10. url="https://xhs/1",
  11. content_id="1",
  12. unique_key="xhs:1",
  13. title="脚本教程",
  14. content_type="note",
  15. content_mode="image_post",
  16. media_count=2,
  17. cover_url="https://cdn.test/cover.webp",
  18. author_name="作者",
  19. )
  20. def test_build_how_payload_uses_formal_dim_attribute_and_scope_union():
  21. knowledge = {
  22. "id": "k1",
  23. "type": "how",
  24. "title": "先定受众再写脚本",
  25. "purpose": "写出稳定脚本",
  26. "业务阶段": ["脚本"],
  27. "steps": [
  28. {
  29. "input": "选题",
  30. "directive": "收窄受众",
  31. "output": "受众画像",
  32. "创作阶段": "定向",
  33. "动作": "收窄",
  34. "作用域": [
  35. {"scope_type": "intent", "value": "吸引注意", "link": "复用"},
  36. {"scope_type": "intent", "value": "吸引注意", "link": "复用"},
  37. ],
  38. }
  39. ],
  40. }
  41. payload = build_payload(_post(), knowledge)
  42. assert payload["dim_attributes"] == ["how"]
  43. assert payload["source"]["source_metadata"] == {
  44. "platform": "xiaohongshu",
  45. "url": "https://xhs/1",
  46. "unique_key": "xhs:1",
  47. "platform_item_id": "1",
  48. "content_type": "note",
  49. "content_mode": "image_post",
  50. "media_count": 2,
  51. "cover_url": "https://cdn.test/cover.webp",
  52. }
  53. assert "输入:选题" in payload["content"]
  54. assert "指引:收窄受众" in payload["content"]
  55. assert payload["scopes"] == [{"scope_type": "intent", "value": "吸引注意"}]
  56. assert {"key": "创作阶段", "type": "str", "value": "定向"} in payload["custom_ext"]
  57. def test_build_what_and_why_payload_content_is_string_contract():
  58. what = {
  59. "id": "w1",
  60. "type": "what",
  61. "title": "脚本三要素",
  62. "kind": "子集",
  63. "概要": "脚本由开头中段结尾构成",
  64. "维度拆分规则": "按表达顺序",
  65. "body": [{"item_name": "开头", "item_desc": "抓注意"}],
  66. "业务阶段": ["脚本"],
  67. "作用域": [{"scope_type": "form", "value": "脚本结构", "top": []}],
  68. }
  69. why = {
  70. "id": "y1",
  71. "type": "why",
  72. "title": "冲突带来停留",
  73. "阐述": "冲突能制造期待,所以提升停留。",
  74. "业务阶段": ["选题"],
  75. "作用域": [{"scope_type": "effect", "value": "提升停留", "score": 0.95}],
  76. }
  77. what_payload, why_payload = build_payloads(_post(), [what, why])
  78. assert what_payload["dim_attributes"] == ["what"]
  79. assert json.loads(what_payload["content"])["body"][0]["item_name"] == "开头"
  80. assert {"key": "概要", "type": "str", "value": "脚本由开头中段结尾构成"} in what_payload["custom_ext"]
  81. assert why_payload["dim_attributes"] == ["why"]
  82. assert why_payload["content"] == "冲突能制造期待,所以提升停留。"
  83. assert "score" not in why_payload["scopes"][0]
  84. def test_build_why_payload_accepts_explanation_alias():
  85. payload = build_payload(
  86. _post(),
  87. {
  88. "id": "y2",
  89. "type": "why",
  90. "title": "动作变化带来画面节奏",
  91. "explanation": "动作变化能制造连续观看的期待。",
  92. "业务阶段": ["脚本"],
  93. "作用域": [{"scope_type": "effect", "value": "提升节奏感"}],
  94. },
  95. )
  96. assert payload["content"] == "动作变化能制造连续观看的期待。"
  97. def test_validate_ingest_payload_blocks_external_api_contract_violations():
  98. payload = build_payload(
  99. _post(),
  100. {
  101. "id": "h1",
  102. "type": "how",
  103. "title": "先定受众再写脚本",
  104. "purpose": "写出稳定脚本",
  105. "业务阶段": ["脚本"],
  106. "steps": [
  107. {
  108. "input": "选题",
  109. "directive": "收窄受众",
  110. "output": "受众画像",
  111. "作用域": [{"scope_type": "intent", "value": "吸引注意"}],
  112. }
  113. ],
  114. },
  115. )
  116. bad = {**payload, "content": ""}
  117. with pytest.raises(IngestPayloadValidationError, match="content"):
  118. validate_ingest_payload(bad)
  119. bad = {**payload, "source": {**payload["source"], "id": "x" * 65}}
  120. with pytest.raises(IngestPayloadValidationError, match="source.id"):
  121. validate_ingest_payload(bad)
  122. bad = {**payload, "title": "x" * 513}
  123. with pytest.raises(IngestPayloadValidationError, match="title"):
  124. validate_ingest_payload(bad)
  125. bad = {**payload, "source": {**payload["source"], "author": "x" * 129}}
  126. with pytest.raises(IngestPayloadValidationError, match="author"):
  127. validate_ingest_payload(bad)
  128. bad = {**payload, "scopes": [{"scope_type": "intent", "value": "x" * 129}]}
  129. with pytest.raises(IngestPayloadValidationError, match="scopes"):
  130. validate_ingest_payload(bad)
  131. bad = {**payload, "custom_ext": [{"key": "推荐指数", "type": "bad", "value": "5"}]}
  132. with pytest.raises(IngestPayloadValidationError, match="custom_ext"):
  133. validate_ingest_payload(bad)
  134. def test_validate_ingest_payload_rejects_legacy_dim_attributes():
  135. payload = build_payload(
  136. _post(),
  137. {
  138. "id": "h1",
  139. "type": "how",
  140. "title": "先定受众再写脚本",
  141. "purpose": "写出稳定脚本",
  142. "业务阶段": ["脚本"],
  143. "steps": [
  144. {
  145. "input": "选题",
  146. "directive": "收窄受众",
  147. "output": "受众画像",
  148. "作用域": [{"scope_type": "intent", "value": "吸引注意"}],
  149. }
  150. ],
  151. },
  152. )
  153. legacy_values = [
  154. "how" + "\u5de5\u5e8f",
  155. "what" + "\u6784\u6210",
  156. "why" + "\u539f\u7406",
  157. ]
  158. for legacy_value in legacy_values:
  159. bad = {**payload, "dim_attributes": [legacy_value]}
  160. with pytest.raises(IngestPayloadValidationError, match="dim_attributes"):
  161. validate_ingest_payload(bad)