"""Extended tests for search_system.py - covering settings processing, programmatic mode, strategy selection, and result processing.""" from unittest.mock import Mock, patch def _make_strategy_mock(): """Create a mock strategy with required attributes.""" mock_strategy = Mock() mock_strategy.questions_by_iteration = [] mock_strategy.all_links_of_system = [] return mock_strategy def _patch_system(): """Return nested context managers for search system patching. Usage: with _create_patched(): ... Returns a helper that patches both create_strategy and StandardCitationHandler. """ class PatchContext: def __init__(self): self.strategy = _make_strategy_mock() self._p1 = patch( "local_deep_research.search_system_factory.create_strategy", return_value=self.strategy, ) self._p2 = patch( "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler", return_value=Mock( _create_documents=Mock(), _format_sources=Mock() ), ) def __enter__(self): self.mock_create = self._p1.__enter__() self.mock_citation = self._p2.__enter__() return self def __exit__(self, *args): self._p2.__exit__(*args) self._p1.__exit__(*args) return PatchContext() class TestAdvancedSearchSystemSettingsProcessing: """Tests for settings snapshot processing in AdvancedSearchSystem.__init__.""" def test_max_iterations_from_dict_settings(self): """max_iterations should be extracted from dict-wrapped settings.""" settings = { "search.iterations": {"value": 5, "ui_element": "number"}, "search.questions_per_iteration": { "value": 4, "ui_element": "number", }, } with _patch_system(): from local_deep_research.search_system import AdvancedSearchSystem system = AdvancedSearchSystem( llm=Mock(), search=Mock(), settings_snapshot=settings ) assert system.max_iterations == 5 assert system.questions_per_iteration == 4 def test_max_iterations_from_direct_value_settings(self): """max_iterations should work with direct (non-dict) settings values.""" settings = { "search.iterations": 3, "search.questions_per_iteration": 2, } with _patch_system(): from local_deep_research.search_system import AdvancedSearchSystem system = AdvancedSearchSystem( llm=Mock(), search=Mock(), settings_snapshot=settings ) assert system.max_iterations == 3 assert system.questions_per_iteration == 2 def test_max_iterations_defaults_when_missing(self): """max_iterations should default to 1 when not in settings.""" with _patch_system(): from local_deep_research.search_system import AdvancedSearchSystem system = AdvancedSearchSystem( llm=Mock(), search=Mock(), settings_snapshot={} ) assert system.max_iterations == 1 assert system.questions_per_iteration == 3 def test_explicit_max_iterations_overrides_settings(self): """Explicit max_iterations parameter should override settings snapshot.""" settings = {"search.iterations": {"value": 10}} with _patch_system(): from local_deep_research.search_system import AdvancedSearchSystem system = AdvancedSearchSystem( llm=Mock(), search=Mock(), max_iterations=7, settings_snapshot=settings, ) assert system.max_iterations == 7 class TestAdvancedSearchSystemProgrammaticMode: """Tests for programmatic mode behavior.""" def test_programmatic_mode_sets_flag(self): """programmatic_mode flag should be stored.""" with _patch_system(): from local_deep_research.search_system import AdvancedSearchSystem system = AdvancedSearchSystem( llm=Mock(), search=Mock(), programmatic_mode=True ) assert system.programmatic_mode is True def test_non_programmatic_mode_default(self): """programmatic_mode should default to False.""" with _patch_system(): from local_deep_research.search_system import AdvancedSearchSystem system = AdvancedSearchSystem(llm=Mock(), search=Mock()) assert system.programmatic_mode is False class TestAdvancedSearchSystemResearchContext: """Tests for research context handling.""" def test_stores_research_context(self): """research_context should be stored on the system.""" context = {"delegate_strategy": "rapid", "some_key": "some_value"} with _patch_system(): from local_deep_research.search_system import AdvancedSearchSystem system = AdvancedSearchSystem( llm=Mock(), search=Mock(), research_context=context ) assert system.research_context == context def test_none_research_context(self): """None research_context should not cause errors.""" with _patch_system(): from local_deep_research.search_system import AdvancedSearchSystem system = AdvancedSearchSystem( llm=Mock(), search=Mock(), research_context=None ) assert system.research_context is None def test_research_id_stored(self): """research_id should be stored on the system.""" with _patch_system(): from local_deep_research.search_system import AdvancedSearchSystem system = AdvancedSearchSystem( llm=Mock(), search=Mock(), research_id="test-123" ) assert system.research_id == "test-123" class TestAdvancedSearchSystemContextualFollowUp: """Tests for contextual follow-up strategy initialization.""" def test_contextual_followup_uses_delegate(self): """Contextual follow-up strategy should create a delegate strategy.""" context = {"delegate_strategy": "rapid"} with _patch_system() as ctx: with patch( "local_deep_research.search_system.EnhancedContextualFollowUpStrategy" ) as mock_followup_cls: mock_followup_cls.return_value = Mock() from local_deep_research.search_system import ( AdvancedSearchSystem, ) AdvancedSearchSystem( llm=Mock(), search=Mock(), strategy_name="enhanced-contextual-followup", research_context=context, ) # Verify delegate strategy was created with rapid ctx.mock_create.assert_called() call_kwargs = ctx.mock_create.call_args assert call_kwargs.kwargs.get("strategy_name") == "rapid" or ( call_kwargs[1].get("strategy_name") == "rapid" ) def test_contextual_followup_defaults_delegate_to_source_based(self): """Contextual follow-up without delegate_strategy should default to source-based.""" with _patch_system() as ctx: with patch( "local_deep_research.search_system.EnhancedContextualFollowUpStrategy" ) as mock_followup_cls: mock_followup_cls.return_value = Mock() from local_deep_research.search_system import ( AdvancedSearchSystem, ) AdvancedSearchSystem( llm=Mock(), search=Mock(), strategy_name="contextual-followup", research_context={}, ) call_kwargs = ctx.mock_create.call_args strategy_name = call_kwargs.kwargs.get( "strategy_name" ) or call_kwargs[1].get("strategy_name") assert strategy_name == "source-based" def test_contextual_followup_name_variants(self): """All contextual follow-up name variants should be recognized.""" variants = [ "enhanced-contextual-followup", "enhanced_contextual_followup", "contextual-followup", "contextual_followup", ] for variant in variants: with _patch_system(): with patch( "local_deep_research.search_system.EnhancedContextualFollowUpStrategy" ) as mock_followup_cls: mock_followup_cls.return_value = Mock() from local_deep_research.search_system import ( AdvancedSearchSystem, ) AdvancedSearchSystem( llm=Mock(), search=Mock(), strategy_name=variant, ) # Should use EnhancedContextualFollowUpStrategy mock_followup_cls.assert_called_once() class TestAdvancedSearchSystemSearchOriginalQuery: """Tests for search_original_query parameter.""" def test_search_original_query_default_true(self): """search_original_query should default to True.""" with _patch_system(): from local_deep_research.search_system import AdvancedSearchSystem system = AdvancedSearchSystem(llm=Mock(), search=Mock()) assert system.search_original_query is True def test_search_original_query_can_be_disabled(self): """search_original_query can be set to False for news searches.""" with _patch_system(): from local_deep_research.search_system import AdvancedSearchSystem system = AdvancedSearchSystem( llm=Mock(), search=Mock(), search_original_query=False ) assert system.search_original_query is False class TestAdvancedSearchSystemPerformSearch: """Tests for _perform_search result processing.""" def test_result_includes_query(self): """Result should include the query if not returned by strategy.""" with _patch_system() as ctx: ctx.strategy.analyze_topic.return_value = { "current_knowledge": "some results" } ctx.strategy.questions_by_iteration = {1: ["q1"]} from local_deep_research.search_system import AdvancedSearchSystem system = AdvancedSearchSystem(llm=Mock(), search=Mock()) with patch( "local_deep_research.news.core.search_integration.NewsSearchCallback" ): result = system.analyze_topic("test query") assert result["query"] == "test query" def test_result_includes_search_system_reference(self): """Result should include reference to the search system.""" with _patch_system() as ctx: ctx.strategy.analyze_topic.return_value = { "current_knowledge": "results" } ctx.strategy.questions_by_iteration = {} from local_deep_research.search_system import AdvancedSearchSystem system = AdvancedSearchSystem(llm=Mock(), search=Mock()) with patch( "local_deep_research.news.core.search_integration.NewsSearchCallback" ): result = system.analyze_topic("test query") assert result["search_system"] is system def test_settings_snapshot_extraction_in_perform_search(self): """_perform_search should extract LLM/search info from settings snapshot.""" settings = { "llm.provider": {"value": "openai"}, "llm.model": {"value": "gpt-4"}, "search.tool": {"value": "searxng"}, } progress_calls = [] def capture_progress(msg, progress, metadata): progress_calls.append((msg, progress, metadata)) with _patch_system() as ctx: ctx.strategy.analyze_topic.return_value = {"current_knowledge": "r"} ctx.strategy.questions_by_iteration = {} from local_deep_research.search_system import AdvancedSearchSystem system = AdvancedSearchSystem( llm=Mock(), search=Mock(), settings_snapshot=settings ) system.set_progress_callback(capture_progress) with patch( "local_deep_research.news.core.search_integration.NewsSearchCallback" ): system.analyze_topic("test") # Should have progress callbacks with LLM and search info assert len(progress_calls) >= 2 assert "openai" in progress_calls[0][0] assert "gpt-4" in progress_calls[0][0] def test_links_deduplication(self): """all_links_of_system should not be duplicated when same object.""" shared_links = [{"title": "link1", "url": "http://example.com"}] with _patch_system() as ctx: ctx.strategy.analyze_topic.return_value = {"current_knowledge": "r"} ctx.strategy.questions_by_iteration = {} ctx.strategy.all_links_of_system = shared_links from local_deep_research.search_system import AdvancedSearchSystem system = AdvancedSearchSystem(llm=Mock(), search=Mock()) system.all_links_of_system = shared_links with patch( "local_deep_research.news.core.search_integration.NewsSearchCallback" ): system.analyze_topic("test") # Should NOT have duplicated the links assert len(system.all_links_of_system) == 1 class TestAdvancedSearchSystemProgressCallback: """Tests for progress callback mechanism.""" def test_set_progress_callback_propagates_to_strategy(self): """set_progress_callback should propagate to the strategy.""" with _patch_system() as ctx: from local_deep_research.search_system import AdvancedSearchSystem system = AdvancedSearchSystem(llm=Mock(), search=Mock()) def noop_callback(msg, pct, meta): return None system.set_progress_callback(noop_callback) ctx.strategy.set_progress_callback.assert_called_with(noop_callback) def test_progress_callback_before_strategy_exists(self): """Setting callback before strategy is created should not crash.""" with _patch_system(): from local_deep_research.search_system import AdvancedSearchSystem system = AdvancedSearchSystem(llm=Mock(), search=Mock()) # Default callback should be a no-op lambda system.progress_callback("test", 50, {})