""" Tests for web_search_engines/rate_limiting/tracker.py Tests cover: - AdaptiveRateLimitTracker initialization - _apply_profile method - _load_estimates method - get_wait_time method - record_attempt method """ from unittest.mock import patch class TestAdaptiveRateLimitTrackerInit: """Tests for AdaptiveRateLimitTracker initialization.""" def test_init_default_settings(self): """Test initialization with default settings.""" from local_deep_research.config.thread_settings import ( NoSettingsContextError, ) with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.get_setting_from_snapshot" ) as mock_get_setting: mock_get_setting.side_effect = NoSettingsContextError("No settings") with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.logger" ): from local_deep_research.web_search_engines.rate_limiting.tracker import ( AdaptiveRateLimitTracker, ) tracker = AdaptiveRateLimitTracker(programmatic_mode=True) assert tracker.memory_window == 100 assert tracker.enabled is False # Programmatic mode default assert tracker.programmatic_mode is True def test_init_with_settings_snapshot(self): """Test initialization with settings snapshot.""" settings = { "rate_limiting.memory_window": 50, "rate_limiting.exploration_rate": 0.15, "rate_limiting.learning_rate": 0.4, "rate_limiting.decay_per_day": 0.9, "rate_limiting.enabled": True, "rate_limiting.profile": "aggressive", } with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.get_setting_from_snapshot" ) as mock_get_setting: def get_setting_side_effect(key, **kwargs): return settings.get(key) mock_get_setting.side_effect = get_setting_side_effect with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.logger" ): from local_deep_research.web_search_engines.rate_limiting.tracker import ( AdaptiveRateLimitTracker, ) tracker = AdaptiveRateLimitTracker( settings_snapshot=settings, programmatic_mode=True ) assert tracker.settings_snapshot == settings def test_init_in_memory_structures(self): """Test that in-memory structures are initialized.""" from local_deep_research.config.thread_settings import ( NoSettingsContextError, ) with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.get_setting_from_snapshot" ) as mock_get_setting: mock_get_setting.side_effect = NoSettingsContextError("No settings") with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.logger" ): from local_deep_research.web_search_engines.rate_limiting.tracker import ( AdaptiveRateLimitTracker, ) tracker = AdaptiveRateLimitTracker(programmatic_mode=True) assert tracker.recent_attempts == {} assert tracker.current_estimates == {} assert tracker._estimates_loaded is True # In programmatic mode class TestApplyProfile: """Tests for _apply_profile method.""" def test_conservative_profile(self): """Test conservative profile reduces exploration.""" from local_deep_research.config.thread_settings import ( NoSettingsContextError, ) with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.get_setting_from_snapshot" ) as mock_get_setting: mock_get_setting.side_effect = NoSettingsContextError("No settings") with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.logger" ): from local_deep_research.web_search_engines.rate_limiting.tracker import ( AdaptiveRateLimitTracker, ) tracker = AdaptiveRateLimitTracker(programmatic_mode=True) tracker._apply_profile("conservative") # Conservative should reduce exploration rate assert tracker.exploration_rate <= 0.05 def test_aggressive_profile(self): """Test aggressive profile increases exploration.""" from local_deep_research.config.thread_settings import ( NoSettingsContextError, ) with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.get_setting_from_snapshot" ) as mock_get_setting: mock_get_setting.side_effect = NoSettingsContextError("No settings") with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.logger" ): from local_deep_research.web_search_engines.rate_limiting.tracker import ( AdaptiveRateLimitTracker, ) tracker = AdaptiveRateLimitTracker(programmatic_mode=True) tracker._apply_profile("aggressive") # Aggressive should increase exploration rate (but max 0.2) assert tracker.exploration_rate <= 0.2 def test_balanced_profile(self): """Test balanced profile keeps settings as-is.""" from local_deep_research.config.thread_settings import ( NoSettingsContextError, ) with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.get_setting_from_snapshot" ) as mock_get_setting: mock_get_setting.side_effect = NoSettingsContextError("No settings") with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.logger" ): from local_deep_research.web_search_engines.rate_limiting.tracker import ( AdaptiveRateLimitTracker, ) tracker = AdaptiveRateLimitTracker(programmatic_mode=True) original_rate = 0.1 tracker.exploration_rate = original_rate tracker._apply_profile("balanced") assert tracker.exploration_rate == original_rate class TestGetWaitTime: """Tests for get_wait_time method.""" def test_disabled_returns_minimal_wait(self): """Test that disabled rate limiting returns minimal wait.""" from local_deep_research.config.thread_settings import ( NoSettingsContextError, ) with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.get_setting_from_snapshot" ) as mock_get_setting: mock_get_setting.side_effect = NoSettingsContextError("No settings") with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.logger" ): from local_deep_research.web_search_engines.rate_limiting.tracker import ( AdaptiveRateLimitTracker, ) tracker = AdaptiveRateLimitTracker(programmatic_mode=True) tracker.enabled = False wait_time = tracker.get_wait_time("test_engine") assert wait_time == 0.1 def test_new_engine_returns_optimistic_default(self): """Test that unknown engine gets optimistic default.""" from local_deep_research.config.thread_settings import ( NoSettingsContextError, ) with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.get_setting_from_snapshot" ) as mock_get_setting: mock_get_setting.side_effect = NoSettingsContextError("No settings") with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.logger" ): with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.get_search_context" ) as mock_context: mock_context.return_value = {"username": "test"} from local_deep_research.web_search_engines.rate_limiting.tracker import ( AdaptiveRateLimitTracker, ) tracker = AdaptiveRateLimitTracker(programmatic_mode=True) tracker.enabled = True tracker._estimates_loaded = True wait_time = tracker.get_wait_time("new_engine") assert wait_time == 0.1 # Optimistic default def test_known_engine_uses_estimate(self): """Test that known engine uses learned estimate.""" from local_deep_research.config.thread_settings import ( NoSettingsContextError, ) with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.get_setting_from_snapshot" ) as mock_get_setting: mock_get_setting.side_effect = NoSettingsContextError("No settings") with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.logger" ): with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.get_search_context" ) as mock_context: mock_context.return_value = {"username": "test"} from local_deep_research.web_search_engines.rate_limiting.tracker import ( AdaptiveRateLimitTracker, ) tracker = AdaptiveRateLimitTracker(programmatic_mode=True) tracker.enabled = True tracker._estimates_loaded = True tracker.exploration_rate = ( 0 # Disable exploration for predictable test ) tracker.current_estimates["test_engine"] = { "base": 1.0, "min": 0.5, "max": 2.0, "confidence": 1.0, } wait_time = tracker.get_wait_time("test_engine") # Should be within jitter range of base assert 0.5 <= wait_time <= 2.0 class TestLoadEstimates: """Tests for _load_estimates method.""" def test_programmatic_mode_skips_loading(self): """Test that programmatic mode skips database loading.""" from local_deep_research.config.thread_settings import ( NoSettingsContextError, ) with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.get_setting_from_snapshot" ) as mock_get_setting: mock_get_setting.side_effect = NoSettingsContextError("No settings") with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.logger" ): from local_deep_research.web_search_engines.rate_limiting.tracker import ( AdaptiveRateLimitTracker, ) tracker = AdaptiveRateLimitTracker(programmatic_mode=True) assert tracker._estimates_loaded is True def test_ci_mode_skips_loading(self): """Test that CI mode skips database loading.""" from local_deep_research.config.thread_settings import ( NoSettingsContextError, ) with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.get_setting_from_snapshot" ) as mock_get_setting: mock_get_setting.side_effect = NoSettingsContextError("No settings") with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.logger" ): with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.is_ci_environment", return_value=True, ): from local_deep_research.web_search_engines.rate_limiting.tracker import ( AdaptiveRateLimitTracker, ) tracker = AdaptiveRateLimitTracker(programmatic_mode=False) # Should mark as not loaded since CI mode assert tracker._estimates_loaded is False class TestGetDbImports: """Tests for _get_db_imports function.""" def test_get_db_imports_success(self): """Test successful database import loading.""" from local_deep_research.web_search_engines.rate_limiting.tracker import ( _get_db_imports, ) # Reset global state import local_deep_research.web_search_engines.rate_limiting.tracker as tracker_module tracker_module._db_imports = None imports = _get_db_imports() assert "RateLimitAttempt" in imports assert "RateLimitEstimate" in imports assert "get_user_db_session" in imports def test_get_db_imports_cached(self): """Test that imports are cached.""" import local_deep_research.web_search_engines.rate_limiting.tracker as tracker_module # Set cached value tracker_module._db_imports = {"cached": True} from local_deep_research.web_search_engines.rate_limiting.tracker import ( _get_db_imports, ) imports = _get_db_imports() assert imports == {"cached": True} # Reset for other tests tracker_module._db_imports = None class TestEnsureEstimatesLoaded: """Tests for _ensure_estimates_loaded method.""" def test_already_loaded_returns_early(self): """Test that already loaded returns early.""" from local_deep_research.config.thread_settings import ( NoSettingsContextError, ) with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.get_setting_from_snapshot" ) as mock_get_setting: mock_get_setting.side_effect = NoSettingsContextError("No settings") with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.logger" ): with patch( "local_deep_research.web_search_engines.rate_limiting.tracker._get_db_imports" ) as mock_imports: from local_deep_research.web_search_engines.rate_limiting.tracker import ( AdaptiveRateLimitTracker, ) tracker = AdaptiveRateLimitTracker(programmatic_mode=True) tracker._estimates_loaded = True tracker._ensure_estimates_loaded() mock_imports.assert_not_called() class TestTrackerSettings: """Tests for tracker settings and defaults.""" def test_memory_window_default(self): """Test default memory window value.""" from local_deep_research.config.thread_settings import ( NoSettingsContextError, ) with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.get_setting_from_snapshot" ) as mock_get_setting: mock_get_setting.side_effect = NoSettingsContextError("No settings") with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.logger" ): from local_deep_research.web_search_engines.rate_limiting.tracker import ( AdaptiveRateLimitTracker, ) tracker = AdaptiveRateLimitTracker(programmatic_mode=True) assert tracker.memory_window == 100 def test_exploration_rate_default(self): """Test default exploration rate.""" from local_deep_research.config.thread_settings import ( NoSettingsContextError, ) with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.get_setting_from_snapshot" ) as mock_get_setting: mock_get_setting.side_effect = NoSettingsContextError("No settings") with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.logger" ): from local_deep_research.web_search_engines.rate_limiting.tracker import ( AdaptiveRateLimitTracker, ) tracker = AdaptiveRateLimitTracker(programmatic_mode=True) assert tracker.exploration_rate == 0.1 def test_learning_rate_default(self): """Test default learning rate.""" from local_deep_research.config.thread_settings import ( NoSettingsContextError, ) with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.get_setting_from_snapshot" ) as mock_get_setting: mock_get_setting.side_effect = NoSettingsContextError("No settings") with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.logger" ): from local_deep_research.web_search_engines.rate_limiting.tracker import ( AdaptiveRateLimitTracker, ) tracker = AdaptiveRateLimitTracker(programmatic_mode=True) assert tracker.learning_rate == 0.3 def test_decay_per_day_default(self): """Test default decay per day.""" from local_deep_research.config.thread_settings import ( NoSettingsContextError, ) with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.get_setting_from_snapshot" ) as mock_get_setting: mock_get_setting.side_effect = NoSettingsContextError("No settings") with patch( "local_deep_research.web_search_engines.rate_limiting.tracker.logger" ): from local_deep_research.web_search_engines.rate_limiting.tracker import ( AdaptiveRateLimitTracker, ) tracker = AdaptiveRateLimitTracker(programmatic_mode=True) assert tracker.decay_per_day == 0.95