diff --git a/hate_crack/main.py b/hate_crack/main.py index 37f0772..bb16253 100755 --- a/hate_crack/main.py +++ b/hate_crack/main.py @@ -4108,6 +4108,29 @@ def toggle_notifications(): ) +def toggle_per_crack_notifications(): + """Runtime toggle for ``notify_per_crack_enabled`` with a UI-level guard. + + Per-crack notifications require global notifications to be ON in order + to fire (see ``notify.start_tailer``). Turning per-crack ON while the + global switch is OFF is silently ineffective, which surprises users — + so we refuse the transition and point them at the global toggle. + + Turning per-crack OFF is always allowed, regardless of the global + state, so users can clean up an inconsistent config without friction. + """ + settings = _notify.get_settings() + if not settings.per_crack_enabled and not settings.enabled: + print( + "\n[!] Global Pushover notifications are OFF. Enable option 1 " + "(Toggle Pushover Notifications) first." + ) + return + new_state = _notify.toggle_per_crack_enabled() + label = "ON" if new_state else "OFF" + print(f"\nPer-crack notifications are now {label}.") + + def test_pushover_notification(): """Send a canned test notification so the user can verify Pushover works. diff --git a/tests/test_notify_per_crack_toggle.py b/tests/test_notify_per_crack_toggle.py index ee985ee..ecda4b1 100644 --- a/tests/test_notify_per_crack_toggle.py +++ b/tests/test_notify_per_crack_toggle.py @@ -1,9 +1,17 @@ """Unit tests for the toggle_per_crack_enabled runtime toggle.""" +import importlib.util import json from pathlib import Path from hate_crack import notify as _notify +PROJECT_ROOT = Path(__file__).resolve().parents[1] +_CLI_SPEC = importlib.util.spec_from_file_location( + "hate_crack_cli_percrack", PROJECT_ROOT / "hate_crack.py" +) +CLI_MODULE = importlib.util.module_from_spec(_CLI_SPEC) +_CLI_SPEC.loader.exec_module(CLI_MODULE) + def _init_with(tmp_path: Path, **overrides) -> Path: """Seed a config file with defaults + overrides and init the notify module.""" @@ -65,3 +73,64 @@ class TestTogglePerCrackEnabled: assert data["notify_per_crack_enabled"] is True finally: _notify.clear_state_for_tests() + + +class TestTogglePerCrackNotificationsUI: + def _seed_settings(self, monkeypatch, *, enabled: bool, per_crack: bool): + from hate_crack.notify.settings import NotifySettings + + settings = NotifySettings(enabled=enabled, per_crack_enabled=per_crack) + monkeypatch.setattr( + CLI_MODULE._notify, "get_settings", lambda: settings + ) + return settings + + def test_guard_refuses_on_when_global_off(self, monkeypatch, capsys): + self._seed_settings(monkeypatch, enabled=False, per_crack=False) + called = {"n": 0} + + def _fake_toggle() -> bool: + called["n"] += 1 + return True + + monkeypatch.setattr( + CLI_MODULE._notify, "toggle_per_crack_enabled", _fake_toggle + ) + CLI_MODULE.toggle_per_crack_notifications() + captured = capsys.readouterr().out + assert "Global Pushover notifications are OFF" in captured + assert called["n"] == 0 + + def test_flips_on_when_global_on(self, monkeypatch, capsys): + self._seed_settings(monkeypatch, enabled=True, per_crack=False) + called = {"n": 0} + + def _fake_toggle() -> bool: + called["n"] += 1 + return True + + monkeypatch.setattr( + CLI_MODULE._notify, "toggle_per_crack_enabled", _fake_toggle + ) + CLI_MODULE.toggle_per_crack_notifications() + captured = capsys.readouterr().out + assert "Per-crack notifications are now ON" in captured + assert called["n"] == 1 + + def test_off_to_off_is_allowed_even_if_global_off(self, monkeypatch, capsys): + # Turning OFF must always succeed, even with global OFF, so a user + # can clean up an inconsistent (per_crack=True, enabled=False) config. + self._seed_settings(monkeypatch, enabled=False, per_crack=True) + called = {"n": 0} + + def _fake_toggle() -> bool: + called["n"] += 1 + return False + + monkeypatch.setattr( + CLI_MODULE._notify, "toggle_per_crack_enabled", _fake_toggle + ) + CLI_MODULE.toggle_per_crack_notifications() + captured = capsys.readouterr().out + assert "Per-crack notifications are now OFF" in captured + assert called["n"] == 1