test_video_discovery_records.py 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239
  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. create_time=now,
  53. update_time=now,
  54. ),
  55. VideoDiscoveryRun(
  56. id=2,
  57. run_id="find-002",
  58. biz_dt="20260729",
  59. demand_grade_id=11,
  60. demand_word="退休生活",
  61. relevant_points_json="[]",
  62. status="failed",
  63. search_count=0,
  64. primary_count=0,
  65. create_time=now,
  66. update_time=now,
  67. ),
  68. ]
  69. )
  70. session.add(
  71. VideoDiscoverySearch(
  72. id=101,
  73. run_id="find-001",
  74. search_key="key-1",
  75. keyword="老年人 手机 教程",
  76. query_reason="验证教程内容",
  77. source_type="demand",
  78. provider="internal_keyword",
  79. provider_state_json='{"cursor":"next"}',
  80. content_type="视频",
  81. sort_type="综合排序",
  82. publish_time="不限",
  83. cursor="0",
  84. page_no=1,
  85. results_count=2,
  86. new_candidate_count=2,
  87. has_more=1,
  88. result_ids_json='["aweme-1","aweme-2"]',
  89. status="success",
  90. create_time=now,
  91. update_time=now,
  92. )
  93. )
  94. session.add_all(
  95. [
  96. VideoDiscoveryCandidate(
  97. id=1001,
  98. run_id="find-001",
  99. search_id=101,
  100. aweme_id="aweme-1",
  101. title="教爸妈设置大字体",
  102. author_name="数字生活助手",
  103. tags_json='["手机教程"]',
  104. relevance_score=Decimal("0.91"),
  105. elder_score=Decimal("0.88"),
  106. share_score=Decimal("0.70"),
  107. value_score=Decimal("0.82"),
  108. decision_bucket="primary",
  109. create_time=now,
  110. update_time=now,
  111. ),
  112. VideoDiscoveryCandidate(
  113. id=1002,
  114. run_id="find-001",
  115. search_id=101,
  116. aweme_id="aweme-2",
  117. title="手机发布会",
  118. decision_bucket="rejected",
  119. create_time=now,
  120. update_time=now,
  121. ),
  122. ]
  123. )
  124. def test_lists_runs_with_live_relation_counts(monkeypatch) -> None:
  125. factory = _patch_sessions(monkeypatch)
  126. _seed(factory)
  127. response = records_service.list_video_discovery_runs(
  128. biz_dt="20260730",
  129. keyword="智能手机",
  130. limit=20,
  131. offset=0,
  132. )
  133. assert response["total"] == 1
  134. run = response["items"][0]
  135. assert run["run_id"] == "find-001"
  136. assert run["search_count"] == 1
  137. assert run["candidate_count"] == 2
  138. assert run["primary_count"] == 1
  139. assert run["rejected_count"] == 1
  140. assert run["relevant_points"] == [{"point": "大字模式"}]
  141. def test_hides_runs_before_visible_date(monkeypatch) -> None:
  142. factory = _patch_sessions(monkeypatch)
  143. _seed(factory)
  144. response = records_service.list_video_discovery_runs(limit=20, offset=0)
  145. assert response["total"] == 1
  146. assert [item["run_id"] for item in response["items"]] == ["find-001"]
  147. assert records_service.get_video_discovery_run("find-002") is None
  148. assert (
  149. records_service.list_video_discovery_searches(
  150. "find-002",
  151. limit=20,
  152. offset=0,
  153. )
  154. is None
  155. )
  156. assert (
  157. records_service.list_video_discovery_candidates(
  158. "find-002",
  159. limit=20,
  160. offset=0,
  161. )
  162. is None
  163. )
  164. def test_lists_searches_and_candidates_with_filters(monkeypatch) -> None:
  165. factory = _patch_sessions(monkeypatch)
  166. _seed(factory)
  167. searches = records_service.list_video_discovery_searches(
  168. "find-001",
  169. keyword="教程",
  170. limit=20,
  171. offset=0,
  172. )
  173. assert searches is not None
  174. assert searches["total"] == 1
  175. assert searches["items"][0]["provider_state"] == {"cursor": "next"}
  176. assert searches["items"][0]["result_ids"] == ["aweme-1", "aweme-2"]
  177. assert [item["aweme_id"] for item in searches["items"][0]["candidates"]] == [
  178. "aweme-1",
  179. "aweme-2",
  180. ]
  181. assert searches["items"][0]["candidates"][0]["title"] == "教爸妈设置大字体"
  182. assert searches["items"][0]["candidates"][0]["comment_count"] is None
  183. assert searches["items"][0]["candidates"][0]["collect_count"] is None
  184. assert searches["items"][0]["candidates"][0]["share_count"] is None
  185. candidates = records_service.list_video_discovery_candidates(
  186. "find-001",
  187. bucket="primary",
  188. keyword="数字生活",
  189. limit=20,
  190. offset=0,
  191. )
  192. assert candidates is not None
  193. assert candidates["total"] == 1
  194. assert candidates["items"][0]["id"] == 1001
  195. assert candidates["items"][0]["tags"] == ["手机教程"]
  196. assert candidates["items"][0]["relevance_score"] == 0.91
  197. def test_record_children_return_none_for_missing_run(monkeypatch) -> None:
  198. _patch_sessions(monkeypatch)
  199. assert records_service.get_video_discovery_run("missing") is None
  200. assert (
  201. records_service.list_video_discovery_searches(
  202. "missing",
  203. limit=20,
  204. offset=0,
  205. )
  206. is None
  207. )
  208. assert (
  209. records_service.list_video_discovery_candidates(
  210. "missing",
  211. limit=20,
  212. offset=0,
  213. )
  214. is None
  215. )