"""Unit: watchdog sweep.""" from __future__ import annotations import time import unittest import unittest.mock from datetime import datetime, timedelta from bot_bottle.orchestrator.model import STATUS_FROZEN, STATUS_RUNNING, RunRecord from bot_bottle.orchestrator.store import InMemoryStateStore from bot_bottle.orchestrator.watchdog import Watchdog from ._fakes import FakeRunner _NOW = datetime(2026, 7, 1, 12, 0, 0).astimezone() def _record(issue: int, status: str, checkin: str) -> RunRecord: return RunRecord( owner="o", repo="r", issue_number=issue, slug=f"s{issue}", agent_name="a", status=status, last_checkin_at=checkin, ) class WatchdogSweepTest(unittest.TestCase): def setUp(self): self.store = InMemoryStateStore() self.runner = FakeRunner() self.wd = Watchdog(store=self.store, runner=self.runner, timeout_secs=1800) def _status(self, issue: int) -> str: rec = self.store.get("o", "r", issue) assert rec is not None return rec.status def test_stale_running_is_frozen(self): stale = (_NOW - timedelta(minutes=31)).isoformat() self.store.upsert(_record(1, STATUS_RUNNING, stale)) fired = self.wd.sweep(_NOW) self.assertEqual([1], [r.issue_number for r in fired]) self.assertEqual(STATUS_FROZEN, self._status(1)) self.assertIn(("freeze", "s1"), self.runner.calls) def test_fresh_running_untouched(self): fresh = (_NOW - timedelta(minutes=5)).isoformat() self.store.upsert(_record(2, STATUS_RUNNING, fresh)) self.assertEqual([], self.wd.sweep(_NOW)) self.assertEqual(STATUS_RUNNING, self._status(2)) def test_non_running_ignored(self): stale = (_NOW - timedelta(hours=2)).isoformat() self.store.upsert(_record(3, STATUS_FROZEN, stale)) self.assertEqual([], self.wd.sweep(_NOW)) def test_unparseable_checkin_skipped(self): self.store.upsert(_record(4, STATUS_RUNNING, "not-a-time")) self.assertEqual([], self.wd.sweep(_NOW)) def test_start_and_stop(self): # Exercises the daemon-thread start/stop path; stop sets the event # so the loop's wait returns immediately. self.wd.start() self.wd.stop() def test_loop_sweeps_stale_record(self): # Patch tick to near-zero so the loop iterates quickly. stale = (_NOW - timedelta(hours=1)).isoformat() self.store.upsert(_record(5, STATUS_RUNNING, stale)) with unittest.mock.patch("bot_bottle.orchestrator.watchdog._TICK_SECS", 0.01): self.wd.start() time.sleep(0.05) # enough for several iterations at 0.01s tick self.wd.stop() rec = self.store.get("o", "r", 5) assert rec is not None self.assertEqual(STATUS_FROZEN, rec.status) if __name__ == "__main__": unittest.main()