"""Tests for WeFax auto-scheduler behavior and regressions.""" from __future__ import annotations from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock, patch from utils.wefax_scheduler import ScheduledBroadcast, WeFaxScheduler class TestWeFaxScheduler: """WeFaxScheduler regression tests.""" @patch('threading.Timer') def test_refresh_reschedules_same_utc_slot_next_day(self, mock_timer): """Completed broadcasts must not block the next day's same UTC slot.""" scheduler = WeFaxScheduler() scheduler._enabled = True scheduler._station = 'USCG Kodiak' scheduler._callsign = 'NOJ' scheduler._frequency_khz = 4298.0 now = datetime.now(timezone.utc) utc_time = (now - timedelta(hours=2)).strftime('%H:%M') today = now.date().isoformat() prior = ScheduledBroadcast( station='USCG Kodiak', callsign='NOJ', frequency_khz=4298.0, utc_time=utc_time, duration_min=20, content='Chart', occurrence_date=today, ) prior.status = 'complete' scheduler._broadcasts = [prior] mock_timer.return_value = MagicMock() with patch('utils.wefax_scheduler.get_station', return_value={ 'name': 'USCG Kodiak', 'schedule': [{ 'utc': utc_time, 'duration_min': 20, 'content': 'Chart', }], }): scheduler._refresh_schedule() capture_calls = [ c for c in mock_timer.call_args_list if len(c.args) >= 2 and getattr(c.args[1], '__name__', '') == '_execute_capture' ] assert capture_calls, "Expected a capture timer for the next-day occurrence" scheduled = [b for b in scheduler._broadcasts if b.status == 'scheduled'] assert len(scheduled) == 1 assert scheduled[0].occurrence_date != today def test_execute_capture_stops_immediately_if_window_elapsed(self): """If stop delay computes to <= 0, capture should close out immediately.""" scheduler = WeFaxScheduler() scheduler._enabled = True scheduler._callsign = 'NOJ' scheduler._frequency_khz = 4298.0 scheduler._device = 0 scheduler._gain = 40.0 scheduler._ioc = 576 scheduler._lpm = 120 scheduler._direct_sampling = True now = datetime.now(timezone.utc) sb = ScheduledBroadcast( station='USCG Kodiak', callsign='NOJ', frequency_khz=4298.0, utc_time=now.strftime('%H:%M'), duration_min=0, content='Late chart', occurrence_date=now.date().isoformat(), ) sb.status = 'scheduled' mock_decoder = MagicMock() mock_decoder.is_running = False mock_decoder.start.return_value = True with patch('utils.wefax_scheduler.get_wefax_decoder', return_value=mock_decoder), \ patch('utils.wefax_scheduler.WEFAX_CAPTURE_BUFFER_SECONDS', 0), \ patch('app.claim_sdr_device', return_value=None), \ patch.object(scheduler, '_stop_capture') as mock_stop_capture: scheduler._execute_capture_inner(sb) mock_stop_capture.assert_called_once() @patch('threading.Timer') def test_terminal_progress_releases_scheduler_device_early(self, mock_timer): """Scheduler captures must release SDR as soon as terminal progress arrives.""" scheduler = WeFaxScheduler() scheduler._enabled = True scheduler._callsign = 'NOJ' scheduler._frequency_khz = 4298.0 scheduler._device = 0 scheduler._gain = 40.0 scheduler._ioc = 576 scheduler._lpm = 120 scheduler._direct_sampling = True sb = ScheduledBroadcast( station='USCG Kodiak', callsign='NOJ', frequency_khz=4298.0, utc_time='12:00', duration_min=20, content='Chart', occurrence_date='2026-01-01', ) sb.status = 'scheduled' mock_decoder = MagicMock() mock_decoder.is_running = False mock_decoder.start.return_value = True mock_timer.return_value = MagicMock() with patch('utils.wefax_scheduler.get_wefax_decoder', return_value=mock_decoder), \ patch('app.claim_sdr_device', return_value=None), \ patch('app.release_sdr_device') as mock_release: scheduler._execute_capture_inner(sb) progress_cb = mock_decoder.set_callback.call_args[0][0] progress_cb({ 'type': 'wefax_progress', 'status': 'error', 'message': 'rtl_fm failed', }) mock_release.assert_called_once_with(0) assert sb.status == 'skipped' def test_stop_capture_non_capturing_only_releases(self): """_stop_capture should be idempotent when capture already ended.""" scheduler = WeFaxScheduler() sb = ScheduledBroadcast( station='USCG Kodiak', callsign='NOJ', frequency_khz=4298.0, utc_time='12:00', duration_min=20, content='Chart', occurrence_date='2026-01-01', ) sb.status = 'complete' release_fn = MagicMock() with patch('utils.wefax_scheduler.get_wefax_decoder') as mock_get_decoder: scheduler._stop_capture(sb, release_fn) release_fn.assert_called_once() mock_get_decoder.assert_not_called()