"""Unit tests for the connectivity health state machine.""" from dataclasses import dataclass import pytest from app.models.device import DeviceMonitoringPolicy from app.services.state_machine import evaluate_health @dataclass(frozen=True) class RecentProbe: """Minimal real-data-shaped record consumed by the pure state machine.""" sent_count: int received_count: int is_valid: bool = True def loss(sent_count: int) -> RecentProbe: return RecentProbe(sent_count=sent_count, received_count=0) def partial(sent_count: int, received_count: int) -> RecentProbe: return RecentProbe( sent_count=sent_count, received_count=received_count, ) def clean(sent_count: int) -> RecentProbe: return RecentProbe(sent_count=sent_count, received_count=sent_count) @pytest.fixture def policy() -> DeviceMonitoringPolicy: return DeviceMonitoringPolicy( probe_packets_per_round=3, offline_consecutive_rounds=2, degraded_window_rounds=5, degraded_loss_percent=20.0, recovery_consecutive_clean_rounds=3, ) def test_two_full_loss_rounds_take_device_offline(policy): """Configured consecutive full-loss rounds transition to offline once.""" decision = evaluate_health("online", [loss(3), loss(3)], policy) assert (decision.next_status, decision.event_type) == ( "offline", "offline", ) assert decision.should_notify is True def test_offline_priority_wins_over_aggregate_degraded_loss(policy): """Full-loss streaks are classified offline even when the window also degrades.""" recent = [partial(3, 2)] * 3 + [loss(3), loss(3)] decision = evaluate_health("online", recent, policy) assert decision.next_status == "offline" assert decision.event_type == "offline" def test_five_round_window_with_loss_is_degraded(policy): """Aggregate loss at the configured window threshold is degraded.""" decision = evaluate_health( "online", [partial(3, 2)] * 5, policy, ) assert decision.next_status == "degraded" assert decision.event_type == "degraded" def test_degraded_requires_a_complete_window(policy): """A partial history cannot satisfy an aggregate-window decision.""" decision = evaluate_health( "online", [partial(3, 2)] * 4, policy, ) assert decision.next_status == "online" assert decision.event_type is None def test_three_clean_rounds_recovers(policy): """An alerted device recovers only after the configured clean streak.""" decision = evaluate_health( "offline", [clean(3)] * 3, policy, ) assert (decision.next_status, decision.event_type) == ( "online", "recovered", ) assert decision.should_notify is True def test_clean_online_device_does_not_emit_duplicate_event(policy): """Clean probes retain an already-online state without notifying.""" decision = evaluate_health( "online", [clean(3)] * 5, policy, ) assert decision.next_status == "online" assert decision.event_type is None assert decision.should_notify is False def test_latest_invalid_probe_retains_state(policy): """A malformed current result cannot reuse old loss to trigger a fault.""" recent = [loss(3), RecentProbe(3, 0, is_valid=False)] decision = evaluate_health("online", recent, policy) assert decision.next_status == "online" assert decision.event_type is None assert decision.should_notify is False def test_offline_does_not_recover_before_clean_streak_is_complete(policy): """Recovery debounce retains offline until enough clean rounds exist.""" decision = evaluate_health( "offline", [clean(3), clean(3)], policy, ) assert decision.next_status == "offline" assert decision.event_type is None