test_stages.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. """M4 离线测试:注入假 chat,逐环节断言;assemble 纯函数对齐设计文档 §6。"""
  2. from __future__ import annotations
  3. import json
  4. from creation_knowledge.models import (
  5. Deconstruction,
  6. ExtractedContent,
  7. KnowledgeItem,
  8. Post,
  9. Scope,
  10. )
  11. from creation_knowledge.stages import (
  12. build_ingest_payload,
  13. deconstruct_item,
  14. screen_post,
  15. split_post,
  16. )
  17. def _post() -> Post:
  18. return Post(
  19. id="xhs_abc", url="https://www.xiaohongshu.com/explore/abc", content_id="abc",
  20. title="只要学会这几样,写短视频脚本真的不难", platform="xiaohongshu",
  21. author_name="海狸教自媒体运营", topic_list=["短视频"],
  22. )
  23. def _content() -> ExtractedContent:
  24. return ExtractedContent(
  25. text="短视频脚本包含标题、拍摄地点、分镜等要素",
  26. from_image="九宫格图讲了脚本6要素", is_empty=False,
  27. )
  28. def test_screen_passed():
  29. r = screen_post(_post(), _content(),
  30. chat=lambda s, u: {"passed": True, "score": 8, "reason": "含明确 How"})
  31. assert r.passed and r.score == 8
  32. def test_screen_feeds_extracted_not_body():
  33. seen = {}
  34. def chat(s, u):
  35. seen["u"] = u
  36. return {"passed": True, "score": 7, "reason": "x"}
  37. screen_post(_post(), _content(), chat=chat)
  38. assert "九宫格" in seen["u"] # 多模态提取内容进了 prompt,而非空 body_text
  39. def test_split_consistency_and_drop_empty():
  40. def chat(s, u):
  41. return {"items": [
  42. {"title": "脚本要素", "knowledge_types": ["what", "how", "why"],
  43. "what": "脚本含6要素", "why": "null", "how": "逐个填要素",
  44. "evidence": ["原句"]},
  45. {"title": "空", "knowledge_types": ["what"],
  46. "what": "null", "why": "null", "how": "null", "evidence": []},
  47. ]}
  48. items = split_post(_post(), _content(), chat=chat)
  49. assert len(items) == 1 # 三个都空的片段被丢弃
  50. it = items[0]
  51. assert it.why is None # "null" 归一为 None
  52. assert set(it.knowledge_types) == {"what", "how"} # 与非空字段对齐,why 被剔除
  53. def test_deconstruct_filters_invalid():
  54. def chat(s, u):
  55. return {"stages": ["选题", "脚本", "乱写"],
  56. "scopes": [{"scope_type": "form", "value": "操作流程"},
  57. {"scope_type": "bad", "value": "x"},
  58. {"scope_type": "effect", "value": ""}],
  59. "stage_reason": "r1", "scope_reason": "r2"}
  60. d = deconstruct_item(KnowledgeItem(title="t", knowledge_types=["how"], how="做法"),
  61. chat=chat)
  62. assert d.stages == ["选题", "脚本"] # 非法阶段过滤
  63. assert [s.scope_type for s in d.scopes] == ["form"] # 非法 scope / 空 value 过滤
  64. def test_assemble_matches_design_doc():
  65. item = KnowledgeItem(title="评论区选题法", knowledge_types=["what", "how"],
  66. what="从评论提取痛点", why=None, how="收集高赞评论改写",
  67. evidence=["原句a"])
  68. deco = Deconstruction(
  69. stages=["选题", "脚本"],
  70. scopes=[Scope(scope_type="form", value="公式框架"),
  71. Scope(scope_type="effect", value="生成选题")],
  72. stage_reason="既选题又脚本", scope_reason="形式+作用")
  73. p = build_ingest_payload(_post(), item, deco)
  74. assert p.source["id"] == "xhs_abc"
  75. assert p.source["source_type"] == "post"
  76. assert p.source["source_metadata"]["platform"] == "xiaohongshu"
  77. assert p.dim_attributes == ["what", "how"]
  78. assert p.dim_creations == ["选题", "脚本"]
  79. assert {"scope_type": "form", "value": "公式框架"} in p.scopes
  80. assert json.loads(p.content) == {"what": "从评论提取痛点", "why": None,
  81. "how": "收集高赞评论改写"}
  82. assert "原文证据" in [e["key"] for e in p.custom_ext]