test_platform_access.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314
  1. from content_agent.business_modules import platform_access
  2. from content_agent.errors import ContentAgentError, ErrorCode
  3. from content_agent.integrations.crawapi_http import CrawapiBusinessError
  4. class DuplicateContentClient:
  5. def search(self, search_query):
  6. return [
  7. {
  8. "content_discovery_id": f"{search_query['search_query_id']}_content_001",
  9. "search_query_id": search_query["search_query_id"],
  10. "platform_content_id": "7601814454925298994",
  11. "description": "重复内容",
  12. }
  13. ]
  14. def test_platform_access_deduplicates_same_content_across_search_queries():
  15. search_queries = [
  16. {
  17. "search_query_id": "q_001",
  18. "search_query": "对比分析",
  19. "search_query_generation_method": "item_single",
  20. },
  21. {
  22. "search_query_id": "q_002",
  23. "search_query": "对比分析 人物故事",
  24. "search_query_generation_method": "item_single",
  25. },
  26. ]
  27. result = platform_access.run(search_queries, DuplicateContentClient())
  28. results = result["platform_results"]
  29. assert len(results) == 1
  30. assert results[0]["search_query_id"] == "q_001"
  31. assert results[0]["search_query"] == "对比分析"
  32. assert results[0]["matched_search_query_ids"] == ["q_001", "q_002"]
  33. assert results[0]["matched_search_queries"] == ["对比分析", "对比分析 人物故事"]
  34. class UniqueContentClient:
  35. def search(self, search_query):
  36. return [
  37. {
  38. "content_discovery_id": f"{search_query['search_query_id']}_content_001",
  39. "search_query_id": search_query["search_query_id"],
  40. "platform_content_id": f"platform_{search_query['search_query_id']}",
  41. "description": search_query["search_query"],
  42. }
  43. ]
  44. def test_platform_access_preserves_llm_variant_generation_method():
  45. search_queries = [
  46. {
  47. "search_query_id": "q_001",
  48. "search_query": "对比分析",
  49. "search_query_generation_method": "item_single",
  50. },
  51. {
  52. "search_query_id": "q_002",
  53. "search_query": "人物叙事素材",
  54. "search_query_generation_method": "llm_variant",
  55. "llm_variant_of": "q_001",
  56. },
  57. ]
  58. results = platform_access.run(search_queries, UniqueContentClient())["platform_results"]
  59. assert [result["search_query_generation_method"] for result in results] == [
  60. "item_single",
  61. "llm_variant",
  62. ]
  63. assert results[1]["search_query_id"] == "q_002"
  64. assert results[1]["search_query"] == "人物叙事素材"
  65. class FailingThenSuccessfulClient:
  66. def search(self, search_query):
  67. if search_query["search_query_id"] == "q_001":
  68. raise RuntimeError("temporary platform failure")
  69. return [
  70. {
  71. "content_discovery_id": f"{search_query['search_query_id']}_content_001",
  72. "search_query_id": search_query["search_query_id"],
  73. "platform_content_id": "7601814454925298995",
  74. "description": "成功内容",
  75. }
  76. ]
  77. def test_platform_access_keeps_running_after_single_query_failure():
  78. search_queries = [
  79. {
  80. "search_query_id": "q_001",
  81. "search_query": "接口失败",
  82. "search_query_generation_method": "item_single",
  83. },
  84. {
  85. "search_query_id": "q_002",
  86. "search_query": "正常召回",
  87. "search_query_generation_method": "llm_variant",
  88. },
  89. ]
  90. result = platform_access.run(search_queries, FailingThenSuccessfulClient())
  91. assert [item["platform_content_id"] for item in result["platform_results"]] == [
  92. "7601814454925298995"
  93. ]
  94. assert result["query_failures"][0]["search_query_id"] == "q_001"
  95. assert result["query_failures"][0]["error_code"] == ErrorCode.PLATFORM_REQUEST_FAILED.value
  96. class AlwaysFailingClient:
  97. def search(self, search_query):
  98. raise RuntimeError("platform unavailable")
  99. def test_platform_access_fails_run_when_all_queries_fail():
  100. search_queries = [
  101. {
  102. "search_query_id": "q_001",
  103. "search_query": "接口失败",
  104. "search_query_generation_method": "item_single",
  105. },
  106. {
  107. "search_query_id": "q_002",
  108. "search_query": "仍然失败",
  109. "search_query_generation_method": "llm_variant",
  110. }
  111. ]
  112. try:
  113. platform_access.run(search_queries, AlwaysFailingClient())
  114. except ContentAgentError as exc:
  115. assert exc.error_code == ErrorCode.PLATFORM_REQUEST_FAILED
  116. assert [failure["search_query_id"] for failure in exc.detail["query_failures"]] == [
  117. "q_001",
  118. "q_002",
  119. ]
  120. for failure in exc.detail["query_failures"]:
  121. assert failure["status"] == "failed"
  122. assert failure["error_code"] == ErrorCode.PLATFORM_REQUEST_FAILED.value
  123. assert failure["message"] == "platform query failed"
  124. assert failure["error_detail"]["exception_type"] == "RuntimeError"
  125. else:
  126. raise AssertionError("expected platform request failure")
  127. class RateLimitedClient:
  128. def search(self, search_query):
  129. raise ContentAgentError(
  130. ErrorCode.PLATFORM_RATE_LIMITED,
  131. "crawapi keyword_search failed: rate_limited",
  132. {"operation": "keyword_search", "status_code": 429},
  133. )
  134. class HealthyClient:
  135. def search(self, search_query):
  136. return [
  137. {
  138. "content_discovery_id": f"{search_query['search_query_id']}_content_001",
  139. "search_query_id": search_query["search_query_id"],
  140. "platform_content_id": "7601814454925298001",
  141. "description": "正常内容",
  142. }
  143. ]
  144. class RuntimeErrorClient:
  145. def search(self, search_query):
  146. raise RuntimeError("crawapi keyword_search failed: HTTP 500")
  147. class SplitClient:
  148. """First query rate limited, second query succeeds."""
  149. def search(self, search_query):
  150. if search_query["search_query_id"] == "q_001":
  151. return RateLimitedClient().search(search_query)
  152. return HealthyClient().search(search_query)
  153. def test_platform_access_preserves_rate_limited_error_code():
  154. search_queries = [
  155. {"search_query_id": "q_001", "search_query": "被限流", "search_query_generation_method": "item_single"},
  156. {"search_query_id": "q_002", "search_query": "正常", "search_query_generation_method": "item_single"},
  157. ]
  158. result = platform_access.run(search_queries, SplitClient())
  159. failure = result["query_failures"][0]
  160. assert failure["search_query_id"] == "q_001"
  161. assert failure["error_code"] == ErrorCode.PLATFORM_RATE_LIMITED.value
  162. assert failure["message"] == "crawapi keyword_search failed: rate_limited"
  163. assert failure["error_detail"]["operation"] == "keyword_search"
  164. assert len(result["platform_results"]) == 1
  165. def test_platform_access_counts_runtime_error_as_platform_request_failed():
  166. search_queries = [
  167. {"search_query_id": "q_001", "search_query": "普通失败", "search_query_generation_method": "item_single"},
  168. {"search_query_id": "q_002", "search_query": "正常", "search_query_generation_method": "item_single"},
  169. ]
  170. class MixedClient:
  171. def search(self, search_query):
  172. if search_query["search_query_id"] == "q_001":
  173. raise RuntimeError("crawapi keyword_search failed: HTTP 500")
  174. return HealthyClient().search(search_query)
  175. result = platform_access.run(search_queries, MixedClient())
  176. failure = result["query_failures"][0]
  177. assert failure["error_code"] == ErrorCode.PLATFORM_REQUEST_FAILED.value
  178. assert failure["error_detail"]["exception_type"] == "RuntimeError"
  179. assert (
  180. failure["error_detail"]["exception_message"]
  181. == "crawapi keyword_search failed: HTTP 500"
  182. )
  183. def test_platform_access_preserves_structured_crawapi_business_detail():
  184. search_queries = [
  185. {"search_query_id": "q_001", "search_query": "历史人物评价", "search_query_generation_method": "item_single"},
  186. {"search_query_id": "q_002", "search_query": "正常", "search_query_generation_method": "item_single"},
  187. ]
  188. class MixedClient:
  189. def search(self, search_query):
  190. if search_query["search_query_id"] == "q_001":
  191. raise CrawapiBusinessError(
  192. "crawapi keyword_search failed: business_error code=10000 message=未知错误",
  193. {
  194. "operation": "keyword_search",
  195. "business_code": "10000",
  196. "business_message": "未知错误",
  197. "business_data": {"trace_id": "trace-001", "upstream": "opaque"},
  198. "trace_refs": {"x-request-id": "req-001"},
  199. },
  200. )
  201. return HealthyClient().search(search_query)
  202. result = platform_access.run(search_queries, MixedClient())
  203. failure = result["query_failures"][0]
  204. assert failure["error_code"] == ErrorCode.PLATFORM_REQUEST_FAILED.value
  205. assert failure["message"] == "platform query failed"
  206. assert failure["error_detail"]["exception_type"] == "CrawapiBusinessError"
  207. assert failure["error_detail"]["operation"] == "keyword_search"
  208. assert failure["error_detail"]["business_code"] == "10000"
  209. assert failure["error_detail"]["business_data"] == {
  210. "trace_id": "trace-001",
  211. "upstream": "opaque",
  212. }
  213. assert failure["error_detail"]["trace_refs"] == {"x-request-id": "req-001"}
  214. def test_platform_access_records_runtime_error_cause_message():
  215. class CausedFailureClient:
  216. def search(self, search_query):
  217. cause = ValueError("upstream body was not json")
  218. raise RuntimeError("crawapi keyword_search failed: bad_json") from cause
  219. search_queries = [
  220. {
  221. "search_query_id": "q_001",
  222. "search_query": "坏响应",
  223. "search_query_generation_method": "item_single",
  224. }
  225. ]
  226. try:
  227. platform_access.run(search_queries, CausedFailureClient())
  228. except ContentAgentError as exc:
  229. failure = exc.detail["query_failures"][0]
  230. assert failure["error_detail"]["exception_type"] == "RuntimeError"
  231. assert (
  232. failure["error_detail"]["exception_message"]
  233. == "crawapi keyword_search failed: bad_json"
  234. )
  235. assert failure["error_detail"]["cause_exception_type"] == "ValueError"
  236. assert failure["error_detail"]["cause_message"] == "upstream body was not json"
  237. else:
  238. raise AssertionError("expected platform request failure")
  239. def test_platform_access_accepts_search_only_client():
  240. class SearchOnlyClient:
  241. def search(self, search_query):
  242. return [
  243. {
  244. "content_discovery_id": f"{search_query['search_query_id']}_content_001",
  245. "search_query_id": search_query["search_query_id"],
  246. "platform_content_id": "ks_001",
  247. "description": "搜索协议只要求 search",
  248. }
  249. ]
  250. search_queries = [
  251. {
  252. "search_query_id": "q_001",
  253. "search_query": "快手祝福",
  254. "search_query_generation_method": "item_single",
  255. }
  256. ]
  257. result = platform_access.run(search_queries, SearchOnlyClient())
  258. assert result["platform_results"][0]["platform_content_id"] == "ks_001"
  259. assert result["query_failures"] == []