fieldwitness/tests/test_deadman_enforcement.py
Aaron D. Lee 5c74a5f4aa
Some checks failed
CI / lint (push) Failing after 36s
CI / typecheck (push) Failing after 37s
CI / test (push) Failing after 24s
Fix black formatting and target Python 3.12 in CI
Reformat 8 files and add --target-version py312 to avoid
3.13 AST parsing issues with Python 3.12 container.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 18:26:32 -04:00

298 lines
9.7 KiB
Python

"""
Tests for dead man's switch background enforcement and CLI command.
Covers:
- _deadman_enforcement_loop: does not call execute_purge when not armed
- _deadman_enforcement_loop: calls execute_purge when armed and overdue
- _deadman_enforcement_loop: exits after firing so execute_purge is not called twice
- _start_deadman_thread: returns a live daemon thread
- check-deadman CLI: exits 0 when disarmed
- check-deadman CLI: exits 0 and prints OK when armed but not overdue
- check-deadman CLI: exits 0 and prints OVERDUE warning when past interval but in grace
- check-deadman CLI: exits 2 when fully expired (past interval + grace)
"""
from __future__ import annotations
import json
import time
from datetime import UTC, datetime, timedelta
from pathlib import Path
import pytest
from click.testing import CliRunner
# ── Fixtures ────────────────────────────────────────────────────────────────
@pytest.fixture()
def soosef_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Redirect soosef paths to a tmp directory."""
import soosef.paths as paths
data_dir = tmp_path / ".soosef"
data_dir.mkdir()
monkeypatch.setattr(paths, "BASE_DIR", data_dir)
return data_dir
def _write_deadman_state(
state_file: Path,
*,
armed: bool,
last_checkin: datetime,
interval_hours: int = 24,
grace_hours: int = 2,
) -> None:
state_file.parent.mkdir(parents=True, exist_ok=True)
state = {
"armed": armed,
"last_checkin": last_checkin.isoformat(),
"interval_hours": interval_hours,
"grace_hours": grace_hours,
}
state_file.write_text(json.dumps(state))
# ── Unit tests: enforcement loop ─────────────────────────────────────────────
def test_enforcement_loop_no_op_when_disarmed(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
"""Loop should not call check() when the switch is not armed."""
from soosef.cli import _deadman_enforcement_loop
from soosef.fieldkit import deadman as deadman_mod
# Redirect the module-level DEADMAN_STATE constant so DeadmanSwitch() default is our tmp file
state_file = tmp_path / "deadman.json"
monkeypatch.setattr(deadman_mod, "DEADMAN_STATE", state_file)
check_calls = []
def fake_check(self):
check_calls.append("fired")
monkeypatch.setattr(deadman_mod.DeadmanSwitch, "check", fake_check)
iterations = [0]
def one_shot_sleep(n):
iterations[0] += 1
if iterations[0] >= 2:
raise StopIteration("stop test loop")
monkeypatch.setattr(time, "sleep", one_shot_sleep)
with pytest.raises(StopIteration):
_deadman_enforcement_loop(interval_seconds=0)
assert check_calls == []
def test_enforcement_loop_fires_when_overdue(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
"""Loop must call DeadmanSwitch.check() when armed and past interval + grace."""
from soosef.cli import _deadman_enforcement_loop
from soosef.fieldkit import deadman as deadman_mod
state_file = tmp_path / "deadman.json"
monkeypatch.setattr(deadman_mod, "DEADMAN_STATE", state_file)
last_checkin = datetime.now(UTC) - timedelta(hours=100)
_write_deadman_state(
state_file,
armed=True,
last_checkin=last_checkin,
interval_hours=24,
grace_hours=2,
)
check_calls = []
def fake_check(self):
check_calls.append("fired")
monkeypatch.setattr(deadman_mod.DeadmanSwitch, "check", fake_check)
monkeypatch.setattr(time, "sleep", lambda n: None)
_deadman_enforcement_loop(interval_seconds=0)
assert len(check_calls) == 1
def test_enforcement_loop_exits_after_firing(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
"""After firing, the loop must return and not call check() a second time."""
from soosef.cli import _deadman_enforcement_loop
from soosef.fieldkit import deadman as deadman_mod
state_file = tmp_path / "deadman.json"
monkeypatch.setattr(deadman_mod, "DEADMAN_STATE", state_file)
last_checkin = datetime.now(UTC) - timedelta(hours=100)
_write_deadman_state(state_file, armed=True, last_checkin=last_checkin)
check_calls = []
def fake_check(self):
check_calls.append("fired")
monkeypatch.setattr(deadman_mod.DeadmanSwitch, "check", fake_check)
monkeypatch.setattr(time, "sleep", lambda n: None)
_deadman_enforcement_loop(interval_seconds=0)
# Called exactly once — loop exited after firing
assert len(check_calls) == 1
def test_enforcement_loop_tolerates_exceptions(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
"""Transient errors in check() must not kill the loop."""
from soosef.cli import _deadman_enforcement_loop
from soosef.fieldkit import deadman as deadman_mod
state_file = tmp_path / "deadman.json"
monkeypatch.setattr(deadman_mod, "DEADMAN_STATE", state_file)
call_count = [0]
def counting_sleep(n):
call_count[0] += 1
if call_count[0] >= 3:
raise StopIteration("stop test loop")
monkeypatch.setattr(time, "sleep", counting_sleep)
error_calls = [0]
def flaky_is_armed(self):
error_calls[0] += 1
if error_calls[0] == 1:
raise OSError("state file temporarily unreadable")
return False # not armed — loop just skips
monkeypatch.setattr(deadman_mod.DeadmanSwitch, "is_armed", flaky_is_armed)
with pytest.raises(StopIteration):
_deadman_enforcement_loop(interval_seconds=0)
# Should have survived the first exception and continued
assert call_count[0] >= 2
# ── Unit tests: _start_deadman_thread ────────────────────────────────────────
def test_start_deadman_thread_is_daemon(monkeypatch: pytest.MonkeyPatch):
"""Thread must be a daemon so it dies with the process."""
from soosef.cli import _start_deadman_thread
# Patch the loop to exit immediately so the thread doesn't hang in tests
import soosef.cli as cli_mod
monkeypatch.setattr(cli_mod, "_deadman_enforcement_loop", lambda interval_seconds: None)
t = _start_deadman_thread(interval_seconds=60)
assert t is not None
assert t.daemon is True
assert t.name == "deadman-enforcement"
t.join(timeout=2)
# ── CLI integration: check-deadman ───────────────────────────────────────────
@pytest.fixture()
def cli_runner():
return CliRunner()
def test_check_deadman_disarmed(
tmp_path: Path, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch
):
"""check-deadman exits 0 and prints helpful message when not armed."""
from soosef.fieldkit import deadman as deadman_mod
from soosef.cli import main
# Point at an empty tmp dir so the real ~/.soosef/fieldkit/deadman.json isn't read
state_file = tmp_path / "deadman.json"
monkeypatch.setattr(deadman_mod, "DEADMAN_STATE", state_file)
result = cli_runner.invoke(main, ["fieldkit", "check-deadman"])
assert result.exit_code == 0
assert "not armed" in result.output
def test_check_deadman_armed_ok(
tmp_path: Path, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch
):
"""check-deadman exits 0 when armed and check-in is current."""
from soosef.fieldkit import deadman as deadman_mod
from soosef.cli import main
state_file = tmp_path / "deadman.json"
monkeypatch.setattr(deadman_mod, "DEADMAN_STATE", state_file)
last_checkin = datetime.now(UTC) - timedelta(hours=1)
_write_deadman_state(
state_file,
armed=True,
last_checkin=last_checkin,
interval_hours=24,
grace_hours=2,
)
result = cli_runner.invoke(main, ["fieldkit", "check-deadman"])
assert result.exit_code == 0
assert "OK" in result.output
def test_check_deadman_overdue_in_grace(
tmp_path: Path, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch
):
"""check-deadman exits 0 but prints OVERDUE warning when past interval but in grace."""
from soosef.fieldkit import deadman as deadman_mod
from soosef.cli import main
state_file = tmp_path / "deadman.json"
monkeypatch.setattr(deadman_mod, "DEADMAN_STATE", state_file)
# Past 24h interval but within 26h total (grace=2)
last_checkin = datetime.now(UTC) - timedelta(hours=25)
_write_deadman_state(
state_file,
armed=True,
last_checkin=last_checkin,
interval_hours=24,
grace_hours=2,
)
result = cli_runner.invoke(main, ["fieldkit", "check-deadman"])
# Not yet fired (grace not expired), so exit code is 0
assert result.exit_code == 0
assert "OVERDUE" in result.output
def test_check_deadman_fires_when_expired(
tmp_path: Path, cli_runner: CliRunner, monkeypatch: pytest.MonkeyPatch
):
"""check-deadman exits 2 when the switch has fully expired."""
from soosef.fieldkit import deadman as deadman_mod
from soosef.cli import main
state_file = tmp_path / "deadman.json"
monkeypatch.setattr(deadman_mod, "DEADMAN_STATE", state_file)
last_checkin = datetime.now(UTC) - timedelta(hours=100)
_write_deadman_state(
state_file,
armed=True,
last_checkin=last_checkin,
interval_hours=24,
grace_hours=2,
)
# Patch check() so we don't invoke the real killswitch during tests
monkeypatch.setattr(deadman_mod.DeadmanSwitch, "check", lambda self: None)
result = cli_runner.invoke(main, ["fieldkit", "check-deadman"])
assert result.exit_code == 2
assert "killswitch triggered" in result.output.lower() or "expired" in result.output.lower()