Add pytest suite, unlimited daily cap, and vocal batch generator.
Tests cover curation, Lyria, queue, and API routes. Setting max_new_songs_per_day to 0 disables the limit; generate-batch runs 20 curated multilingual vocal directions. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
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("<html></html>", 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
|
||||
Reference in New Issue
Block a user