""" Deep behavioral tests for scheduler configuration, DocumentSchedulerSettings, TTL cache operations, and config loading. """ import threading from unittest.mock import Mock import pytest from cachetools import TTLCache from local_deep_research.scheduler.background import ( DocumentSchedulerSettings, BackgroundJobScheduler, ) # --- DocumentSchedulerSettings frozen dataclass --- class TestDocumentSchedulerSettingsDefaults: """Tests for DocumentSchedulerSettings default values.""" def test_default_enabled_true(self): s = DocumentSchedulerSettings() assert s.enabled is True def test_default_interval_seconds(self): s = DocumentSchedulerSettings() assert s.interval_seconds == 1800 def test_default_download_pdfs_false(self): s = DocumentSchedulerSettings() assert s.download_pdfs is False def test_default_extract_text_true(self): s = DocumentSchedulerSettings() assert s.extract_text is True def test_default_generate_rag_false(self): s = DocumentSchedulerSettings() assert s.generate_rag is False def test_default_last_run_empty(self): s = DocumentSchedulerSettings() assert s.last_run == "" class TestDocumentSchedulerSettingsCustom: """Tests for custom DocumentSchedulerSettings values.""" def test_custom_enabled_false(self): s = DocumentSchedulerSettings(enabled=False) assert s.enabled is False def test_custom_interval(self): s = DocumentSchedulerSettings(interval_seconds=3600) assert s.interval_seconds == 3600 def test_custom_download_pdfs(self): s = DocumentSchedulerSettings(download_pdfs=True) assert s.download_pdfs is True def test_custom_extract_text_false(self): s = DocumentSchedulerSettings(extract_text=False) assert s.extract_text is False def test_custom_generate_rag_true(self): s = DocumentSchedulerSettings(generate_rag=True) assert s.generate_rag is True def test_custom_last_run(self): s = DocumentSchedulerSettings(last_run="2025-01-15T10:00:00") assert s.last_run == "2025-01-15T10:00:00" class TestDocumentSchedulerSettingsFrozen: """Tests that DocumentSchedulerSettings is immutable.""" def test_cannot_modify_enabled(self): s = DocumentSchedulerSettings() with pytest.raises(AttributeError): s.enabled = False def test_cannot_modify_interval(self): s = DocumentSchedulerSettings() with pytest.raises(AttributeError): s.interval_seconds = 100 def test_cannot_modify_download_pdfs(self): s = DocumentSchedulerSettings() with pytest.raises(AttributeError): s.download_pdfs = True def test_cannot_modify_extract_text(self): s = DocumentSchedulerSettings() with pytest.raises(AttributeError): s.extract_text = False def test_cannot_modify_generate_rag(self): s = DocumentSchedulerSettings() with pytest.raises(AttributeError): s.generate_rag = True def test_cannot_modify_last_run(self): s = DocumentSchedulerSettings() with pytest.raises(AttributeError): s.last_run = "new" class TestDocumentSchedulerSettingsClassmethod: """Tests for the defaults() classmethod.""" def test_defaults_returns_instance(self): s = DocumentSchedulerSettings.defaults() assert isinstance(s, DocumentSchedulerSettings) def test_defaults_enabled_true(self): s = DocumentSchedulerSettings.defaults() assert s.enabled is True def test_defaults_interval_1800(self): s = DocumentSchedulerSettings.defaults() assert s.interval_seconds == 1800 def test_defaults_download_pdfs_false(self): s = DocumentSchedulerSettings.defaults() assert s.download_pdfs is False def test_defaults_extract_text_true(self): s = DocumentSchedulerSettings.defaults() assert s.extract_text is True def test_defaults_generate_rag_false(self): s = DocumentSchedulerSettings.defaults() assert s.generate_rag is False def test_defaults_last_run_empty(self): s = DocumentSchedulerSettings.defaults() assert s.last_run == "" class TestDocumentSchedulerSettingsEquality: """Tests for dataclass equality.""" def test_equal_defaults(self): a = DocumentSchedulerSettings() b = DocumentSchedulerSettings() assert a == b def test_equal_custom(self): a = DocumentSchedulerSettings(enabled=False, interval_seconds=600) b = DocumentSchedulerSettings(enabled=False, interval_seconds=600) assert a == b def test_not_equal_different_enabled(self): a = DocumentSchedulerSettings(enabled=True) b = DocumentSchedulerSettings(enabled=False) assert a != b def test_not_equal_different_interval(self): a = DocumentSchedulerSettings(interval_seconds=100) b = DocumentSchedulerSettings(interval_seconds=200) assert a != b # --- _load_default_config --- class TestLoadDefaultConfig: """Tests for BackgroundJobScheduler._load_default_config.""" @pytest.fixture(autouse=True) def reset_singleton(self): """Reset the singleton before each test.""" BackgroundJobScheduler._instance = None yield BackgroundJobScheduler._instance = None def test_returns_dict(self): s = BackgroundJobScheduler() config = s._load_default_config() assert isinstance(config, dict) def test_has_enabled(self): s = BackgroundJobScheduler() config = s._load_default_config() assert config["enabled"] is True def test_has_retention_hours(self): s = BackgroundJobScheduler() config = s._load_default_config() assert config["retention_hours"] == 48 def test_has_cleanup_interval_hours(self): s = BackgroundJobScheduler() config = s._load_default_config() assert config["cleanup_interval_hours"] == 1 def test_has_max_jitter_seconds(self): s = BackgroundJobScheduler() config = s._load_default_config() assert config["max_jitter_seconds"] == 300 def test_has_max_concurrent_jobs(self): s = BackgroundJobScheduler() config = s._load_default_config() assert config["max_concurrent_jobs"] == 10 def test_has_subscription_batch_size(self): s = BackgroundJobScheduler() config = s._load_default_config() assert config["subscription_batch_size"] == 5 def test_has_activity_check_interval(self): s = BackgroundJobScheduler() config = s._load_default_config() assert config["activity_check_interval_minutes"] == 5 # --- _get_setting --- class TestGetSetting: """Tests for BackgroundJobScheduler._get_setting.""" @pytest.fixture(autouse=True) def reset_singleton(self): BackgroundJobScheduler._instance = None yield BackgroundJobScheduler._instance = None def test_returns_default_without_settings_manager(self): s = BackgroundJobScheduler() result = s._get_setting("test.key", "default_val") assert result == "default_val" def test_returns_default_int(self): s = BackgroundJobScheduler() result = s._get_setting("test.key", 42) assert result == 42 def test_returns_default_bool(self): s = BackgroundJobScheduler() result = s._get_setting("test.key", False) assert result is False def test_uses_settings_manager_when_available(self): s = BackgroundJobScheduler() sm = Mock() sm.get_setting.return_value = "from_settings" s.settings_manager = sm result = s._get_setting("news.key", "default") assert result == "from_settings" def test_settings_manager_called_with_key(self): s = BackgroundJobScheduler() sm = Mock() sm.get_setting.return_value = "val" s.settings_manager = sm s._get_setting("news.scheduler.enabled", True) sm.get_setting.assert_called_once_with( "news.scheduler.enabled", default=True ) # --- invalidate_user_settings_cache --- class TestInvalidateUserSettingsCache: """Tests for cache invalidation of a single user.""" @pytest.fixture(autouse=True) def reset_singleton(self): BackgroundJobScheduler._instance = None yield BackgroundJobScheduler._instance = None def test_returns_true_if_found(self): s = BackgroundJobScheduler() s._settings_cache["user1"] = DocumentSchedulerSettings() assert s.invalidate_user_settings_cache("user1") is True def test_returns_false_if_not_found(self): s = BackgroundJobScheduler() assert s.invalidate_user_settings_cache("nonexistent") is False def test_removes_entry(self): s = BackgroundJobScheduler() s._settings_cache["user1"] = DocumentSchedulerSettings() s.invalidate_user_settings_cache("user1") assert "user1" not in s._settings_cache def test_does_not_affect_other_users(self): s = BackgroundJobScheduler() s._settings_cache["user1"] = DocumentSchedulerSettings() s._settings_cache["user2"] = DocumentSchedulerSettings() s.invalidate_user_settings_cache("user1") assert "user2" in s._settings_cache def test_empty_cache_returns_false(self): s = BackgroundJobScheduler() assert s.invalidate_user_settings_cache("anyone") is False # --- invalidate_all_settings_cache --- class TestInvalidateAllSettingsCache: """Tests for clearing the entire settings cache.""" @pytest.fixture(autouse=True) def reset_singleton(self): BackgroundJobScheduler._instance = None yield BackgroundJobScheduler._instance = None def test_returns_count_cleared(self): s = BackgroundJobScheduler() s._settings_cache["u1"] = DocumentSchedulerSettings() s._settings_cache["u2"] = DocumentSchedulerSettings() count = s.invalidate_all_settings_cache() assert count == 2 def test_returns_zero_if_empty(self): s = BackgroundJobScheduler() count = s.invalidate_all_settings_cache() assert count == 0 def test_cache_empty_after_clear(self): s = BackgroundJobScheduler() s._settings_cache["u1"] = DocumentSchedulerSettings() s.invalidate_all_settings_cache() assert len(s._settings_cache) == 0 def test_returns_correct_count_large(self): s = BackgroundJobScheduler() for i in range(50): s._settings_cache[f"user{i}"] = DocumentSchedulerSettings() count = s.invalidate_all_settings_cache() assert count == 50 # --- Singleton behavior --- class TestNewsSchedulerSingleton: """Tests for the singleton pattern.""" @pytest.fixture(autouse=True) def reset_singleton(self): BackgroundJobScheduler._instance = None yield BackgroundJobScheduler._instance = None def test_same_instance(self): a = BackgroundJobScheduler() b = BackgroundJobScheduler() assert a is b def test_initialized_flag_set(self): s = BackgroundJobScheduler() assert s._initialized is True def test_user_sessions_empty(self): s = BackgroundJobScheduler() assert s.user_sessions == {} def test_is_running_false(self): s = BackgroundJobScheduler() assert s.is_running is False def test_has_lock(self): s = BackgroundJobScheduler() assert isinstance(s.lock, type(threading.Lock())) def test_has_settings_cache(self): s = BackgroundJobScheduler() assert isinstance(s._settings_cache, TTLCache) def test_settings_cache_maxsize_100(self): s = BackgroundJobScheduler() assert s._settings_cache.maxsize == 100 # --- Settings cache with TTL --- class TestSettingsCacheTTL: """Tests for TTLCache behavior in settings cache.""" @pytest.fixture(autouse=True) def reset_singleton(self): BackgroundJobScheduler._instance = None yield BackgroundJobScheduler._instance = None def test_cache_stores_value(self): s = BackgroundJobScheduler() settings = DocumentSchedulerSettings(enabled=False) s._settings_cache["user1"] = settings assert s._settings_cache["user1"] == settings def test_cache_overwrites_value(self): s = BackgroundJobScheduler() s._settings_cache["user1"] = DocumentSchedulerSettings(enabled=True) s._settings_cache["user1"] = DocumentSchedulerSettings(enabled=False) assert s._settings_cache["user1"].enabled is False def test_cache_get_missing_returns_none(self): s = BackgroundJobScheduler() assert s._settings_cache.get("nonexistent") is None def test_cache_respects_maxsize(self): s = BackgroundJobScheduler() # Cache maxsize is 100, adding 101 should evict oldest for i in range(101): s._settings_cache[f"user{i}"] = DocumentSchedulerSettings() assert len(s._settings_cache) <= 100