From 0c520ffa00aa09b82484720ec3e52a5ceceae0ac Mon Sep 17 00:00:00 2001 From: viewit Date: Wed, 5 Aug 2026 09:35:26 +0200 Subject: [PATCH] feat(update): treat the testing channel as docker-only in the update check A testing- build has no Gitea releases (the testing workflow builds only a Docker image), so the in-app update check must not fall through to the stable path - which would wrongly offer a stable "update". handle_api_update_check now short-circuits for a "testing" version with docker_only:true and nothing to update (no Gitea round-trip at all), and handle_api_update_apply refuses self-update on the testing channel the same way it already does for nightly. Two new tests cover both. (Mirrors the same fix already on the testing branch, applied here to the pre-refactor monolithic module.) --- kobrax_moonraker_bridge.py | 21 +++++++++++++++++++-- tests/test_update_check.py | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/kobrax_moonraker_bridge.py b/kobrax_moonraker_bridge.py index e4e8245..6696986 100644 --- a/kobrax_moonraker_bridge.py +++ b/kobrax_moonraker_bridge.py @@ -5451,6 +5451,21 @@ class KobraXBridge: async def handle_api_update_check(self, request): current = self._read_version() + # Testing channel (testing-) has no Gitea releases at all - it's + # a Docker-only channel. Report that directly instead of falling + # through to the stable path (which would wrongly offer a stable + # "update"). The :testing image is rolling, so there's nothing to + # compare a version against. + if "testing" in current: + return web.json_response({ + "current": current, + "latest": current, + "update_available": False, + "tag": current, + "docker_only": True, + "changelog": "Testing channel - updates are delivered via Docker: " + "docker compose pull && docker compose up -d", + }) is_nightly = "nightly" in current is_dev = "-dev+" in current if is_nightly: @@ -5535,9 +5550,11 @@ class KobraXBridge: except Exception: return web.json_response({"error": "invalid json"}, status=400) new_tag = data.get("tag", "") - if "nightly" in self._read_version(): + _cur = self._read_version() + if "nightly" in _cur or "testing" in _cur: + channel = "testing" if "testing" in _cur else "nightly" return web.json_response( - {"error": "nightly updates are delivered via Docker: " + {"error": f"{channel} updates are delivered via Docker: " "docker compose pull && docker compose up -d"}, status=400) if getattr(sys, "frozen", False): return web.json_response( diff --git a/tests/test_update_check.py b/tests/test_update_check.py index 4f11827..28686d6 100644 --- a/tests/test_update_check.py +++ b/tests/test_update_check.py @@ -21,6 +21,40 @@ async def test_update_apply_invalid_json_returns_400(client): assert resp.status == 400 +@pytest.mark.asyncio +async def test_update_check_testing_channel_is_docker_only(client): + """A testing- build has no Gitea releases at all - the check must + report a docker-only channel with nothing to update, NOT fall through to + the stable path and wrongly offer a stable "update". Must not even call + the Gitea API.""" + c, bridge = client + bridge._read_version = lambda: "testing-2e4dbf0" + + with patch("aiohttp.ClientSession.get") as mock_get: + resp = await c.get("/api/update/check") + data = await resp.json() + + assert resp.status == 200 + assert data["update_available"] is False + assert data["docker_only"] is True + assert data["current"] == "testing-2e4dbf0" + mock_get.assert_not_called() # no Gitea round-trip for the testing channel + + +@pytest.mark.asyncio +async def test_update_apply_testing_channel_blocked(client): + """Self-update must be refused on the testing channel, same as nightly - + testing images are delivered via Docker only.""" + c, bridge = client + bridge._read_version = lambda: "testing-2e4dbf0" + + resp = await c.post("/api/update/apply", json={"tag": "whatever"}) + data = await resp.json() + assert resp.status == 400 + assert "testing" in data["error"] + assert "docker" in data["error"].lower() + + def _fake_releases_response(payload): resp = MagicMock() resp.status = 200