6e92841352
Harden DeepSeek JSON parsing with retry, pre-sanitize Lyria prompts, and instrumental fallback. Add pure HTML Winamp skin at /winamp with playlist export support. Co-authored-by: Cursor <cursoragent@cursor.com>
163 lines
5.3 KiB
Python
163 lines
5.3 KiB
Python
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")
|
|
(gateway / "winamp.html").write_text("<html><title>Winamp</title></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"
|
|
assert r.json()["endpoints"]["winamp"] == "/winamp"
|
|
|
|
|
|
def test_winamp_page(client: TestClient):
|
|
r = client.get("/winamp")
|
|
assert r.status_code == 200
|
|
assert "Winamp" in r.text
|
|
|
|
|
|
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
|