test_pipeline_e2e.py 3.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. """M5 离线编排测试:注入 fake store/fetch/extract/chat,验证 done 与 rejected 两条路径。
  2. 真实落库的 e2e 见 scripts/run_batch.py(在能连 DB 的云端跑)。
  3. """
  4. from __future__ import annotations
  5. import json
  6. from pathlib import Path
  7. from creation_knowledge.config import PgConfig, Settings
  8. from creation_knowledge.integrations.crawler import parse_detail_response
  9. from creation_knowledge.models import ExtractedContent
  10. from creation_knowledge.pipeline import run_pipeline
  11. FIXTURES = Path(__file__).parent / "fixtures"
  12. CID = "67e4bdf50000000006028a59"
  13. def _settings() -> Settings:
  14. return Settings(
  15. pg=PgConfig(host="h", port=5432, user="u", password="p", database="d"),
  16. crawler_base_url="http://x", crawler_key="", crawler_timeout=30,
  17. video_model="m", gemini_api_key="", openrouter_base_url="http://x",
  18. openrouter_api_key="k", llm_model="m", knowhub_api="http://x",
  19. ingest_enabled=False, max_cards=12, frames_dir="runtime/frames",
  20. douyin_ratio="540p",
  21. )
  22. class FakeStore:
  23. def __init__(self):
  24. self.posts: dict = {}
  25. self.items: list = []
  26. def upsert_post(self, post):
  27. self.posts[post.id] = {"stage": "fetched"}
  28. def set_extracted(self, pid, ex):
  29. self.posts[pid].update(stage="extracted", extracted=ex)
  30. def set_screening(self, pid, sc):
  31. self.posts[pid].update(stage="screened", screening=sc)
  32. def update_stage(self, pid, stage):
  33. self.posts[pid]["stage"] = stage
  34. def save_item(self, pid, item, deco, payload):
  35. self.items.append({"post_id": pid, "item": item, "deco": deco, "payload": payload})
  36. return len(self.items)
  37. def update_item_ingest(self, iid, status, kid):
  38. self.items[iid - 1].update(ingest_status=status, knowledge_id=kid)
  39. def _fetch(url):
  40. resp = json.loads((FIXTURES / f"xhs_case_{CID}.json").read_text("utf-8"))
  41. return parse_detail_response(resp, fallback_content_id=CID)
  42. def _extract(post):
  43. return ExtractedContent(text=post.body_text or "内容", is_empty=False)
  44. def _chat_pass(system, user):
  45. if "筛选" in system:
  46. return {"passed": True, "score": 8, "reason": "ok"}
  47. if "拆分" in system:
  48. return {"items": [{"title": "脚本要素", "knowledge_types": ["how"],
  49. "what": "null", "why": "null", "how": "逐个填要素",
  50. "evidence": ["原句"]}]}
  51. if "解构" in system:
  52. return {"stages": ["脚本"], "scopes": [{"scope_type": "form", "value": "操作流程"}],
  53. "stage_reason": "r", "scope_reason": "r"}
  54. raise AssertionError(f"unexpected system: {system}")
  55. def _chat_reject(system, user):
  56. if "筛选" in system:
  57. return {"passed": False, "score": 2, "reason": "只是作品本身"}
  58. raise AssertionError("rejected post 不应进入拆分/解构")
  59. def test_pipeline_done_path():
  60. store = FakeStore()
  61. results = run_pipeline(
  62. ["https://www.xiaohongshu.com/explore/" + CID],
  63. settings=_settings(), ingest_enabled=False, store=store,
  64. fetch_fn=_fetch, extract_fn=_extract, chat=_chat_pass,
  65. )
  66. r = results[0]
  67. assert r["status"] == "done" and r["items"] == 1
  68. pid = f"xhs_{CID}"
  69. assert store.posts[pid]["stage"] == "done"
  70. assert len(store.items) == 1
  71. saved = store.items[0]
  72. assert saved["payload"]["source"]["id"] == pid
  73. assert saved["payload"]["dim_attributes"] == ["how"]
  74. def test_pipeline_rejected_path():
  75. store = FakeStore()
  76. results = run_pipeline(
  77. [CID], settings=_settings(), ingest_enabled=False, store=store,
  78. fetch_fn=_fetch, extract_fn=_extract, chat=_chat_reject,
  79. )
  80. r = results[0]
  81. assert r["status"] == "rejected"
  82. assert store.posts[f"xhs_{CID}"]["stage"] == "rejected"
  83. assert store.items == []