From 4a66a5e2650932ada8e6631cec4df84082570b3a Mon Sep 17 00:00:00 2001 From: Carolina <79524656+carochacs@users.noreply.github.com> Date: Sat, 1 Aug 2026 15:31:57 -0600 Subject: [PATCH 1/4] Add regression for failed sloppak websocket load --- lib/routers/ws_highway.py | 4 ++ tests/test_highway_ws_failed_sloppak_load.py | 63 ++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 tests/test_highway_ws_failed_sloppak_load.py diff --git a/lib/routers/ws_highway.py b/lib/routers/ws_highway.py index fd1ad602..a73faff9 100644 --- a/lib/routers/ws_highway.py +++ b/lib/routers/ws_highway.py @@ -212,6 +212,10 @@ async def _send_keepalives(): None, lambda: _ctx.run(sloppak_mod.load_song, filename, dlc, appstate.sloppak_cache_dir), ) + if loaded_slop is None: + await websocket.send_json({"error": "Failed to load sloppak"}) + await websocket.close() + return song = loaded_slop.song tmp = str(loaded_slop.source_dir) owns_tmp = False diff --git a/tests/test_highway_ws_failed_sloppak_load.py b/tests/test_highway_ws_failed_sloppak_load.py new file mode 100644 index 00000000..cd6ba64f --- /dev/null +++ b/tests/test_highway_ws_failed_sloppak_load.py @@ -0,0 +1,63 @@ +"""Regression coverage for sloppak load failures in the highway websocket.""" + +import asyncio +import importlib +import sys + +import pytest + + +class _CapturingWS: + def __init__(self): + self.messages = [] + self.accepted = False + self.closed = False + + async def accept(self): + self.accepted = True + + async def send_json(self, data): + self.messages.append(data) + + async def receive_text(self): + await asyncio.sleep(0) + return "" + + async def close(self): + self.closed = True + + +@pytest.fixture() +def server(tmp_path, monkeypatch): + (tmp_path / "dlc").mkdir() + monkeypatch.setenv("CONFIG_DIR", str(tmp_path / "config")) + monkeypatch.setenv("DLC_DIR", str(tmp_path / "dlc")) + monkeypatch.setenv("FEEDBACK_SKIP_STARTUP_TASKS", "1") + sys.modules.pop("server", None) + mod = importlib.import_module("server") + yield mod, tmp_path / "dlc", tmp_path / "cache" + conn = getattr(getattr(mod, "meta_db", None), "conn", None) + if conn is not None: + getattr(mod, "_join_background_db_threads", lambda: None)() + conn.close() + sys.modules.pop("server", None) + + +def test_sloppak_loader_returning_none_sends_error_without_touching_stems( + server, monkeypatch +): + _server, dlc, cache = server + (dlc / "broken.feedpak").mkdir() + + import appstate + from routers import ws_highway + + monkeypatch.setattr(appstate, "sloppak_cache_dir", cache) + monkeypatch.setattr(ws_highway.sloppak_mod, "load_song", lambda *a, **kw: None) + + ws = _CapturingWS() + asyncio.run(ws_highway.highway_ws(ws, "broken.feedpak", arrangement=0)) + + assert ws.accepted is True + assert ws.closed is True + assert {"error": "Failed to load sloppak"} in ws.messages From e2614a8a81511a61728922534a004966b3fddee8 Mon Sep 17 00:00:00 2001 From: Carolina <79524656+carochacs@users.noreply.github.com> Date: Sun, 2 Aug 2026 11:20:21 -0600 Subject: [PATCH 2/4] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Signed-off-by: Carolina <79524656+carochacs@users.noreply.github.com> --- lib/routers/ws_highway.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/routers/ws_highway.py b/lib/routers/ws_highway.py index a73faff9..fae2dd3e 100644 --- a/lib/routers/ws_highway.py +++ b/lib/routers/ws_highway.py @@ -213,6 +213,8 @@ async def _send_keepalives(): lambda: _ctx.run(sloppak_mod.load_song, filename, dlc, appstate.sloppak_cache_dir), ) if loaded_slop is None: + _keepalive_active = False + keepalive_task.cancel() await websocket.send_json({"error": "Failed to load sloppak"}) await websocket.close() return From 7a36467e9d464bd07c1ac2be1ef1902a04f83b47 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 05:39:57 +0000 Subject: [PATCH 3/4] Strengthen regression test and add CHANGELOG entry MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - test_highway_ws_failed_sloppak_load.py: assert the exact two-frame message sequence and a single close() call, not just that the error was somewhere in the messages list — a handler that sends the error and keeps processing would previously still pass (CodeRabbit). - CHANGELOG.md: add the [Unreleased] Fixed entry for this bug, per the PR template checklist. --- CHANGELOG.md | 9 +++++++++ tests/test_highway_ws_failed_sloppak_load.py | 8 +++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c262781..212d1912 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed +- **Failed sloppak loads at the highway websocket no longer crash on a `None` song.** + When `sloppak_mod.load_song()` returns `None` (cache corruption, partial + extraction, etc.), the handler previously dereferenced `loaded_slop.song` + before any guard existed, crashing the connection instead of reporting the + failure. The load-failure guard now sits immediately after the load call — + the handler sends a `Failed to load sloppak` error and closes the socket + instead of continuing into arrangement/stem access with a `None` song. + ### Added - **Core reader for source rigs (feedpak 1.18.0).** A pack can declare what a MIDI part should sound like by binding a rig; core now reads that binding and diff --git a/tests/test_highway_ws_failed_sloppak_load.py b/tests/test_highway_ws_failed_sloppak_load.py index cd6ba64f..d3e859cd 100644 --- a/tests/test_highway_ws_failed_sloppak_load.py +++ b/tests/test_highway_ws_failed_sloppak_load.py @@ -12,6 +12,7 @@ def __init__(self): self.messages = [] self.accepted = False self.closed = False + self.close_calls = 0 async def accept(self): self.accepted = True @@ -24,6 +25,7 @@ async def receive_text(self): return "" async def close(self): + self.close_calls += 1 self.closed = True @@ -60,4 +62,8 @@ def test_sloppak_loader_returning_none_sends_error_without_touching_stems( assert ws.accepted is True assert ws.closed is True - assert {"error": "Failed to load sloppak"} in ws.messages + assert ws.close_calls == 1 + assert ws.messages == [ + {"type": "loading", "stage": "Extracting..."}, + {"error": "Failed to load sloppak"}, + ] From 2ae2dc7f434c49c1ff0363d38df7eed83ed5fe39 Mon Sep 17 00:00:00 2001 From: Carolina <79524656+carochacs@users.noreply.github.com> Date: Tue, 4 Aug 2026 22:03:15 -0600 Subject: [PATCH 4/4] Fix changelog heading spacing --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 212d1912..457a410c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Fixed + - **Failed sloppak loads at the highway websocket no longer crash on a `None` song.** When `sloppak_mod.load_song()` returns `None` (cache corruption, partial extraction, etc.), the handler previously dereferenced `loaded_slop.song`