from __future__ import annotations import json from pathlib import Path import pytest from fastapi.testclient import TestClient from ozan_radio import server @pytest.fixture def client(tmp_path: Path, monkeypatch): songs = tmp_path / "songs" songs.mkdir() gateway = tmp_path / "gateway" gateway.mkdir() (gateway / "index.html").write_text("", encoding="utf-8") (tmp_path / "vocal_cues.json").write_text( json.dumps( { "tracks": { "11111111": { "title": "Cue Test", "skip_intro_sec": 20, "phrases": [], } } } ), encoding="utf-8", ) (tmp_path / "settings.json").write_text( json.dumps( { "taste": {"summary": "test taste", "genres": ["dub"]}, "playback": {"shuffle": True, "mix_existing_and_new": True, "new_song_chance": 0.35}, "limits": {"max_new_songs_per_day": 10}, "costs": { "lyria_pro_usd": 0.08, "lyria_clip_usd": 0.04, "deepseek_per_track_usd": 0.002, }, "lyria": {"model": "lyria-3-pro-preview", "vocal_mode": "mix"}, } ), encoding="utf-8", ) monkeypatch.setattr(server, "_config", None) monkeypatch.setattr(server, "_queue", None) monkeypatch.setattr(server, "_generating", False) monkeypatch.setattr( "ozan_radio.config.Config.from_env", lambda: type( "Cfg", (), { "output_dir": songs, "radio_host": "127.0.0.1", "radio_port": 8787, "lyria_model": "lyria-3-pro-preview", "gemini_api_key": None, "deepseek_api_key": None, "deepseek_base_url": "https://api.deepseek.com/v1", "deepseek_model": "deepseek-chat", "require_gemini": lambda self: (_ for _ in ()).throw(RuntimeError("no key")), "require_deepseek": lambda self: (_ for _ in ()).throw(RuntimeError("no key")), }, )(), ) monkeypatch.setattr("ozan_radio.radio_settings._settings_path", lambda repo_root=None: tmp_path / "settings.json") monkeypatch.setattr("ozan_radio.settings._settings_path", lambda repo_root=None: tmp_path / "settings.json") monkeypatch.setattr("ozan_radio.web_playlist.Path", Path) monkeypatch.setattr( "ozan_radio.server._vocal_cues_path", lambda: tmp_path / "vocal_cues.json", ) monkeypatch.setattr( "ozan_radio.server.Path", Path, ) return TestClient(server.app) def test_root_lists_lyria_endpoint(client: TestClient): r = client.get("/") assert r.status_code == 200 assert r.json()["endpoints"]["lyria"] == "/api/lyria" def test_lyria_capabilities_without_api_key(client: TestClient): r = client.get("/api/lyria") assert r.status_code == 200 body = r.json() assert body["api"]["key_configured"] is False assert "capabilities" in body assert len(body["capabilities"]["vocal_modes"]) >= 3 def test_vocal_cues_for_track(client: TestClient): r = client.get("/api/vocal-cues/11111111") assert r.status_code == 200 assert r.json()["title"] == "Cue Test" def test_vocal_cues_missing_returns_404(client: TestClient): assert client.get("/api/vocal-cues/missing").status_code == 404 def test_settings_get_and_patch(client: TestClient): r = client.get("/api/settings") assert r.status_code == 200 assert r.json()["lyria"]["vocal_mode"] == "mix" patch = client.patch("/api/settings", json={"lyria": {"vocal_mode": "instrumental"}}) assert patch.status_code == 200 assert patch.json()["lyria"]["vocal_mode"] == "instrumental" def test_stats_endpoint(client: TestClient): r = client.get("/api/stats") assert r.status_code == 200 assert "today" in r.json() assert "generation" in r.json() def test_unlimited_daily_limit_when_max_is_zero(tmp_path: Path, monkeypatch): (tmp_path / "settings.json").write_text( json.dumps( { "limits": {"max_new_songs_per_day": 0}, "costs": {"lyria_pro_usd": 0.08, "lyria_clip_usd": 0.04}, "lyria": {"model": "lyria-3-pro-preview"}, } ), encoding="utf-8", ) songs = tmp_path / "songs" songs.mkdir() monkeypatch.setattr("ozan_radio.radio_settings._settings_path", lambda repo_root=None: tmp_path / "settings.json") monkeypatch.setattr( "ozan_radio.config.Config.from_env", lambda: type("Cfg", (), {"output_dir": songs, "lyria_model": "lyria-3-pro-preview"})(), ) class FakeCfg: output_dir = songs lyria_model = "lyria-3-pro-preview" ok, budget = server._can_generate_today(FakeCfg()) # type: ignore[arg-type] assert ok is True assert budget["unlimited"] is True assert budget["remaining"] == -1