mqtt_manager: expose last connect error; health tells refused from unreachable (node-26#21)
mqtt_manager.last_error = {kind, detail, epoch}: 'refused' from a non-zero
CONNACK, 'unreachable' from the initial connect loop or paho's
on_connect_fail, 'disconnected' otherwise. Cleared on connect. The /api/health
MQTT check now gives a key fix for a refusal and a network fix for an
unreachable broker instead of guessing from whether a key is on disk.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a456b502db
commit
1b4fcefbf2
@@ -100,6 +100,17 @@ def test_key_on_disk_but_mqtt_down_says_restart():
|
||||
assert "restart the edge-node container" in c["mqtt"]["action"]
|
||||
|
||||
|
||||
def test_mqtt_refused_vs_unreachable_are_told_apart():
|
||||
"""node-26#21: 'not authorised' and 'can't reach the broker' need different fixes."""
|
||||
refused = _by_id(health.build_checks(_facts(
|
||||
mqtt_connected=False, mqtt_last_error={"kind": "refused", "detail": "Not authorized", "epoch": 1.0})))
|
||||
assert refused["mqtt"]["state"] == "error"
|
||||
assert "refused" in refused["mqtt"]["summary"] and "install.sh" in refused["mqtt"]["action"]
|
||||
unreachable = _by_id(health.build_checks(_facts(
|
||||
mqtt_connected=False, mqtt_last_error={"kind": "unreachable", "detail": "timed out", "epoch": 1.0})))
|
||||
assert "Can't reach" in unreachable["mqtt"]["summary"] and "firewall" in unreachable["mqtt"]["action"]
|
||||
|
||||
|
||||
def test_standalone_node_is_not_an_error():
|
||||
c = _by_id(health.build_checks(_facts(c2_url=None, has_api_key=False, mqtt_connected=False)))
|
||||
assert c["c2"]["state"] == "off"
|
||||
|
||||
@@ -153,3 +153,28 @@ def test_disk_report_nulls_when_path_unreadable(tmp_path):
|
||||
# node-26#41: nulls (not omitted keys) so C2 never keeps a stale reading.
|
||||
from app.internal.mqtt_manager import disk_report
|
||||
assert disk_report(str(tmp_path / "missing")) == {"disk_free_bytes": None, "disk_total_bytes": None}
|
||||
|
||||
|
||||
def test_last_error_records_refusal_and_clears_on_connect(monkeypatch):
|
||||
"""node-26#21: the refusal reason survives the disconnect that follows it."""
|
||||
from unittest.mock import MagicMock
|
||||
mqtt_manager._last_error = None
|
||||
mqtt_manager._loop = None
|
||||
mqtt_manager._on_connect(MagicMock(), None, None, 135, None)
|
||||
mqtt_manager._on_disconnect(MagicMock(), None, None, 135, None)
|
||||
assert mqtt_manager.last_error["kind"] == "refused"
|
||||
monkeypatch.setattr(mqtt_manager, "_flush_offline_buffer", MagicMock())
|
||||
monkeypatch.setattr(mqtt_manager, "_publish_checkin", MagicMock())
|
||||
monkeypatch.setattr(mqtt_manager, "_maybe_request_key", MagicMock())
|
||||
monkeypatch.setattr("asyncio.run_coroutine_threadsafe", lambda *a, **k: None)
|
||||
mqtt_manager._on_connect(MagicMock(), None, None, 0, None)
|
||||
assert mqtt_manager.last_error is None
|
||||
mqtt_manager._connected = False
|
||||
|
||||
|
||||
def test_last_error_unreachable_from_connect_fail():
|
||||
mqtt_manager._last_error = None
|
||||
mqtt_manager._connected = False
|
||||
mqtt_manager._on_connect_fail(None, None)
|
||||
assert mqtt_manager.last_error["kind"] == "unreachable"
|
||||
mqtt_manager._last_error = None
|
||||
|
||||
Reference in New Issue
Block a user