"""Tests for check_and_manage_ap_mode_with_state (src/wifi_manager.py). The wifi monitor daemon used to fetch WiFi status before AND after each check on top of the check's own internal fetch — every fetch is several nmcli subprocess forks, every 30s, forever. The new API returns the state the check observed, so the daemon runs exactly one fetch battery per tick. """ import os import sys from unittest.mock import MagicMock, patch import pytest sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) from src.wifi_manager import WiFiManager, WiFiStatus # noqa: E402 @pytest.fixture def wm(tmp_path): with patch.object(WiFiManager, "_load_config", return_value={}, create=True): manager = WiFiManager.__new__(WiFiManager) # minimal attribute setup without running the real __init__ manager.config = {"auto_enable_ap_mode": True} manager._disconnected_checks = 0 manager._disconnected_checks_required = 3 manager._ap_enabled_at = None return manager def _wire(wm, connected, ethernet, ap_active): wm._get_wifi_status_with_retry = MagicMock( return_value=WiFiStatus(connected=connected, ssid="net" if connected else None)) wm._is_ethernet_connected = MagicMock(return_value=ethernet) wm._is_ap_mode_active = MagicMock(return_value=ap_active) wm.enable_ap_mode = MagicMock(return_value=(True, "ok")) wm.disable_ap_mode = MagicMock(return_value=(True, "ok")) wm.scan_networks = MagicMock(return_value=([], False)) wm._save_cached_scan = MagicMock() wm._FORCE_AP_FLAG_PATH = MagicMock() wm._FORCE_AP_FLAG_PATH.exists.return_value = False wm._connect_in_progress = MagicMock(return_value=False) class TestConnectInProgressHoldsOffTheDaemon: """Connecting from the setup AP takes the AP down before joining. The daemon had already counted the whole AP session as "disconnected", so its next tick re-enabled the AP in the middle of the connect.""" def test_no_ap_enable_while_a_connect_is_running(self, wm): _wire(wm, connected=False, ethernet=False, ap_active=False) wm._connect_in_progress.return_value = True wm._disconnected_checks = 10 # accumulated while the AP was up changed, *_ = wm.check_and_manage_ap_mode_with_state() assert changed is False wm.enable_ap_mode.assert_not_called() assert wm._disconnected_checks == 0 def test_grace_period_restarts_after_the_connect(self, wm): _wire(wm, connected=False, ethernet=False, ap_active=False) wm._connect_in_progress.return_value = True wm._disconnected_checks = 10 wm.check_and_manage_ap_mode_with_state() wm._connect_in_progress.return_value = False wm.check_and_manage_ap_mode_with_state() wm.enable_ap_mode.assert_not_called() def test_flag_is_held_for_the_connect_and_removed_after(self, wm, tmp_path): flag = tmp_path / "connecting" wm._CONNECT_IN_PROGRESS_FLAG_PATH = flag seen = [] wm._connect_validated = lambda ssid, pw: seen.append(flag.exists()) or (True, "ok") assert wm.connect_to_network("HomeNet", "hunter22") == (True, "ok") assert seen == [True] assert not flag.exists() def test_flag_is_removed_when_the_connect_raises(self, wm, tmp_path): flag = tmp_path / "connecting" wm._CONNECT_IN_PROGRESS_FLAG_PATH = flag wm._connect_validated = MagicMock(side_effect=RuntimeError("nmcli gone")) with pytest.raises(RuntimeError): wm.connect_to_network("HomeNet", "hunter22") assert not flag.exists() def test_a_stale_flag_from_a_dead_process_is_ignored(self, wm, tmp_path): flag = tmp_path / "connecting" flag.touch() wm._CONNECT_IN_PROGRESS_FLAG_PATH = flag assert WiFiManager._connect_in_progress(wm) is True old = flag.stat().st_mtime - WiFiManager._CONNECT_FLAG_MAX_AGE_SECONDS - 1 os.utime(flag, (old, old)) assert WiFiManager._connect_in_progress(wm) is False class TestWithState: def test_single_fetch_per_call(self, wm): _wire(wm, connected=True, ethernet=False, ap_active=False) wm.check_and_manage_ap_mode_with_state() assert wm._get_wifi_status_with_retry.call_count == 1 assert wm._is_ethernet_connected.call_count == 1 assert wm._is_ap_mode_active.call_count == 1 def test_returns_observed_state(self, wm): _wire(wm, connected=True, ethernet=True, ap_active=False) changed, status, ethernet, ap_after = wm.check_and_manage_ap_mode_with_state() assert changed is False assert status.connected is True assert ethernet is True assert ap_after is False def test_ap_after_inverts_on_disable(self, wm): """WiFi reconnects while AP is up -> auto-disable -> ap_after False.""" _wire(wm, connected=True, ethernet=False, ap_active=True) changed, _status, _ethernet, ap_after = wm.check_and_manage_ap_mode_with_state() assert changed is True assert ap_after is False wm.disable_ap_mode.assert_called_once() def test_ap_after_inverts_on_enable(self, wm): """Grace period exhausted with nothing connected -> enable -> True.""" _wire(wm, connected=False, ethernet=False, ap_active=False) wm._disconnected_checks = 2 # this call is the 3rd changed, _status, _ethernet, ap_after = wm.check_and_manage_ap_mode_with_state() assert changed is True assert ap_after is True wm.enable_ap_mode.assert_called_once() def test_bool_wrapper_is_back_compatible(self, wm): _wire(wm, connected=True, ethernet=False, ap_active=False) assert wm.check_and_manage_ap_mode() is False _wire(wm, connected=True, ethernet=False, ap_active=True) assert wm.check_and_manage_ap_mode() is True def test_exception_path_never_raises(self, wm): wm._get_wifi_status_with_retry = MagicMock(side_effect=RuntimeError("nmcli gone")) changed, status, _ethernet, _ap_after = wm.check_and_manage_ap_mode_with_state() assert changed is False assert status.connected is False if __name__ == "__main__": sys.exit(pytest.main([__file__, "-v"]))