test_gemini_video.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. """V3-M2B: GeminiVideoClient.analyze (mocked fetch + httpx)."""
  2. from __future__ import annotations
  3. import httpx
  4. from content_agent.integrations.gemini_video import (
  5. GeminiVideoClient,
  6. MissingGeminiVideoClient,
  7. )
  8. class FakeResponse:
  9. def __init__(self, content):
  10. self._content = content
  11. def raise_for_status(self):
  12. return None
  13. def json(self):
  14. return {"choices": [{"message": {"content": self._content}}]}
  15. def _client(content=None, *, post=None, fetch=None):
  16. return GeminiVideoClient(
  17. api_key="k",
  18. fetch_fn=fetch or (lambda play_url, platform: "data:video/mp4;base64,AAAA"),
  19. http_post=post or (lambda *a, **k: FakeResponse(content)),
  20. )
  21. _ITEM = {"platform": "douyin", "platform_content_id": "c1"}
  22. _MEDIA = {"play_url": "http://v/x"}
  23. _CTX = {"ext_data": {"evidence_pack": {"seed_terms": ["中医养生"]}}}
  24. def test_analyze_returns_four_fields():
  25. body = '{"fit_senior_50plus": true, "fit_confidence": 0.85, "relevance_score": 0.7, "reason": "贴切"}'
  26. result = _client(body).analyze(_ITEM, _MEDIA, _CTX)
  27. assert result == {
  28. "fit_senior_50plus": True,
  29. "fit_confidence": 0.85,
  30. "relevance_score": 0.7,
  31. "reason": "贴切",
  32. }
  33. def test_analyze_parses_json_in_markdown_fence():
  34. body = '```json\n{"fit_senior_50plus": false, "fit_confidence": 0.4, "relevance_score": 0.2, "reason": "x"}\n```'
  35. result = _client(body).analyze(_ITEM, _MEDIA, _CTX)
  36. assert result["fit_senior_50plus"] is False
  37. assert result["fit_confidence"] == 0.4
  38. def test_analyze_clamps_out_of_range_scores():
  39. body = '{"fit_senior_50plus": true, "fit_confidence": 1.7, "relevance_score": -3, "reason": "x"}'
  40. result = _client(body).analyze(_ITEM, _MEDIA, _CTX)
  41. assert result["fit_confidence"] == 1.0
  42. assert result["relevance_score"] == 0.0
  43. def test_analyze_passes_raw_save_path_when_dir_configured(tmp_path):
  44. # 2026-06-12 拍板: 配置留档目录后,analyze 按 {dir}/{run_id}/{platform_content_id}.mp4
  45. # 把路径传给 fetch_fn;未配置(默认 None)不传该 kwarg——老签名 lambda 桩零改仍可用。
  46. body = '{"fit_senior_50plus": true, "fit_confidence": 0.9, "relevance_score": 0.8, "reason": "x"}'
  47. seen = {}
  48. def fetch(play_url, platform, **kwargs):
  49. seen.update(kwargs)
  50. return "data:video/mp4;base64,AAAA"
  51. client = GeminiVideoClient(
  52. api_key="k",
  53. fetch_fn=fetch,
  54. http_post=lambda *a, **k: FakeResponse(body),
  55. raw_video_save_dir=str(tmp_path),
  56. )
  57. item = {**_ITEM, "run_id": "run_1"}
  58. client.analyze(item, _MEDIA, _CTX)
  59. assert seen["save_raw_to"] == str(tmp_path / "run_1" / "c1.mp4")
  60. result = _client(body).analyze(item, _MEDIA, _CTX)
  61. assert result["fit_senior_50plus"] is True
  62. def test_analyze_no_play_url_returns_fail():
  63. result = _client("{}").analyze(_ITEM, {}, _CTX)
  64. assert result["status"] == "failed"
  65. assert result["reason"] == "no_play_url"
  66. def test_analyze_video_fetch_failure_returns_fail():
  67. def boom(play_url, platform):
  68. raise RuntimeError("dl")
  69. result = _client("{}", fetch=boom).analyze(_ITEM, _MEDIA, _CTX)
  70. assert result["status"] == "failed"
  71. assert "video_fetch_failed" in result["reason"]
  72. def test_analyze_http_error_returns_fail():
  73. def post(*a, **k):
  74. raise httpx.ConnectError("boom")
  75. result = _client(post=post).analyze(_ITEM, _MEDIA, _CTX)
  76. assert result["status"] == "failed"
  77. assert "gemini_http_error" in result["reason"]
  78. def test_analyze_bad_json_returns_fail():
  79. result = _client("not-json").analyze(_ITEM, _MEDIA, _CTX)
  80. assert result["status"] == "failed"
  81. assert "gemini_response_invalid" in result["reason"]
  82. def test_from_env_missing_key_returns_missing_client():
  83. client = GeminiVideoClient.from_env({})
  84. assert isinstance(client, MissingGeminiVideoClient)
  85. assert client.analyze(_ITEM, _MEDIA, _CTX)["status"] == "failed"