test_video_discovery_records.py 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245
  1. from __future__ import annotations
  2. import json
  3. from collections.abc import Generator
  4. from contextlib import contextmanager
  5. from datetime import datetime
  6. from decimal import Decimal
  7. from sqlalchemy import create_engine
  8. from sqlalchemy.orm import Session, sessionmaker
  9. from api.services import video_discovery_records as records_service
  10. from supply_infra.db.models.video_discovery import (
  11. VideoDiscoveryCandidate,
  12. VideoDiscoveryRun,
  13. VideoDiscoverySearch,
  14. )
  15. def _patch_sessions(monkeypatch) -> sessionmaker[Session]:
  16. engine = create_engine("sqlite+pysqlite:///:memory:")
  17. VideoDiscoveryRun.__table__.create(engine)
  18. VideoDiscoverySearch.__table__.create(engine)
  19. VideoDiscoveryCandidate.__table__.create(engine)
  20. factory = sessionmaker(bind=engine, autoflush=False, autocommit=False)
  21. @contextmanager
  22. def get_test_session() -> Generator[Session, None, None]:
  23. session = factory()
  24. try:
  25. yield session
  26. session.commit()
  27. except Exception:
  28. session.rollback()
  29. raise
  30. finally:
  31. session.close()
  32. monkeypatch.setattr(records_service, "get_session", get_test_session)
  33. return factory
  34. def _seed(factory: sessionmaker[Session]) -> None:
  35. now = datetime(2026, 7, 30, 12, 30)
  36. with factory.begin() as session:
  37. session.add_all(
  38. [
  39. VideoDiscoveryRun(
  40. id=1,
  41. run_id="find-001",
  42. biz_dt="20260730",
  43. demand_grade_id=10,
  44. demand_word="老年人智能手机教程",
  45. seed_video_id="seed-1",
  46. seed_video_title="手机使用入门",
  47. relevant_points_json=json.dumps([{"point": "大字模式"}]),
  48. intent_summary="寻找步骤清楚、面向老年人的手机教程。",
  49. status="finished",
  50. search_count=0,
  51. primary_count=0,
  52. valid_primary_count=1,
  53. outcome_status="partial",
  54. attempt_count=1,
  55. create_time=now,
  56. update_time=now,
  57. ),
  58. VideoDiscoveryRun(
  59. id=2,
  60. run_id="find-002",
  61. biz_dt="20260729",
  62. demand_grade_id=11,
  63. demand_word="退休生活",
  64. relevant_points_json="[]",
  65. status="failed",
  66. search_count=0,
  67. primary_count=0,
  68. create_time=now,
  69. update_time=now,
  70. ),
  71. ]
  72. )
  73. session.add(
  74. VideoDiscoverySearch(
  75. id=101,
  76. run_id="find-001",
  77. search_key="key-1",
  78. keyword="老年人 手机 教程",
  79. query_reason="验证教程内容",
  80. source_type="demand",
  81. provider="internal_keyword",
  82. provider_state_json='{"cursor":"next"}',
  83. content_type="视频",
  84. sort_type="综合排序",
  85. publish_time="不限",
  86. cursor="0",
  87. page_no=1,
  88. results_count=2,
  89. new_candidate_count=2,
  90. has_more=1,
  91. result_ids_json='["aweme-1","aweme-2"]',
  92. status="success",
  93. create_time=now,
  94. update_time=now,
  95. )
  96. )
  97. session.add_all(
  98. [
  99. VideoDiscoveryCandidate(
  100. id=1001,
  101. run_id="find-001",
  102. search_id=101,
  103. aweme_id="aweme-1",
  104. title="教爸妈设置大字体",
  105. author_name="数字生活助手",
  106. tags_json='["手机教程"]',
  107. relevance_score=Decimal("0.91"),
  108. elder_score=Decimal("0.88"),
  109. share_score=Decimal("0.70"),
  110. value_score=Decimal("0.82"),
  111. decision_bucket="primary",
  112. create_time=now,
  113. update_time=now,
  114. ),
  115. VideoDiscoveryCandidate(
  116. id=1002,
  117. run_id="find-001",
  118. search_id=101,
  119. aweme_id="aweme-2",
  120. title="手机发布会",
  121. decision_bucket="rejected",
  122. create_time=now,
  123. update_time=now,
  124. ),
  125. ]
  126. )
  127. def test_lists_runs_with_live_relation_counts(monkeypatch) -> None:
  128. factory = _patch_sessions(monkeypatch)
  129. _seed(factory)
  130. response = records_service.list_video_discovery_runs(
  131. biz_dt="20260730",
  132. keyword="智能手机",
  133. limit=20,
  134. offset=0,
  135. )
  136. assert response["total"] == 1
  137. run = response["items"][0]
  138. assert run["run_id"] == "find-001"
  139. assert run["search_count"] == 1
  140. assert run["candidate_count"] == 2
  141. assert run["primary_count"] == 1
  142. assert run["valid_primary_count"] == 1
  143. assert run["outcome_status"] == "partial"
  144. assert run["attempt_count"] == 1
  145. assert run["rejected_count"] == 1
  146. assert run["relevant_points"] == [{"point": "大字模式"}]
  147. def test_hides_runs_before_visible_date(monkeypatch) -> None:
  148. factory = _patch_sessions(monkeypatch)
  149. _seed(factory)
  150. response = records_service.list_video_discovery_runs(limit=20, offset=0)
  151. assert response["total"] == 1
  152. assert [item["run_id"] for item in response["items"]] == ["find-001"]
  153. assert records_service.get_video_discovery_run("find-002") is None
  154. assert (
  155. records_service.list_video_discovery_searches(
  156. "find-002",
  157. limit=20,
  158. offset=0,
  159. )
  160. is None
  161. )
  162. assert (
  163. records_service.list_video_discovery_candidates(
  164. "find-002",
  165. limit=20,
  166. offset=0,
  167. )
  168. is None
  169. )
  170. def test_lists_searches_and_candidates_with_filters(monkeypatch) -> None:
  171. factory = _patch_sessions(monkeypatch)
  172. _seed(factory)
  173. searches = records_service.list_video_discovery_searches(
  174. "find-001",
  175. keyword="教程",
  176. limit=20,
  177. offset=0,
  178. )
  179. assert searches is not None
  180. assert searches["total"] == 1
  181. assert searches["items"][0]["provider_state"] == {"cursor": "next"}
  182. assert searches["items"][0]["result_ids"] == ["aweme-1", "aweme-2"]
  183. assert [item["aweme_id"] for item in searches["items"][0]["candidates"]] == [
  184. "aweme-1",
  185. "aweme-2",
  186. ]
  187. assert searches["items"][0]["candidates"][0]["title"] == "教爸妈设置大字体"
  188. assert searches["items"][0]["candidates"][0]["comment_count"] is None
  189. assert searches["items"][0]["candidates"][0]["collect_count"] is None
  190. assert searches["items"][0]["candidates"][0]["share_count"] is None
  191. candidates = records_service.list_video_discovery_candidates(
  192. "find-001",
  193. bucket="primary",
  194. keyword="数字生活",
  195. limit=20,
  196. offset=0,
  197. )
  198. assert candidates is not None
  199. assert candidates["total"] == 1
  200. assert candidates["items"][0]["id"] == 1001
  201. assert candidates["items"][0]["tags"] == ["手机教程"]
  202. assert candidates["items"][0]["relevance_score"] == 0.91
  203. def test_record_children_return_none_for_missing_run(monkeypatch) -> None:
  204. _patch_sessions(monkeypatch)
  205. assert records_service.get_video_discovery_run("missing") is None
  206. assert (
  207. records_service.list_video_discovery_searches(
  208. "missing",
  209. limit=20,
  210. offset=0,
  211. )
  212. is None
  213. )
  214. assert (
  215. records_service.list_video_discovery_candidates(
  216. "missing",
  217. limit=20,
  218. offset=0,
  219. )
  220. is None
  221. )