import pytest import respx from httpx import Response from app.pipeline import review @pytest.mark.asyncio @respx.mock async def test_review_returns_pass_when_above_threshold(monkeypatch): monkeypatch.setenv("ANTHROPIC_API_KEY", "k") body = {"content": [{"type": "text", "text": '{"metadata_quality":{"score":80,"notes":"x"},' '"policy_compliance":{"score":90,"issues":[]},' '"viewer_experience":{"score":75,"notes":"y"},' '"trend_alignment":{"score":70,"matched_keywords":["lofi"]},' '"summary":"good"}'}]} respx.post("https://api.anthropic.com/v1/messages").mock(return_value=Response(200, json=body)) result = await review.run_4_axis( pipeline={"id": 1}, track={"title": "x", "genre": "lo-fi", "bpm": 85}, video_meta={"length_sec": 120, "resolution": "1920x1080"}, metadata={"title": "Y", "description": "Z", "tags": ["lofi"], "category_id": 10}, thumbnail_url="/m/x.jpg", trend_top=["lofi"], weights={"meta": 25, "policy": 30, "viewer": 25, "trend": 20}, threshold=60, ) assert result["verdict"] == "pass" expected_total = 0.25 * 80 + 0.30 * 90 + 0.25 * 75 + 0.20 * 70 assert result["weighted_total"] == pytest.approx(expected_total, abs=0.01) @pytest.mark.asyncio @respx.mock async def test_review_fail_below_threshold(monkeypatch): monkeypatch.setenv("ANTHROPIC_API_KEY", "k") body = {"content": [{"type": "text", "text": '{"metadata_quality":{"score":40,"notes":"x"},' '"policy_compliance":{"score":50,"issues":[]},' '"viewer_experience":{"score":30,"notes":"y"},' '"trend_alignment":{"score":20,"matched_keywords":[]},' '"summary":"bad"}'}]} respx.post("https://api.anthropic.com/v1/messages").mock(return_value=Response(200, json=body)) result = await review.run_4_axis( pipeline={"id": 2}, track={"title": "x", "genre": "lo-fi", "bpm": 85}, video_meta={"length_sec": 120, "resolution": "1920x1080"}, metadata={"title": "Y", "description": "Z", "tags": [], "category_id": 10}, thumbnail_url="/m/x.jpg", trend_top=[], weights={"meta": 25, "policy": 30, "viewer": 25, "trend": 20}, threshold=60, ) assert result["verdict"] == "fail" @pytest.mark.asyncio @respx.mock async def test_review_heuristic_fallback_on_llm_error(monkeypatch): monkeypatch.setenv("ANTHROPIC_API_KEY", "k") respx.post("https://api.anthropic.com/v1/messages").mock(return_value=Response(500)) result = await review.run_4_axis( pipeline={"id": 3}, track={"title": "x", "genre": "lo-fi", "bpm": 85, "duration_sec": 120}, video_meta={"length_sec": 120, "resolution": "1920x1080"}, metadata={"title": "Y" * 30, "description": "Z" * 200, "tags": ["a", "b", "c", "d", "e"], "category_id": 10}, thumbnail_url="/m/x.jpg", trend_top=["lofi"], weights={"meta": 25, "policy": 30, "viewer": 25, "trend": 20}, threshold=60, ) assert result["used_fallback"] is True assert "weighted_total" in result @pytest.mark.asyncio async def test_review_heuristic_when_no_api_key(monkeypatch): monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) result = await review.run_4_axis( pipeline={"id": 4}, track={"title": "x", "genre": "lo-fi", "bpm": 85, "duration_sec": 120}, video_meta={"length_sec": 120, "resolution": "1920x1080"}, metadata={"title": "Test Title", "description": "Description here, more text " * 5, "tags": ["lofi", "study", "chill", "ambient", "instrumental"], "category_id": 10}, thumbnail_url="/m/x.jpg", trend_top=["lofi"], weights={"meta": 25, "policy": 30, "viewer": 25, "trend": 20}, threshold=60, ) assert result["used_fallback"] is True # 휴리스틱: 좋은 메타+영상길이 일치+태그 트렌드 겹침 → pass 기대 assert result["verdict"] == "pass"