Files
ozan 6e92841352 Fix generation JSON/Lyria errors, add Winamp player, and ship Echoes of the Sahel.
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>
2026-06-07 16:38:52 +01:00

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