improve MQTT error bubble up and massage communitymqtt + debug etc. for version management. Closes #70

This commit is contained in:
Jack Kingsman
2026-03-17 20:32:57 -07:00
parent 020acbda02
commit e33bc553f5
22 changed files with 344 additions and 155 deletions

View File

@@ -5,7 +5,6 @@ Uses httpx.AsyncClient or direct function calls with real in-memory SQLite.
"""
import hashlib
import json
import time
from unittest.mock import AsyncMock, MagicMock, patch
@@ -20,6 +19,7 @@ from app.repository import (
MessageRepository,
RawPacketRepository,
)
from app.version_info import AppBuildInfo
@pytest.fixture(autouse=True)
@@ -144,7 +144,9 @@ class TestDebugEndpoint:
"app.routers.debug._build_application_info",
return_value=DebugApplicationInfo(
version="3.2.0",
version_source="pyproject",
commit_hash="deadbeef",
commit_source="git",
git_branch="main",
git_dirty=False,
python_version="3.12.0",
@@ -186,24 +188,24 @@ class TestDebugApplicationInfo:
"""Release bundles should still surface commit metadata without a .git directory."""
from app.routers import debug as debug_router
(tmp_path / "build_info.json").write_text(
json.dumps(
{
"commit_hash": "cf1a55e25828ee62fb077d6202b174f69f6e6340",
"build_source": "prebuilt-release",
}
)
)
with (
patch("app.routers.debug._repo_root", return_value=tmp_path),
patch("app.routers.debug._get_app_version", return_value="3.4.0"),
patch("app.routers.debug._git_output", return_value=None),
patch(
"app.routers.debug.get_app_build_info",
return_value=AppBuildInfo(
version="3.4.0",
version_source="pyproject",
commit_hash="cf1a55e2",
commit_source="build_info",
),
),
patch("app.routers.debug.git_output", return_value=None),
):
info = debug_router._build_application_info()
assert info.version == "3.4.0"
assert info.commit_hash == "cf1a55e25828ee62fb077d6202b174f69f6e6340"
assert info.version_source == "pyproject"
assert info.commit_hash == "cf1a55e2"
assert info.commit_source == "build_info"
assert info.git_branch is None
assert info.git_dirty is False
@@ -211,17 +213,24 @@ class TestDebugApplicationInfo:
"""Malformed release metadata should not break the debug endpoint."""
from app.routers import debug as debug_router
(tmp_path / "build_info.json").write_text("{not-json")
with (
patch("app.routers.debug._repo_root", return_value=tmp_path),
patch("app.routers.debug._get_app_version", return_value="3.4.0"),
patch("app.routers.debug._git_output", return_value=None),
patch(
"app.routers.debug.get_app_build_info",
return_value=AppBuildInfo(
version="3.4.0",
version_source="pyproject",
commit_hash=None,
commit_source=None,
),
),
patch("app.routers.debug.git_output", return_value=None),
):
info = debug_router._build_application_info()
assert info.version == "3.4.0"
assert info.version_source == "pyproject"
assert info.commit_hash is None
assert info.commit_source is None
assert info.git_branch is None
assert info.git_dirty is False

View File

@@ -631,15 +631,18 @@ class TestCommunityMqttPublisher:
class TestPublishFailureSetsDisconnected:
@pytest.mark.asyncio
async def test_publish_error_sets_connected_false(self):
async def test_publish_error_sets_connected_false(self, caplog):
"""A publish error should set connected=False so the loop can detect it."""
pub = CommunityMqttPublisher()
pub.set_integration_name("LetsMesh West")
pub.connected = True
mock_client = MagicMock()
mock_client.publish = MagicMock(side_effect=Exception("broker gone"))
pub._client = mock_client
await pub.publish("topic", {"data": "test"})
assert pub.connected is False
assert "LetsMesh West" in caplog.text
assert "if it self-resolves" in caplog.text
class TestBuildStatusTopic:
@@ -1217,21 +1220,13 @@ class TestGetClientVersion:
result = _get_client_version()
assert result.startswith("RemoteTerm ")
def test_returns_version_from_metadata(self):
"""Should use importlib.metadata to get version."""
with patch("app.fanout.community_mqtt.importlib.metadata.version", return_value="1.2.3"):
def test_returns_version_from_build_helper(self):
"""Should use the shared backend build-info helper."""
with patch("app.fanout.community_mqtt.get_app_build_info") as mock_build_info:
mock_build_info.return_value.version = "1.2.3"
result = _get_client_version()
assert result == "RemoteTerm 1.2.3"
def test_fallback_on_error(self):
"""Should return 'RemoteTerm unknown' if metadata lookup fails."""
with patch(
"app.fanout.community_mqtt.importlib.metadata.version",
side_effect=Exception("not found"),
):
result = _get_client_version()
assert result == "RemoteTerm unknown"
class TestPublishStatus:
@pytest.mark.asyncio

View File

@@ -9,6 +9,7 @@ from unittest.mock import patch
import pytest
from app.routers.health import build_health_data
from app.version_info import AppBuildInfo
class TestHealthFanoutStatus:
@@ -48,6 +49,15 @@ class TestHealthFanoutStatus:
"app.routers.health.RawPacketRepository.get_oldest_undecrypted", return_value=None
),
patch("app.routers.health.radio_manager") as mock_rm,
patch(
"app.routers.health.get_app_build_info",
return_value=AppBuildInfo(
version="3.4.1",
version_source="pyproject",
commit_hash="abcdef12",
commit_source="git",
),
),
):
mock_rm.is_setup_in_progress = False
mock_rm.is_setup_complete = True
@@ -58,6 +68,10 @@ class TestHealthFanoutStatus:
assert data["radio_initializing"] is False
assert data["radio_state"] == "connected"
assert data["connection_info"] == "Serial: /dev/ttyUSB0"
assert data["app_info"] == {
"version": "3.4.1",
"commit_hash": "abcdef12",
}
@pytest.mark.asyncio
async def test_health_includes_cached_radio_device_info(self, test_db):

View File

@@ -132,8 +132,9 @@ class TestMqttPublisher:
assert call_args[1]["retain"] is False
@pytest.mark.asyncio
async def test_publish_handles_exception_gracefully(self):
async def test_publish_handles_exception_gracefully(self, caplog):
pub = MqttPublisher()
pub.set_integration_name("Primary MQTT")
pub.connected = True
mock_client = AsyncMock()
mock_client.publish.side_effect = Exception("Network error")
@@ -145,6 +146,8 @@ class TestMqttPublisher:
# After a publish failure, connected should be cleared to stop
# further attempts and reflect accurate status
assert pub.connected is False
assert "Primary MQTT" in caplog.text
assert "usually transient network noise" in caplog.text
@pytest.mark.asyncio
async def test_stop_resets_state(self):

View File

@@ -0,0 +1,41 @@
from app import version_info
class TestAppBuildInfo:
def setup_method(self):
version_info.get_app_build_info.cache_clear()
def teardown_method(self):
version_info.get_app_build_info.cache_clear()
def test_prefers_package_metadata_and_git(self, monkeypatch):
monkeypatch.setattr(version_info, "_package_metadata_version", lambda: "3.4.1")
monkeypatch.setattr(version_info, "_env_version", lambda: "3.4.0-env")
monkeypatch.setattr(version_info, "_build_info_version", lambda build_info: "3.3.0-build")
monkeypatch.setattr(version_info, "_pyproject_version", lambda root: "3.2.0-pyproject")
monkeypatch.setattr(version_info, "_git_output", lambda root, *args: "abcdef12")
monkeypatch.setattr(version_info, "_env_commit_hash", lambda: "fedcba0987654321")
monkeypatch.setattr(version_info, "_build_info_commit_hash", lambda build_info: "11223344")
info = version_info.get_app_build_info()
assert info.version == "3.4.1"
assert info.version_source == "package_metadata"
assert info.commit_hash == "abcdef12"
assert info.commit_source == "git"
def test_falls_back_to_pyproject_and_build_info(self, monkeypatch):
monkeypatch.setattr(version_info, "_package_metadata_version", lambda: None)
monkeypatch.setattr(version_info, "_env_version", lambda: None)
monkeypatch.setattr(version_info, "_build_info_version", lambda build_info: None)
monkeypatch.setattr(version_info, "_pyproject_version", lambda root: "3.2.0")
monkeypatch.setattr(version_info, "_git_output", lambda root, *args: None)
monkeypatch.setattr(version_info, "_env_commit_hash", lambda: None)
monkeypatch.setattr(version_info, "_build_info_commit_hash", lambda build_info: "cf1a55e2")
info = version_info.get_app_build_info()
assert info.version == "3.2.0"
assert info.version_source == "pyproject"
assert info.commit_hash == "cf1a55e2"
assert info.commit_source == "build_info"