test_postgres_repository_contract.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228
  1. from __future__ import annotations
  2. from uuid import uuid4
  3. from acquisition.repositories.postgres import PostgresAcquisitionRepository
  4. class RecordingPostgresRepo(PostgresAcquisitionRepository):
  5. def __init__(self):
  6. super().__init__(conn=None)
  7. self.one_calls = []
  8. self.one_or_none_calls = []
  9. self.all_calls = []
  10. self.one_results = []
  11. self.one_or_none_results = []
  12. self.all_results = []
  13. def _one(self, sql, params):
  14. self.one_calls.append((sql, params))
  15. return self.one_results.pop(0)
  16. def _one_or_none(self, sql, params):
  17. self.one_or_none_calls.append((sql, params))
  18. return self.one_or_none_results.pop(0)
  19. def _all(self, sql, params):
  20. self.all_calls.append((sql, params))
  21. return self.all_results.pop(0)
  22. def test_postgres_summary_uses_formal_run_job_item_tables():
  23. run_id = uuid4()
  24. batch_id = uuid4()
  25. query_id = uuid4()
  26. repo = RecordingPostgresRepo()
  27. repo.one_results.append(
  28. {
  29. "id": run_id,
  30. "run_key": "r1",
  31. "batch_id": batch_id,
  32. "status": "done",
  33. "query_count": 1,
  34. "job_count": 3,
  35. "candidate_count": 7,
  36. "creation_hit_count": 2,
  37. "started_at": None,
  38. "finished_at": None,
  39. }
  40. )
  41. repo.all_results.append(
  42. [
  43. {
  44. "query_id": query_id,
  45. "query_text": "脚本 开头",
  46. "job_count": 3,
  47. "candidate_count": 7,
  48. "creation_hit_count": 2,
  49. "platforms": {"xiaohongshu": {"status": "done"}},
  50. }
  51. ]
  52. )
  53. summary = repo.get_run_summary(run_id)
  54. first_sql, first_params = repo.one_calls[0]
  55. second_sql, second_params = repo.all_calls[0]
  56. assert "FROM acquisition_runs ar" in first_sql
  57. assert "LEFT JOIN acquisition_jobs aj" in first_sql
  58. assert "LEFT JOIN candidate_items ci" in first_sql
  59. assert "LEFT JOIN item_classifications ic" in first_sql
  60. assert first_params == (run_id,)
  61. assert "jsonb_object_agg" in second_sql
  62. assert "WHERE aj.run_id = %s" in second_sql
  63. assert second_params == (run_id,)
  64. assert summary["queries"][0]["query_id"] == query_id
  65. def test_postgres_repository_lists_only_creation_candidates_for_decode():
  66. run_id = uuid4()
  67. item_id = uuid4()
  68. repo = RecordingPostgresRepo()
  69. repo.all_results.append(
  70. [
  71. {
  72. "id": item_id,
  73. "platform": "xiaohongshu",
  74. "platform_item_id": "x1",
  75. "unique_key": "xhs:x1",
  76. "canonical_url": "https://xhs.test/1",
  77. "status": "candidate",
  78. }
  79. ]
  80. )
  81. items = repo.list_creation_candidate_items(run_id=run_id, limit=20)
  82. sql, params = repo.all_calls[0]
  83. assert "JOIN acquisition_jobs aj ON aj.id = ci.job_id" in sql
  84. assert "JOIN item_classifications ic ON ic.item_id = ci.id" in sql
  85. assert "ic.is_creation_knowledge IS TRUE" in sql
  86. assert "LIMIT %s" in sql
  87. assert params == (run_id, 20)
  88. assert items[0].id == item_id
  89. assert items[0].platform == "xiaohongshu"
  90. def test_postgres_upsert_candidate_item_prefers_unique_key_for_existing_rows():
  91. item_id = uuid4()
  92. repo = RecordingPostgresRepo()
  93. repo.one_or_none_results.append({"id": item_id})
  94. repo.one_results.append(
  95. {
  96. "id": item_id,
  97. "job_id": None,
  98. "query_id": None,
  99. "platform": "xiaohongshu",
  100. "platform_item_id": "x1",
  101. "unique_key": "xhs:x1",
  102. "canonical_url": "https://xhs.test/1",
  103. "content_type": "图文",
  104. "content_mode": "image_post",
  105. "title": "更新标题",
  106. "author_name": None,
  107. "published_at": None,
  108. "body_text": "完整正文",
  109. "raw_summary": None,
  110. "status": "candidate",
  111. "source_payload": {},
  112. "metadata": {},
  113. "error_message": None,
  114. "created_at": None,
  115. "updated_at": None,
  116. }
  117. )
  118. item = repo.upsert_candidate_item(
  119. platform="xiaohongshu",
  120. platform_item_id="x1",
  121. unique_key="xhs:x1",
  122. canonical_url="https://xhs.test/1",
  123. content_type="图文",
  124. content_mode="image_post",
  125. title="更新标题",
  126. body_text="完整正文",
  127. )
  128. lookup_sql, lookup_params = repo.one_or_none_calls[0]
  129. update_sql, update_params = repo.one_calls[0]
  130. assert "WHERE unique_key = %s" in lookup_sql
  131. assert lookup_params == ("xhs:x1",)
  132. assert "source_payload =" not in update_sql
  133. assert "unique_key = COALESCE(%s, unique_key)" in update_sql
  134. assert "content_mode = %s" in update_sql
  135. assert "body_text = %s" in update_sql
  136. assert "INSERT INTO candidate_items" not in update_sql
  137. assert update_params[2] == "xhs:x1"
  138. assert update_params[5] == "image_post"
  139. assert update_params[8] == "完整正文"
  140. assert item.unique_key == "xhs:x1"
  141. assert item.content_mode == "image_post"
  142. assert item.body_text == "完整正文"
  143. def test_postgres_query_detail_loads_media_and_classifications_only_when_items_exist():
  144. run_id = uuid4()
  145. query_id = uuid4()
  146. item_id = uuid4()
  147. repo = RecordingPostgresRepo()
  148. repo.one_results.append({"id": query_id, "query_text": "q", "status": "ready"})
  149. repo.all_results.extend(
  150. [
  151. [{"id": uuid4(), "run_id": run_id, "query_id": query_id, "platform": "weixin"}],
  152. [{"id": item_id, "platform": "weixin", "query_id": query_id, "status": "candidate"}],
  153. [{"id": uuid4(), "item_id": item_id, "media_type": "image", "status": "done"}],
  154. [{"id": uuid4(), "item_id": item_id, "status": "classified"}],
  155. [{"item_id": item_id, "decode_status": "decoded", "payload_count": 2}],
  156. ]
  157. )
  158. detail = repo.get_query_detail(run_id=run_id, query_id=query_id)
  159. assert len(repo.all_calls) == 5
  160. assert "SELECT * FROM media_assets" in repo.all_calls[2][0]
  161. assert "SELECT * FROM item_classifications" in repo.all_calls[3][0]
  162. assert "FROM decode_results dr" in repo.all_calls[4][0]
  163. assert detail["items"][0]["id"] == item_id
  164. assert detail["media_assets"][0]["item_id"] == item_id
  165. assert detail["decode_summaries"][0]["payload_count"] == 2
  166. def test_postgres_attach_existing_candidate_updates_query_job_and_metadata():
  167. item_id = uuid4()
  168. job_id = uuid4()
  169. query_id = uuid4()
  170. repo = RecordingPostgresRepo()
  171. repo.one_results.append(
  172. {
  173. "id": item_id,
  174. "job_id": job_id,
  175. "query_id": query_id,
  176. "platform": "weixin",
  177. "platform_item_id": "wx1",
  178. "unique_key": "wx:key",
  179. "canonical_url": "https://mp.weixin.qq.com/s?...",
  180. "content_mode": "article",
  181. "body_text": None,
  182. "status": "candidate",
  183. "source_payload": {},
  184. "metadata": {"acquisition_match_status": "existing"},
  185. "created_at": None,
  186. "updated_at": None,
  187. }
  188. )
  189. item = repo.attach_existing_candidate_item(
  190. item_id,
  191. job_id=job_id,
  192. query_id=query_id,
  193. metadata={"acquisition_match_status": "existing"},
  194. )
  195. sql, params = repo.one_calls[0]
  196. assert "UPDATE candidate_items SET" in sql
  197. assert "metadata = metadata || %s" in sql
  198. assert params[0] == job_id
  199. assert params[1] == query_id
  200. assert params[3] == item_id
  201. assert item.metadata["acquisition_match_status"] == "existing"