import pytest import respx from httpx import Response from app.pipeline import metadata @pytest.mark.asyncio @respx.mock async def test_metadata_calls_claude_and_parses_json(monkeypatch): monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key") payload = { "content": [{"type": "text", "text": '{"title":"[Lo-fi] Drive | 85BPM",' '"description":"chill","tags":["lofi","85bpm"],' '"category_id":10}'}] } respx.post("https://api.anthropic.com/v1/messages").mock( return_value=Response(200, json=payload) ) result = await metadata.generate( track={"title": "Drive", "genre": "lo-fi", "bpm": 85, "key": "C", "scale": "minor", "moods": ["chill"], "instruments": ["piano"]}, template={"title": "[{genre}] {title} | {bpm}BPM", "description": "{title}\n", "tags": [], "category_id": 10}, trend_keywords=["lofi", "study"], feedback="", ) assert result["title"].startswith("[Lo-fi]") assert "lofi" in result["tags"] assert result["used_fallback"] is False @pytest.mark.asyncio async def test_metadata_fallback_when_no_api_key(monkeypatch): monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) result = await metadata.generate( track={"title": "Drive", "genre": "lo-fi", "bpm": 85, "key": "C", "scale": "minor", "moods": [], "instruments": []}, template={"title": "[{genre}] {title} | {bpm}BPM", "description": "{title}", "tags": ["lofi"], "category_id": 10}, trend_keywords=[], ) # 템플릿 변수 그대로 치환된 폴백 assert result["title"] == "[lo-fi] Drive | 85BPM" assert result["used_fallback"] is True @pytest.mark.asyncio @respx.mock async def test_metadata_includes_feedback_in_prompt(monkeypatch): import json monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key") captured = {} def hook(req): captured["body"] = json.loads(req.content) return Response(200, json={"content": [{"type": "text", "text": '{"title":"x","description":"y","tags":[],"category_id":10}'}]}) respx.post("https://api.anthropic.com/v1/messages").mock(side_effect=hook) await metadata.generate( track={"title": "X", "genre": "lo-fi", "bpm": 85, "key": "C", "scale": "minor", "moods": [], "instruments": []}, template={"title": "{title}", "description": "{title}", "tags": [], "category_id": 10}, trend_keywords=[], feedback="제목을 짧게", ) assert "제목을 짧게" in str(captured["body"]) @pytest.mark.asyncio @respx.mock async def test_metadata_falls_back_on_api_error(monkeypatch): monkeypatch.setenv("ANTHROPIC_API_KEY", "test-key") respx.post("https://api.anthropic.com/v1/messages").mock( return_value=Response(500) ) result = await metadata.generate( track={"title": "Drive", "genre": "lo-fi", "bpm": 85, "key": "C", "scale": "minor", "moods": [], "instruments": []}, template={"title": "[{genre}] {title}", "description": "x", "tags": ["lofi"], "category_id": 10}, trend_keywords=[], ) assert result["used_fallback"] is True assert "Drive" in result["title"] @pytest.mark.asyncio @respx.mock async def test_metadata_with_tracks_includes_chapter_format(monkeypatch): monkeypatch.setenv("ANTHROPIC_API_KEY", "k") captured = {} def hook(req): import json as _json captured["body"] = _json.loads(req.content) return Response(200, json={"content": [{"type": "text", "text": '{"title":"Lo-Fi Mix 3 Tracks","description":"Track 1: [00:00] T1\\nTrack 2: [03:00] T2",' '"tags":["lofi","mix"],"category_id":10}'}]}) respx.post("https://api.anthropic.com/v1/messages").mock(side_effect=hook) result = await metadata.generate( track={"title": "Mix", "genre": "mix", "duration_sec": 600, "moods": []}, template={"title": "{title}", "description": "{title}", "tags": [], "category_id": 10}, trend_keywords=[], tracks=[ {"id": 1, "title": "T1", "start_offset_sec": 0, "duration_sec": 180}, {"id": 2, "title": "T2", "start_offset_sec": 180, "duration_sec": 200}, {"id": 3, "title": "T3", "start_offset_sec": 380, "duration_sec": 220}, ], ) body_str = str(captured["body"]) assert "T1" in body_str and "T2" in body_str and "T3" in body_str assert "00:00" in body_str assert result["used_fallback"] is False @pytest.mark.asyncio async def test_metadata_fallback_with_tracks(monkeypatch): """API 키 없을 때 폴백에서도 트랙 챕터 포함.""" monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False) result = await metadata.generate( track={"title": "Mix", "genre": "mix", "duration_sec": 600, "moods": []}, template={"title": "{title}", "description": "{title}", "tags": [], "category_id": 10}, trend_keywords=[], tracks=[ {"id": 1, "title": "T1", "start_offset_sec": 0, "duration_sec": 180}, {"id": 2, "title": "T2", "start_offset_sec": 180, "duration_sec": 200}, ], ) assert result["used_fallback"] is True assert "00:00" in result["description"] assert "T1" in result["description"] assert "T2" in result["description"] def test_format_chapters_under_hour(): from app.pipeline.metadata import _format_chapters out = _format_chapters([ {"start_offset_sec": 0, "title": "T1"}, {"start_offset_sec": 180, "title": "T2"}, ]) assert "00:00 T1" in out assert "03:00 T2" in out def test_format_chapters_over_hour(): from app.pipeline.metadata import _format_chapters out = _format_chapters([ {"start_offset_sec": 0, "title": "T1"}, {"start_offset_sec": 3700, "title": "T2"}, ]) assert "00:00 T1" in out assert "01:01:40 T2" in out