feat(notify): add per-crack UI toggle with global-OFF guard

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Justin Bollinger
2026-04-22 18:57:21 -04:00
parent cf2a6a6655
commit 8ff6ac8943
2 changed files with 92 additions and 0 deletions

View File

@@ -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.

View File

@@ -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