"""Tests for briefing_scheduler slot gating and work-day gate.""" import pytest from unittest.mock import AsyncMock, MagicMock, patch def _make_scheduler(): """Return a mock APScheduler with tracked job IDs.""" sched = MagicMock() sched.get_job.return_value = None return sched def test_add_user_jobs_skips_disabled_slots(): """_add_user_jobs should not register jobs for slots set to False in config.""" from fabledassistant.services import briefing_scheduler as bs mock_sched = _make_scheduler() bs._scheduler = mock_sched bs._loop = MagicMock() config = {"slots": {"compilation": True, "morning": False, "midday": False, "afternoon": False}} bs._add_user_jobs(user_id=1, tz="UTC", config=config) added_ids = [call.kwargs.get("id") or call.args[3] for call in mock_sched.add_job.call_args_list] # Only compilation should be scheduled assert any("compilation" in jid for jid in added_ids) assert not any("morning" in jid for jid in added_ids) assert not any("midday" in jid for jid in added_ids) assert not any("afternoon" in jid for jid in added_ids) def test_add_user_jobs_schedules_all_when_config_missing(): """_add_user_jobs with no config should schedule all 4 slots (safe default).""" from fabledassistant.services import briefing_scheduler as bs mock_sched = _make_scheduler() bs._scheduler = mock_sched bs._loop = MagicMock() bs._add_user_jobs(user_id=1, tz="UTC", config=None) added_ids = [ call.kwargs.get("id") or call.args[3] for call in mock_sched.add_job.call_args_list ] assert any("compilation" in jid for jid in added_ids) assert any("morning" in jid for jid in added_ids) assert any("midday" in jid for jid in added_ids) assert any("afternoon" in jid for jid in added_ids) @pytest.mark.asyncio async def test_run_slot_skips_morning_on_non_work_day(): """_run_slot_for_user should return early for morning slot when today is not a work day.""" from fabledassistant.services import briefing_scheduler as bs fake_profile = MagicMock() fake_profile.work_schedule = {"days": ["Mon", "Tue", "Wed", "Thu", "Fri"]} # Patch datetime at module level (it's imported at module level in briefing_scheduler) # Patch get_setting and get_profile at their source modules (they're lazily imported) with patch("fabledassistant.services.briefing_scheduler.datetime") as mock_dt, \ patch("fabledassistant.services.user_profile.get_profile", new_callable=AsyncMock, return_value=fake_profile), \ patch("fabledassistant.services.settings.get_setting", new_callable=AsyncMock, return_value="UTC"), \ patch("fabledassistant.services.briefing_pipeline.run_slot_injection", new_callable=AsyncMock) as mock_inject: mock_now = MagicMock() mock_now.strftime.return_value = "Sat" mock_dt.now.return_value = mock_now await bs._run_slot_for_user(user_id=1, slot="morning") mock_inject.assert_not_called() @pytest.mark.asyncio async def test_run_slot_morning_runs_on_work_day(): """_run_slot_for_user should proceed for morning slot when today is a work day.""" from fabledassistant.services import briefing_scheduler as bs fake_profile = MagicMock() fake_profile.work_schedule = {"days": ["Mon", "Tue", "Wed", "Thu", "Fri"]} with patch("fabledassistant.services.briefing_scheduler.datetime") as mock_dt, \ patch("fabledassistant.services.user_profile.get_profile", new_callable=AsyncMock, return_value=fake_profile), \ patch("fabledassistant.services.settings.get_setting", new_callable=AsyncMock, return_value="UTC"), \ patch("fabledassistant.services.briefing_conversations.get_or_create_today_conversation", new_callable=AsyncMock) as mock_conv, \ patch("fabledassistant.services.briefing_pipeline.run_slot_injection", new_callable=AsyncMock, return_value="") as mock_inject, \ patch("fabledassistant.services.briefing_conversations.post_message", new_callable=AsyncMock), \ patch("fabledassistant.services.push.send_push_notification", new_callable=AsyncMock): mock_now = MagicMock() mock_now.strftime.return_value = "Mon" mock_dt.now.return_value = mock_now mock_conv.return_value = MagicMock(id=1) await bs._run_slot_for_user(user_id=1, slot="morning") mock_inject.assert_called_once() @pytest.mark.asyncio async def test_settings_put_timezone_updates_scheduler(): """Saving user_timezone should call update_user_schedule when briefing is enabled.""" import json from fabledassistant.services.briefing_scheduler import update_user_schedule enabled_config = json.dumps({"enabled": True, "slots": {}, "locations": {}}) config = json.loads(enabled_config) with patch("fabledassistant.services.briefing_scheduler._add_user_jobs") as mock_add: update_user_schedule(1, config, tz_override="America/New_York") mock_add.assert_called_once() call_args = mock_add.call_args assert call_args.args[0] == 1 # user_id assert call_args.args[2] == config # config passed through