""" Tests für _poll_loop's Umgang mit einer toten MQTT-Session (Issue #105). publish()/query_info() swallow send failures internally and return None instead of raising - the poll loop must treat that (combined with is_connected() == False) as "connection lost" and switch to the offline branch, instead of silently retrying forever every poll_interval. """ import argparse import tempfile import threading import time from unittest.mock import MagicMock import pytest from kobrax_moonraker_bridge import KobraXBridge def _bridge(): c = MagicMock() c.callbacks = {} c.connected = False args = argparse.Namespace( printer_ip="192.168.1.100", mqtt_port=9883, username="", password="", mode_id="20030", device_id="", host="127.0.0.1", port=7125, data_dir=tempfile.mkdtemp(prefix="kxpoll-"), poll_interval=0.05, ) return KobraXBridge(c, args=args) def test_poll_loop_switches_to_offline_when_query_returns_none_and_disconnected(): b = _bridge() b._state["print_state"] = "standby" b._state["kobra_state"] = "free" b.client.query_info.return_value = None b.client.is_connected.return_value = False b._printer_reachable = MagicMock(return_value=True) # TCP still fine stop_event = threading.Event() t = threading.Thread(target=b._poll_loop, args=(stop_event,), daemon=True) t.start() time.sleep(0.2) stop_event.set() t.join(timeout=2) assert b._state["kobra_state"] == "offline" b.client.disconnect.assert_called() def test_poll_loop_stays_online_when_query_returns_none_but_still_connected(): """A single missed poll tick (info momentarily falsy) must not flip the bridge offline if the MQTT session itself is still alive.""" b = _bridge() b._state["print_state"] = "standby" b._state["kobra_state"] = "free" b.client.query_info.return_value = None b.client.is_connected.return_value = True # session still up b.client.query_multicolor_box.return_value = None b._printer_reachable = MagicMock(return_value=True) stop_event = threading.Event() t = threading.Thread(target=b._poll_loop, args=(stop_event,), daemon=True) t.start() time.sleep(0.2) stop_event.set() t.join(timeout=2) assert b._state["kobra_state"] != "offline" def test_poll_loop_recovers_via_offline_branch_once_reachable_again(): """Once flipped offline, the existing offline branch should re-connect as soon as the printer becomes reachable again (pre-existing behavior, unaffected by this fix).""" b = _bridge() b._state["print_state"] = "standby" b._state["kobra_state"] = "free" b.client.query_info.return_value = None b.client.is_connected.return_value = False b._printer_reachable = MagicMock(return_value=True) stop_event = threading.Event() t = threading.Thread(target=b._poll_loop, args=(stop_event,), daemon=True) t.start() time.sleep(0.15) # let it flip offline assert b._state["kobra_state"] == "offline" # Printer "comes back": client.connect() succeeds, subsequent query_info # starts returning real data again. b.client.connect.side_effect = None b.client.query_info.return_value = {"data": {"state": "free"}} time.sleep(0.2) stop_event.set() t.join(timeout=2) b.client.connect.assert_called()