From 8a148d5261b2b5d50f22bf165aed9341ecf1984d Mon Sep 17 00:00:00 2001 From: Justin Bollinger Date: Wed, 22 Apr 2026 18:48:24 -0400 Subject: [PATCH] feat(notify): persist notify_per_crack_enabled atomically Add save_per_crack_enabled() as a data-layer sibling to save_enabled(), using the same _atomic_rewrite primitive so mid-write crashes cannot corrupt config.json. Co-Authored-By: Claude Sonnet 4.6 --- hate_crack/notify/settings.py | 9 +++++++++ tests/test_notify_settings.py | 30 ++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/hate_crack/notify/settings.py b/hate_crack/notify/settings.py index 0e388bc..d6279c5 100644 --- a/hate_crack/notify/settings.py +++ b/hate_crack/notify/settings.py @@ -147,6 +147,15 @@ def save_enabled(config_path: str, enabled: bool) -> None: _atomic_rewrite(config_path, _apply) +def save_per_crack_enabled(config_path: str, enabled: bool) -> None: + """Persist ``notify_per_crack_enabled`` without disturbing other config keys.""" + + def _apply(data: dict) -> None: + data["notify_per_crack_enabled"] = bool(enabled) + + _atomic_rewrite(config_path, _apply) + + def add_to_allowlist(config_path: str, attack_name: str) -> None: """Append ``attack_name`` to ``notify_attack_allowlist`` if absent. diff --git a/tests/test_notify_settings.py b/tests/test_notify_settings.py index 8f5dc7b..c8201e4 100644 --- a/tests/test_notify_settings.py +++ b/tests/test_notify_settings.py @@ -7,6 +7,7 @@ from hate_crack.notify.settings import ( add_to_allowlist, load_settings, save_enabled, + save_per_crack_enabled, ) @@ -157,3 +158,32 @@ class TestAddToAllowlist: add_to_allowlist(str(config_path), "Brute Force") data = json.loads(config_path.read_text()) assert data["notify_attack_allowlist"] == ["Brute Force"] + + +class TestSavePerCrackEnabled: + def test_writes_new_config(self, tmp_path: Path) -> None: + config_path = tmp_path / "config.json" + save_per_crack_enabled(str(config_path), True) + data = json.loads(config_path.read_text()) + assert data["notify_per_crack_enabled"] is True + + def test_preserves_existing_keys(self, tmp_path: Path) -> None: + config_path = tmp_path / "config.json" + initial = { + "hcatBin": "hashcat", + "notify_enabled": True, + "notify_per_crack_enabled": False, + } + config_path.write_text(json.dumps(initial)) + save_per_crack_enabled(str(config_path), True) + data = json.loads(config_path.read_text()) + assert data["hcatBin"] == "hashcat" + assert data["notify_enabled"] is True + assert data["notify_per_crack_enabled"] is True + + def test_toggles_back_and_forth(self, tmp_path: Path) -> None: + config_path = tmp_path / "config.json" + save_per_crack_enabled(str(config_path), True) + save_per_crack_enabled(str(config_path), False) + data = json.loads(config_path.read_text()) + assert data["notify_per_crack_enabled"] is False