"""WatchdogStore unit tests (REQ-034 / BAT-010).""" from __future__ import annotations from datetime import datetime, timedelta, timezone from trex_core.application.watchdog import WatchdogStore def _now(): return datetime.now(tz=timezone.utc) class TestWatchdogUpdate: def test_update_marks_slot_as_seen(self): """AC-1: update(slot_id) records current time in store.""" store = WatchdogStore() before = _now() store.update("slot-A") after = _now() last = store._store.get("slot-A") assert last is not None assert before <= last <= after def test_update_overwrites_previous_timestamp(self): """update() on already-seen slot refreshes the timestamp.""" store = WatchdogStore() store.update("slot-A") first = store._store["slot-A"] store.update("slot-A") second = store._store["slot-A"] assert second >= first class TestIsExpired: def test_never_seen_slot_is_expired(self): """AC-2: slot never updated → is_expired returns True.""" store = WatchdogStore() assert store.is_expired("slot-X", timeout_sec=120) is True def test_recently_updated_slot_not_expired(self): """AC-3: slot updated now → not expired with generous timeout.""" store = WatchdogStore() store.update("slot-A") assert store.is_expired("slot-A", timeout_sec=120) is False def test_old_update_is_expired(self): """AC-2: slot last updated 200s ago is expired with 120s timeout.""" store = WatchdogStore() old_time = _now() - timedelta(seconds=200) store._store["slot-A"] = old_time assert store.is_expired("slot-A", timeout_sec=120) is True def test_just_within_timeout_not_expired(self): """Slot updated 100s ago is NOT expired with 120s timeout.""" store = WatchdogStore() recent = _now() - timedelta(seconds=100) store._store["slot-A"] = recent assert store.is_expired("slot-A", timeout_sec=120) is False class TestGracePeriod: def test_in_grace_period_immediately_after_init(self): """AC-4: just-created store is in grace period for 60s.""" store = WatchdogStore() assert store.in_grace_period(grace_sec=60) is True def test_not_in_grace_period_after_expiry(self): """AC-4: store started >60s ago is no longer in grace period.""" store = WatchdogStore() store._started_at = _now() - timedelta(seconds=61) assert store.in_grace_period(grace_sec=60) is False def test_grace_period_exactly_at_boundary(self): """Elapsed == grace_sec is NOT in grace (strictly less than).""" store = WatchdogStore() store._started_at = _now() - timedelta(seconds=60) assert store.in_grace_period(grace_sec=60) is False class TestGracePeriodDefer: def test_grace_period_defers_expiry_check(self): """AC-4: callers skip is_expired during grace period (combined scenario). The REQ states that during grace period no slot should be treated as expired. This test verifies the pattern: in_grace_period() → True means callers must not act on is_expired() results. """ store = WatchdogStore() # Store is brand-new → in grace period assert store.in_grace_period(grace_sec=60) is True # Even though slot is never seen (would be expired), grace is active assert store.is_expired("slot-never-seen", timeout_sec=120) is True # Caller should defer: grace overrides expiry judgement. effective_expired = store.is_expired("slot-never-seen", timeout_sec=120) and not store.in_grace_period(grace_sec=60) assert effective_expired is False class TestStoreReset: def test_new_store_is_empty(self): """AC-5: new WatchdogStore has no entries (simulates daemon restart).""" store = WatchdogStore() assert len(store._store) == 0