mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-07-22 21:01:40 +00:00
* Update GitHub repo references from feedback* to feedBack* * rename: slopsmith -> feedBack, byron -> got-feedBack Renames across the entire codebase: - slopsmith/Slopsmith/SLOPSMITH/SlopSmith -> feedBack/FeedBack/FEEDBACK/FeedBack - byron/Byron/Byrongamatos -> got-feedBack/got-feedBack/got-feedBack - /home/byron/ -> /opt/got-feedBack/ - byron@ougsoft.com -> hi@got-feedBack.org - github.com/byrongamatos/ -> github.com/got-feedback/ - com.byron. -> com.got-feedback. - SLOPSMITH_ env vars -> FEEDBACK_ with backward-compat fallback - Protocol/storage strings migrated with read-old/write-new pattern - window.slopsmith JS API -> window.feedBack (canonical) + backward-compat alias Refs: #rename-slopsmith * rename: complete regen against current main + fix backward-compat alias Regenerated the slopsmith->feedBack / byron->got-feedBack rename on top of current main (3 commits had landed since the branch: #572/#554/#574), resolving the four content conflicts in favour of main's newer content (autoplay/auto-exit, accuracy-badge, Virtuoso re-home, feedpak badge). Completion fixes on top of the mechanical rename: - Re-apply rename to post-branch content the original rename never saw: window.slopsmith(.Tour) consumers in lessons.js / notifications.js / onboarding-tour.js, and the matching JS + python tests (autoplay_exit, progression_*, test_feedpak_extension FEEDBACK_* env vars). The test env vars now match server.py (which reads FEEDBACK_SYNC_STARTUP / FEEDBACK_SKIP_STARTUP_TASKS), so the sync-startup test exercises the real path again. - Restore the window.slopsmith backward-compat alias dropped during conflict resolution, and move the bus aliases to AFTER the _feedBackExisting merge block so they reference the fully-assembled object (also fixes the loop_api.test.js API-surface regex, which the original PR latently broke). - Drop the stray empty data/web_library.db (runtime DB lives in CONFIG_DIR) and gitignore it. - Fix stale tone-source test: feed[dB]ack -> fee[dB]ack to match shipped source labels. Verified locally (org CI billing-blocked): JS 819/819 pass; pytest 1669 passed / 1683 collected with 0 import errors; zero residual slopsmith/byron except the two intentional window.slopsmith aliases. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * rename: implement advertised backward-compat + prune dead community plugins Address gaps where PR #537's "Backward compatibility" section was advertised but not implemented, and clean up the community plugin list. Env vars (FEEDBACK_* canonical, legacy SLOPSMITH_* honoured): - New lib/env_compat.py (getenv_compat / env_flag_compat) + tests. server.py (_env_flag + all FEEDBACK_* reads), diagnostics_hardware, gp2midi and tailwind_rebuild now resolve the legacy alias, so existing SLOPSMITH_UI / SLOPSMITH_PLUGINS_DIR / etc. deployments keep working. - Fix the rename collapsing plugins/__init__.py and minigames/routes.py from `FEEDBACK_PLUGINS_DIR or SLOPSMITH_PLUGINS_DIR` into a redundant `FEEDBACK_ or FEEDBACK_` (the fallback was silently lost). Storage (app.js update-channel): - Read feedBack-update-channel, fall back to legacy slopsmith-update-channel, and clear the legacy key on write — so a user's update-channel preference survives the rename instead of resetting to "stable". Community plugin list (README): the rename rewrote third-party repo URLs we don't own. Probed every one; their owners never renamed, so: - Restore the 13 live community plugins to their real slopsmith-* names. - Prune 6 that are 404 to the public (topkoa splitscreen/stems, OmikronApex tuner, Jafz2001 nam-rig-builder, DeathlySin song-preview, Erikcb91 shuffle). - Fix a pre-existing Guitar Theory clone-command typo (nam-tone -> guitar-theory). Verified: env_compat 7/7, JS 819/819, pytest 1690 collected / 0 import errors, rename-sensitive + startup suites green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: byrongamatos <xasiklas@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
255 lines
8.9 KiB
Python
255 lines
8.9 KiB
Python
"""Integration test for notation streaming on the highway WebSocket.
|
|
|
|
Spins up the real FastAPI app via TestClient against a temp DLC_DIR holding a
|
|
hand-authored notation sloppak, then asserts the wire contract end-to-end:
|
|
|
|
- `song_info` carries `has_notation: true` (and `false` without notation),
|
|
- `notation_info` streams after `sections` and before `anchors` (WS-order-B),
|
|
- `notation_measures` chunks carry every measure with beat times rounded,
|
|
- a notation-only arrangement (no `file:` key) still streams.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import importlib
|
|
import json
|
|
import sys
|
|
|
|
import pytest
|
|
import yaml
|
|
from fastapi.testclient import TestClient
|
|
|
|
|
|
VALID_NOTATION = {
|
|
"version": 1,
|
|
"instrument": "piano",
|
|
"staves": [
|
|
{"id": "rh", "clef": "G2", "label": "Right Hand"},
|
|
{"id": "lh", "clef": "F4", "label": "Left Hand"},
|
|
],
|
|
"measures": [
|
|
{
|
|
"idx": 1,
|
|
"t": 0.0,
|
|
"ts": [4, 4],
|
|
"tempo": 120.0,
|
|
"staves": {
|
|
"rh": {
|
|
"voices": [
|
|
{
|
|
"v": 1,
|
|
"beats": [
|
|
{"t": 0.0001234, "dur": 4, "notes": [{"midi": 64}]},
|
|
{"t": 0.5, "dur": 4, "notes": [{"midi": 67}]},
|
|
],
|
|
}
|
|
]
|
|
},
|
|
"lh": {
|
|
"voices": [
|
|
{
|
|
"v": 1,
|
|
"beats": [
|
|
{"t": 0.0, "dur": 1, "notes": [{"midi": 52}, {"midi": 60}]}
|
|
],
|
|
}
|
|
]
|
|
},
|
|
},
|
|
},
|
|
{
|
|
"idx": 2,
|
|
"t": 2.0,
|
|
"staves": {
|
|
"rh": {
|
|
"voices": [
|
|
{"v": 1, "beats": [{"t": 2.0, "dur": 2, "notes": [{"midi": 65}]}]}
|
|
]
|
|
}
|
|
},
|
|
},
|
|
],
|
|
}
|
|
|
|
|
|
def _write_sloppak(dlc_root, *, notation: bool, include_file: bool = True):
|
|
pak = dlc_root / "wstest.sloppak"
|
|
pak.mkdir()
|
|
(pak / "arrangements").mkdir()
|
|
|
|
entry = {"id": "keys", "name": "Keys"}
|
|
if include_file:
|
|
entry["file"] = "arrangements/keys.json"
|
|
(pak / "arrangements" / "keys.json").write_text(
|
|
json.dumps(
|
|
{
|
|
"notes": [],
|
|
"chords": [],
|
|
"anchors": [],
|
|
"handshapes": [],
|
|
"templates": [],
|
|
"beats": [{"time": 0.0, "measure": 1}, {"time": 2.0, "measure": 2}],
|
|
"sections": [{"name": "intro", "number": 1, "time": 0.0}],
|
|
}
|
|
)
|
|
)
|
|
if notation:
|
|
entry["notation"] = "notation_keys.json"
|
|
(pak / "notation_keys.json").write_text(json.dumps(VALID_NOTATION))
|
|
|
|
manifest = {
|
|
"title": "WS Test",
|
|
"artist": "Tester",
|
|
"album": "",
|
|
"year": 2026,
|
|
"duration": 10.0,
|
|
"arrangements": [entry],
|
|
"stems": [],
|
|
}
|
|
(pak / "manifest.yaml").write_text(yaml.safe_dump(manifest, sort_keys=False))
|
|
return pak
|
|
|
|
|
|
@pytest.fixture()
|
|
def make_client(tmp_path, monkeypatch):
|
|
"""Factory: build the DLC dir first, then import a fresh server bound to it."""
|
|
|
|
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")
|
|
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:
|
|
conn.close()
|
|
|
|
|
|
def _drain(client, path, *, stop_type="anchors", limit=200):
|
|
"""Collect typed frames (skipping keepalive `loading` frames) until ready.
|
|
|
|
Always drains through the ``ready`` terminal frame so the server task can
|
|
finish cleanly — stopping mid-stream with TestClient leaves the server
|
|
coroutine blocked on its next send, which can stall test teardown. Returns
|
|
only frames up to and including *stop_type* so callers can assert ordering
|
|
without caring about later frame types (chord_templates, notes, ready).
|
|
|
|
Asserts the stop_type frame was actually reached — silently returning a
|
|
truncated stream would let ordering assertions pass vacuously.
|
|
|
|
Note: the starlette TestClient WebSocketTestSession uses a synchronous
|
|
portal that runs the ASGI app in a background thread. A per-receive
|
|
threading timeout is not safe here — the daemon thread can block teardown
|
|
when the test exits. Global CI job timeout is the correct safety net for
|
|
WS server regressions that produce no frames at all.
|
|
"""
|
|
|
|
frames = []
|
|
stop_reached = False
|
|
with client.websocket_connect(path) as ws:
|
|
for _ in range(limit):
|
|
msg = ws.receive_json()
|
|
if msg.get("error"):
|
|
raise AssertionError(f"WS error frame: {msg}")
|
|
if msg.get("type") == "loading":
|
|
continue
|
|
if not stop_reached:
|
|
frames.append(msg)
|
|
if msg.get("type") == stop_type:
|
|
stop_reached = True
|
|
if msg.get("type") == "ready":
|
|
break
|
|
assert stop_reached, (
|
|
f"terminal frame {stop_type!r} not reached within {limit} frames; "
|
|
f"got {[f.get('type') for f in frames]}"
|
|
)
|
|
return frames
|
|
|
|
|
|
def test_notation_streams_in_ws_order_b(make_client):
|
|
server = make_client()
|
|
_write_sloppak(server._get_dlc_dir(), notation=True)
|
|
with TestClient(server.app) as client:
|
|
frames = _drain(client, "/ws/highway/wstest.sloppak?arrangement=0")
|
|
|
|
order = [f["type"] for f in frames]
|
|
assert order[0] == "song_info"
|
|
assert frames[0]["has_notation"] is True
|
|
|
|
i_sections = order.index("sections")
|
|
i_info = order.index("notation_info")
|
|
i_anchors = order.index("anchors")
|
|
assert i_sections < i_info < i_anchors, order
|
|
|
|
info = frames[i_info]
|
|
assert info["instrument"] == "piano"
|
|
assert [s["id"] for s in info["staves"]] == ["rh", "lh"]
|
|
assert info["total"] == 2
|
|
|
|
chunks = [f for f in frames if f["type"] == "notation_measures"]
|
|
assert chunks, order
|
|
# Every chunk (not just the first) must arrive between notation_info and anchors.
|
|
chunk_positions = [i for i, f in enumerate(frames) if f["type"] == "notation_measures"]
|
|
assert all(i_info < pos < i_anchors for pos in chunk_positions), (
|
|
f"notation_measures chunk out of order: positions={chunk_positions} "
|
|
f"notation_info={i_info} anchors={i_anchors}"
|
|
)
|
|
measures = [m for c in chunks for m in c["data"]]
|
|
assert [m["idx"] for m in measures] == [1, 2]
|
|
assert all(c["total"] == 2 for c in chunks)
|
|
|
|
# measure_to_wire rounds beat times to 3 decimals on the hot path.
|
|
rh_beats = measures[0]["staves"]["rh"]["voices"][0]["beats"]
|
|
assert rh_beats[0]["t"] == 0.0
|
|
|
|
|
|
def test_notation_chunking_respects_chunk_size(make_client):
|
|
server = make_client()
|
|
pak_root = server._get_dlc_dir()
|
|
many = dict(VALID_NOTATION)
|
|
many["measures"] = [
|
|
{"idx": i + 1, "t": float(i), "staves": {}} for i in range(70)
|
|
]
|
|
pak = _write_sloppak(pak_root, notation=True)
|
|
(pak / "notation_keys.json").write_text(json.dumps(many))
|
|
|
|
with TestClient(server.app) as client:
|
|
frames = _drain(client, "/ws/highway/wstest.sloppak?arrangement=0")
|
|
|
|
chunks = [f for f in frames if f["type"] == "notation_measures"]
|
|
sizes = [len(c["data"]) for c in chunks]
|
|
assert sum(sizes) == 70
|
|
assert max(sizes) <= 32
|
|
assert sizes[:-1] == [32] * (len(sizes) - 1) # all but last chunk are full
|
|
|
|
|
|
def test_notation_only_arrangement_streams_without_file(make_client):
|
|
server = make_client()
|
|
_write_sloppak(server._get_dlc_dir(), notation=True, include_file=False)
|
|
with TestClient(server.app) as client:
|
|
frames = _drain(client, "/ws/highway/wstest.sloppak?arrangement=0")
|
|
|
|
order = [f["type"] for f in frames]
|
|
assert frames[0]["has_notation"] is True
|
|
assert "notation_info" in order and "notation_measures" in order
|
|
|
|
|
|
def test_no_notation_key_means_has_notation_false(make_client):
|
|
server = make_client()
|
|
_write_sloppak(server._get_dlc_dir(), notation=False)
|
|
with TestClient(server.app) as client:
|
|
frames = _drain(client, "/ws/highway/wstest.sloppak?arrangement=0")
|
|
|
|
order = [f["type"] for f in frames]
|
|
assert frames[0]["has_notation"] is False
|
|
assert "notation_info" not in order
|
|
assert "notation_measures" not in order
|