test_pipeline_e2e.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108
  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. )
  21. class FakeStore:
  22. def __init__(self):
  23. self.posts: dict = {}
  24. self.items: list = []
  25. def upsert_post(self, post):
  26. self.posts[post.id] = {"stage": "fetched"}
  27. def set_extracted(self, pid, ex):
  28. self.posts[pid].update(stage="extracted", extracted=ex)
  29. def set_screening(self, pid, sc):
  30. self.posts[pid].update(stage="screened", screening=sc)
  31. def update_stage(self, pid, stage):
  32. self.posts[pid]["stage"] = stage
  33. def save_item(self, pid, item, deco, payload):
  34. self.items.append({"post_id": pid, "item": item, "deco": deco, "payload": payload})
  35. return len(self.items)
  36. def update_item_ingest(self, iid, status, kid):
  37. self.items[iid - 1].update(ingest_status=status, knowledge_id=kid)
  38. def _fetch(url):
  39. resp = json.loads((FIXTURES / f"xhs_case_{CID}.json").read_text("utf-8"))
  40. return parse_detail_response(resp, fallback_content_id=CID)
  41. def _extract(post):
  42. return ExtractedContent(text=post.body_text or "内容", is_empty=False)
  43. def _chat_pass(system, user):
  44. if "筛选" in system:
  45. return {"passed": True, "score": 8, "reason": "ok"}
  46. if "拆分" in system:
  47. return {"items": [{"title": "脚本要素", "knowledge_types": ["how"],
  48. "what": "null", "why": "null", "how": "逐个填要素",
  49. "evidence": ["原句"]}]}
  50. if "解构" in system:
  51. return {"stages": ["脚本"], "scopes": [{"scope_type": "form", "value": "操作流程"}],
  52. "stage_reason": "r", "scope_reason": "r"}
  53. raise AssertionError(f"unexpected system: {system}")
  54. def _chat_reject(system, user):
  55. if "筛选" in system:
  56. return {"passed": False, "score": 2, "reason": "只是作品本身"}
  57. raise AssertionError("rejected post 不应进入拆分/解构")
  58. def test_pipeline_done_path():
  59. store = FakeStore()
  60. results = run_pipeline(
  61. ["https://www.xiaohongshu.com/explore/" + CID],
  62. settings=_settings(), ingest_enabled=False, store=store,
  63. fetch_fn=_fetch, extract_fn=_extract, chat=_chat_pass,
  64. )
  65. r = results[0]
  66. assert r["status"] == "done" and r["items"] == 1
  67. pid = f"xhs_{CID}"
  68. assert store.posts[pid]["stage"] == "done"
  69. assert len(store.items) == 1
  70. saved = store.items[0]
  71. assert saved["payload"]["source"]["id"] == pid
  72. assert saved["payload"]["dim_attributes"] == ["how"]
  73. def test_pipeline_rejected_path():
  74. store = FakeStore()
  75. results = run_pipeline(
  76. [CID], settings=_settings(), ingest_enabled=False, store=store,
  77. fetch_fn=_fetch, extract_fn=_extract, chat=_chat_reject,
  78. )
  79. r = results[0]
  80. assert r["status"] == "rejected"
  81. assert store.posts[f"xhs_{CID}"]["stage"] == "rejected"
  82. assert store.items == []