mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-08-10 18:59:56 +00:00
The single largest handler in server.py — the 902-line /ws/highway/{filename}
chart streamer — plus its 3 exclusive helpers (_pick_smart_arrangement,
_sanitize_authors, _sanitized_song_offset). server.py: 9,008 -> 8,003 (-1,005,
the biggest single R3 cut).
Clean move despite the size: the handler's only server-module deps are the 4
path constants + 3 exclusive helpers + app + log. Everything else it uses is
either NESTED inside the handler (_evict_audio_cache, _fill_scale_degree,
_manifest_entries, _tone_names, _xml_rank, _send_keepalives) or imported from
the shared lib modules (song/audio/sloppak/drums/notation/dlc_paths/metadata_db).
- Path constants read through the appstate seam: added static_dir /
sloppak_cache_dir / audio_cache_dir slots (config_dir already there);
server.py configures them. Bodies otherwise verbatim (@app.websocket ->
@router.websocket, PATHS -> appstate.*, log -> module logger).
- sloppak_cache_dir IS setattr-patched, so the 3 test_highway_ws_* suites now
also `setattr(appstate, "sloppak_cache_dir", ...)` next to their existing
server patch. _sanitize_authors unit tests import it from routers.ws_highway
(it moved). No other test churn.
- Removed 23 now-dead imports from server.py (song/audio/drums/notation/
bisect/contextvars/structlog/WebSocket*/_arr_smart_sort_key) — diffed against
the origin/main unused-import baseline so only NEWLY-dead ones went.
owns_tmp (assigned, never read) moved verbatim — it's pre-existing dead on
origin/main too; left as-is to keep the move faithful.
Verified: route table identical to origin/main (143, paths/methods/order);
handler body verbatim spot-checked; pyflakes clean (server has no new
undefined/dead); pytest 2400 passed; packaging guard 53; eslint 0. Boot smoke:
the highway WS streams the full chart (song_info/beats/sections/notes/chords/
notation/anchors/drum_tab -> ready) BYTE-for-byte the same message sequence as
origin/main across arrangements 0/1/2, zero tracebacks.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
167 lines
5.8 KiB
Python
167 lines
5.8 KiB
Python
"""Tests for feedpak contributor credits on the highway.
|
|
|
|
Covers the `_sanitize_authors` helper (unit) and the `song_info` WebSocket
|
|
frame carrying the manifest `authors` list end-to-end (integration). The
|
|
frontend uses a non-empty `authors` list to gate a credits overlay shown when
|
|
a song loads, so loose/archive/synthetic plays must surface `[]`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import json
|
|
import sys
|
|
|
|
import pytest
|
|
import yaml
|
|
from fastapi.testclient import TestClient
|
|
|
|
from routers.ws_highway import _sanitize_authors
|
|
|
|
|
|
# ── _sanitize_authors unit tests ────────────────────────────────────────────
|
|
|
|
|
|
@pytest.fixture()
|
|
def server_mod(monkeypatch, tmp_path):
|
|
monkeypatch.setenv("CONFIG_DIR", str(tmp_path / "config"))
|
|
monkeypatch.setenv("DLC_DIR", str(tmp_path / "dlc"))
|
|
(tmp_path / "dlc").mkdir()
|
|
sys.modules.pop("server", None)
|
|
mod = importlib.import_module("server")
|
|
yield mod
|
|
conn = getattr(getattr(mod, "meta_db", None), "conn", None)
|
|
if conn is not None:
|
|
getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)()
|
|
conn.close()
|
|
|
|
|
|
def test_sanitize_authors_valid(server_mod):
|
|
out = _sanitize_authors(
|
|
{
|
|
"authors": [
|
|
{"name": "Azure", "role": "charter", "email": "a@b.c", "url": "x"},
|
|
{"name": "Bob Lee", "role": "editor"},
|
|
{"name": "Solo"},
|
|
]
|
|
}
|
|
)
|
|
# name + role only; email/url dropped; missing role → None.
|
|
assert out == [
|
|
{"name": "Azure", "role": "charter"},
|
|
{"name": "Bob Lee", "role": "editor"},
|
|
{"name": "Solo", "role": None},
|
|
]
|
|
|
|
|
|
def test_sanitize_authors_skips_malformed(server_mod):
|
|
out = _sanitize_authors(
|
|
{
|
|
"authors": [
|
|
{"name": ""}, # blank name → skipped
|
|
{"name": " "}, # whitespace name → skipped
|
|
{"role": "mixer"}, # no name → skipped
|
|
"not-a-dict", # non-dict → skipped
|
|
{"name": " Kept ", "role": " arranger "}, # trimmed
|
|
]
|
|
}
|
|
)
|
|
assert out == [{"name": "Kept", "role": "arranger"}]
|
|
|
|
|
|
@pytest.mark.parametrize("manifest", [None, {}, {"authors": None}, {"authors": "x"}, "nope"])
|
|
def test_sanitize_authors_absent_or_nonlist(server_mod, manifest):
|
|
assert _sanitize_authors(manifest) == []
|
|
|
|
|
|
# ── song_info WS integration ────────────────────────────────────────────────
|
|
|
|
|
|
def _write_sloppak(dlc_root, *, authors):
|
|
pak = dlc_root / "authortest.sloppak"
|
|
pak.mkdir()
|
|
(pak / "arrangements").mkdir()
|
|
(pak / "arrangements" / "lead.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"notes": [],
|
|
"chords": [],
|
|
"anchors": [],
|
|
"handshapes": [],
|
|
"templates": [],
|
|
"beats": [{"time": 0.0, "measure": 1}],
|
|
"sections": [{"name": "intro", "number": 1, "time": 0.0}],
|
|
}
|
|
)
|
|
)
|
|
manifest = {
|
|
"title": "Author Test",
|
|
"artist": "Tester",
|
|
"album": "",
|
|
"year": 2026,
|
|
"duration": 10.0,
|
|
"arrangements": [{"id": "lead", "name": "Lead", "file": "arrangements/lead.json"}],
|
|
"stems": [],
|
|
}
|
|
if authors is not None:
|
|
manifest["authors"] = authors
|
|
(pak / "manifest.yaml").write_text(yaml.safe_dump(manifest, sort_keys=False))
|
|
return pak
|
|
|
|
|
|
@pytest.fixture()
|
|
def make_client(tmp_path, monkeypatch):
|
|
def _make():
|
|
monkeypatch.setenv("CONFIG_DIR", str(tmp_path / "config"))
|
|
monkeypatch.setenv("DLC_DIR", str(tmp_path / "dlc"))
|
|
monkeypatch.setenv("FEEDBACK_SYNC_STARTUP", "1")
|
|
sys.modules.pop("server", None)
|
|
server = importlib.import_module("server")
|
|
monkeypatch.setattr(server, "load_plugins", lambda *a, **kw: None)
|
|
monkeypatch.setattr(server, "startup_scan", lambda: None)
|
|
monkeypatch.setattr(server, "SLOPPAK_CACHE_DIR", tmp_path / "cache")
|
|
# ws_highway reads the cache dir through the appstate seam now.
|
|
import appstate as _appstate
|
|
monkeypatch.setattr(_appstate, "sloppak_cache_dir", tmp_path / "cache")
|
|
return server
|
|
|
|
(tmp_path / "dlc").mkdir()
|
|
yield _make
|
|
server = sys.modules.get("server")
|
|
conn = getattr(getattr(server, "meta_db", None), "conn", None)
|
|
if conn is not None:
|
|
getattr(__import__("sys").modules.get("server"), "_join_background_db_threads", lambda: None)()
|
|
conn.close()
|
|
|
|
|
|
def _song_info(client, path):
|
|
with client.websocket_connect(path) as ws:
|
|
for _ in range(200):
|
|
msg = ws.receive_json()
|
|
if msg.get("error"):
|
|
raise AssertionError(f"WS error frame: {msg}")
|
|
if msg.get("type") == "song_info":
|
|
return msg
|
|
if msg.get("type") == "ready":
|
|
break
|
|
raise AssertionError("no song_info frame received")
|
|
|
|
|
|
def test_song_info_carries_authors(make_client):
|
|
server = make_client()
|
|
_write_sloppak(
|
|
server._get_dlc_dir(),
|
|
authors=[{"name": "Azure", "role": "charter", "email": "a@b.c"}],
|
|
)
|
|
with TestClient(server.app) as client:
|
|
info = _song_info(client, "/ws/highway/authortest.sloppak?arrangement=0")
|
|
assert info["authors"] == [{"name": "Azure", "role": "charter"}]
|
|
|
|
|
|
def test_song_info_authors_empty_when_absent(make_client):
|
|
server = make_client()
|
|
_write_sloppak(server._get_dlc_dir(), authors=None)
|
|
with TestClient(server.app) as client:
|
|
info = _song_info(client, "/ws/highway/authortest.sloppak?arrangement=0")
|
|
assert info["authors"] == []
|