""" Tests for search_system.py - Link Deduplication and Settings Extraction Tests cover: - Link deduplication using object identity (id()) - Settings extraction from snapshot dictionaries These tests address issue #301: "too many links in detailed report mode" """ from unittest.mock import MagicMock, Mock, patch import pytest class TestLinkDeduplication: """Tests for link deduplication behavior.""" @pytest.fixture def mock_llm(self): """Create a mock LLM.""" mock = MagicMock() mock.invoke.return_value = MagicMock(content="test response") return mock @pytest.fixture def mock_search_engine(self): """Create a mock search engine.""" mock = MagicMock() mock.run.return_value = [] return mock def _create_system( self, mock_llm, mock_search_engine, mock_strategy, **kwargs ): """Helper to create an AdvancedSearchSystem with mocked dependencies.""" with patch( "local_deep_research.search_system_factory.create_strategy" ) as mock_create: mock_create.return_value = mock_strategy with patch( "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler" ) as mock_citation: mock_citation.return_value = Mock( _create_documents=Mock(), _format_sources=Mock() ) from local_deep_research.search_system import ( AdvancedSearchSystem, ) system = AdvancedSearchSystem( llm=mock_llm, search=mock_search_engine, strategy_name="standard", **kwargs, ) return system def test_same_list_object_not_duplicated( self, mock_llm, mock_search_engine ): """When lists are same object, don't extend.""" mock_strategy = MagicMock() shared_links = [{"title": "Link1", "url": "http://example.com"}] mock_strategy.all_links_of_system = shared_links mock_strategy.questions_by_iteration = [] mock_strategy.analyze_topic.return_value = { "current_knowledge": "test", "query": "test query", } with patch( "local_deep_research.search_system_factory.create_strategy" ) as mock_create: mock_create.return_value = mock_strategy with patch( "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler" ) as mock_citation: mock_citation.return_value = Mock( _create_documents=Mock(), _format_sources=Mock() ) from local_deep_research.search_system import ( AdvancedSearchSystem, ) system = AdvancedSearchSystem( llm=mock_llm, search=mock_search_engine, strategy_name="standard", ) # Make the system's list the SAME object as strategy's list system.all_links_of_system = shared_links # Perform search system.analyze_topic("test query") # Links should NOT be duplicated # Before the fix, this would double the list assert len(system.all_links_of_system) == 1 def test_different_list_objects_extended( self, mock_llm, mock_search_engine ): """Different objects get extended.""" mock_strategy = MagicMock() strategy_links = [{"title": "Link1", "url": "http://example.com"}] mock_strategy.all_links_of_system = strategy_links mock_strategy.questions_by_iteration = [] mock_strategy.analyze_topic.return_value = { "current_knowledge": "test", "query": "test query", } with patch( "local_deep_research.search_system_factory.create_strategy" ) as mock_create: mock_create.return_value = mock_strategy with patch( "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler" ) as mock_citation: mock_citation.return_value = Mock( _create_documents=Mock(), _format_sources=Mock() ) from local_deep_research.search_system import ( AdvancedSearchSystem, ) system = AdvancedSearchSystem( llm=mock_llm, search=mock_search_engine, strategy_name="standard", ) # System has a DIFFERENT list object system.all_links_of_system = [] system.analyze_topic("test query") # Links should be extended from strategy to system assert len(system.all_links_of_system) == 1 def test_empty_strategy_links(self, mock_llm, mock_search_engine): """Empty strategy links don't cause errors.""" mock_strategy = MagicMock() mock_strategy.all_links_of_system = [] mock_strategy.questions_by_iteration = [] mock_strategy.analyze_topic.return_value = { "current_knowledge": "test", "query": "test query", } with patch( "local_deep_research.search_system_factory.create_strategy" ) as mock_create: mock_create.return_value = mock_strategy with patch( "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler" ) as mock_citation: mock_citation.return_value = Mock( _create_documents=Mock(), _format_sources=Mock() ) from local_deep_research.search_system import ( AdvancedSearchSystem, ) system = AdvancedSearchSystem( llm=mock_llm, search=mock_search_engine, strategy_name="standard", ) initial_links = [ {"title": "Existing", "url": "http://existing.com"} ] system.all_links_of_system = initial_links system.analyze_topic("test query") # Existing links should remain, nothing added assert len(system.all_links_of_system) == 1 assert system.all_links_of_system[0]["title"] == "Existing" def test_large_link_list_performance(self, mock_llm, mock_search_engine): """1000+ links don't cause memory issues.""" mock_strategy = MagicMock() large_links = [ {"title": f"Link{i}", "url": f"http://example{i}.com"} for i in range(1000) ] mock_strategy.all_links_of_system = large_links mock_strategy.questions_by_iteration = [] mock_strategy.analyze_topic.return_value = { "current_knowledge": "test", "query": "test query", } with patch( "local_deep_research.search_system_factory.create_strategy" ) as mock_create: mock_create.return_value = mock_strategy with patch( "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler" ) as mock_citation: mock_citation.return_value = Mock( _create_documents=Mock(), _format_sources=Mock() ) from local_deep_research.search_system import ( AdvancedSearchSystem, ) system = AdvancedSearchSystem( llm=mock_llm, search=mock_search_engine, strategy_name="standard", ) system.all_links_of_system = [] system.analyze_topic("test query") assert len(system.all_links_of_system) == 1000 def test_link_dedup_preserves_order(self, mock_llm, mock_search_engine): """Link order preserved after dedup.""" mock_strategy = MagicMock() ordered_links = [ {"title": "First", "url": "http://first.com"}, {"title": "Second", "url": "http://second.com"}, {"title": "Third", "url": "http://third.com"}, ] mock_strategy.all_links_of_system = ordered_links mock_strategy.questions_by_iteration = [] mock_strategy.analyze_topic.return_value = { "current_knowledge": "test", "query": "test query", } with patch( "local_deep_research.search_system_factory.create_strategy" ) as mock_create: mock_create.return_value = mock_strategy with patch( "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler" ) as mock_citation: mock_citation.return_value = Mock( _create_documents=Mock(), _format_sources=Mock() ) from local_deep_research.search_system import ( AdvancedSearchSystem, ) system = AdvancedSearchSystem( llm=mock_llm, search=mock_search_engine, strategy_name="standard", ) system.all_links_of_system = [] system.analyze_topic("test query") assert system.all_links_of_system[0]["title"] == "First" assert system.all_links_of_system[1]["title"] == "Second" assert system.all_links_of_system[2]["title"] == "Third" class TestSettingsExtraction: """Tests for settings extraction from snapshot.""" @pytest.fixture def mock_llm(self): """Create a mock LLM.""" mock = MagicMock() mock.invoke.return_value = MagicMock(content="test response") return mock @pytest.fixture def mock_search_engine(self): """Create a mock search engine.""" mock = MagicMock() mock.run.return_value = [] return mock def test_settings_dict_value_format(self, mock_llm, mock_search_engine): """{'value': 'actual'} extracts correctly.""" settings = { "search.iterations": {"value": 5}, "search.questions_per_iteration": {"value": 4}, } with patch( "local_deep_research.search_system_factory.create_strategy" ) as mock_create: mock_strategy = MagicMock() mock_strategy.questions_by_iteration = [] mock_strategy.all_links_of_system = [] mock_create.return_value = mock_strategy with patch( "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler" ) as mock_citation: mock_citation.return_value = Mock( _create_documents=Mock(), _format_sources=Mock() ) from local_deep_research.search_system import ( AdvancedSearchSystem, ) system = AdvancedSearchSystem( llm=mock_llm, search=mock_search_engine, strategy_name="standard", settings_snapshot=settings, ) assert system.max_iterations == 5 assert system.questions_per_iteration == 4 def test_settings_direct_value_format(self, mock_llm, mock_search_engine): """Direct values work.""" settings = { "search.iterations": 3, "search.questions_per_iteration": 2, } with patch( "local_deep_research.search_system_factory.create_strategy" ) as mock_create: mock_strategy = MagicMock() mock_strategy.questions_by_iteration = [] mock_strategy.all_links_of_system = [] mock_create.return_value = mock_strategy with patch( "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler" ) as mock_citation: mock_citation.return_value = Mock( _create_documents=Mock(), _format_sources=Mock() ) from local_deep_research.search_system import ( AdvancedSearchSystem, ) system = AdvancedSearchSystem( llm=mock_llm, search=mock_search_engine, strategy_name="standard", settings_snapshot=settings, ) assert system.max_iterations == 3 assert system.questions_per_iteration == 2 def test_missing_settings_use_defaults(self, mock_llm, mock_search_engine): """Missing settings get defaults.""" settings = {} # Empty settings with patch( "local_deep_research.search_system_factory.create_strategy" ) as mock_create: mock_strategy = MagicMock() mock_strategy.questions_by_iteration = [] mock_strategy.all_links_of_system = [] mock_create.return_value = mock_strategy with patch( "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler" ) as mock_citation: mock_citation.return_value = Mock( _create_documents=Mock(), _format_sources=Mock() ) from local_deep_research.search_system import ( AdvancedSearchSystem, ) system = AdvancedSearchSystem( llm=mock_llm, search=mock_search_engine, strategy_name="standard", settings_snapshot=settings, ) # Defaults: iterations=1, questions_per_iteration=3 assert system.max_iterations == 1 assert system.questions_per_iteration == 3 def test_partial_settings_snapshot(self, mock_llm, mock_search_engine): """Some present, some missing.""" settings = { "search.iterations": {"value": 7}, # questions_per_iteration is missing } with patch( "local_deep_research.search_system_factory.create_strategy" ) as mock_create: mock_strategy = MagicMock() mock_strategy.questions_by_iteration = [] mock_strategy.all_links_of_system = [] mock_create.return_value = mock_strategy with patch( "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler" ) as mock_citation: mock_citation.return_value = Mock( _create_documents=Mock(), _format_sources=Mock() ) from local_deep_research.search_system import ( AdvancedSearchSystem, ) system = AdvancedSearchSystem( llm=mock_llm, search=mock_search_engine, strategy_name="standard", settings_snapshot=settings, ) assert system.max_iterations == 7 assert system.questions_per_iteration == 3 # Default def test_nested_settings_structure(self, mock_llm, mock_search_engine): """Deeply nested dicts.""" # The code only checks for {'value': ...} at one level settings = { "search.iterations": {"value": 2, "extra": {"nested": "data"}}, } with patch( "local_deep_research.search_system_factory.create_strategy" ) as mock_create: mock_strategy = MagicMock() mock_strategy.questions_by_iteration = [] mock_strategy.all_links_of_system = [] mock_create.return_value = mock_strategy with patch( "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler" ) as mock_citation: mock_citation.return_value = Mock( _create_documents=Mock(), _format_sources=Mock() ) from local_deep_research.search_system import ( AdvancedSearchSystem, ) system = AdvancedSearchSystem( llm=mock_llm, search=mock_search_engine, strategy_name="standard", settings_snapshot=settings, ) assert system.max_iterations == 2 def test_none_settings_snapshot(self, mock_llm, mock_search_engine): """None snapshot uses all defaults.""" with patch( "local_deep_research.search_system_factory.create_strategy" ) as mock_create: mock_strategy = MagicMock() mock_strategy.questions_by_iteration = [] mock_strategy.all_links_of_system = [] mock_create.return_value = mock_strategy with patch( "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler" ) as mock_citation: mock_citation.return_value = Mock( _create_documents=Mock(), _format_sources=Mock() ) from local_deep_research.search_system import ( AdvancedSearchSystem, ) system = AdvancedSearchSystem( llm=mock_llm, search=mock_search_engine, strategy_name="standard", settings_snapshot=None, ) assert system.max_iterations == 1 assert system.questions_per_iteration == 3 def test_empty_settings_snapshot(self, mock_llm, mock_search_engine): """Empty dict uses all defaults.""" with patch( "local_deep_research.search_system_factory.create_strategy" ) as mock_create: mock_strategy = MagicMock() mock_strategy.questions_by_iteration = [] mock_strategy.all_links_of_system = [] mock_create.return_value = mock_strategy with patch( "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler" ) as mock_citation: mock_citation.return_value = Mock( _create_documents=Mock(), _format_sources=Mock() ) from local_deep_research.search_system import ( AdvancedSearchSystem, ) system = AdvancedSearchSystem( llm=mock_llm, search=mock_search_engine, strategy_name="standard", settings_snapshot={}, ) assert system.max_iterations == 1 assert system.questions_per_iteration == 3 class TestProgressCallback: """Tests for progress callback functionality.""" @pytest.fixture def mock_llm(self): """Create a mock LLM.""" mock = MagicMock() return mock @pytest.fixture def mock_search_engine(self): """Create a mock search engine.""" return MagicMock() def test_progress_callback_set_on_strategy( self, mock_llm, mock_search_engine ): """Progress callback is set on strategy.""" with patch( "local_deep_research.search_system_factory.create_strategy" ) as mock_create: mock_strategy = MagicMock() mock_strategy.questions_by_iteration = [] mock_strategy.all_links_of_system = [] mock_create.return_value = mock_strategy with patch( "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler" ) as mock_citation: mock_citation.return_value = Mock( _create_documents=Mock(), _format_sources=Mock() ) from local_deep_research.search_system import ( AdvancedSearchSystem, ) system = AdvancedSearchSystem( llm=mock_llm, search=mock_search_engine, strategy_name="standard", ) callback = MagicMock() system.set_progress_callback(callback) mock_strategy.set_progress_callback.assert_called_with(callback) def test_progress_callback_receives_updates( self, mock_llm, mock_search_engine ): """Progress callback receives progress updates during search.""" with patch( "local_deep_research.search_system_factory.create_strategy" ) as mock_create: mock_strategy = MagicMock() mock_strategy.all_links_of_system = [] mock_strategy.questions_by_iteration = [] mock_strategy.analyze_topic.return_value = { "current_knowledge": "test", "query": "test query", } mock_create.return_value = mock_strategy with patch( "local_deep_research.citation_handlers.standard_citation_handler.StandardCitationHandler" ) as mock_citation: mock_citation.return_value = Mock( _create_documents=Mock(), _format_sources=Mock() ) from local_deep_research.search_system import ( AdvancedSearchSystem, ) system = AdvancedSearchSystem( llm=mock_llm, search=mock_search_engine, strategy_name="standard", settings_snapshot={ "llm.provider": {"value": "test_provider"}, "llm.model": {"value": "test_model"}, "search.tool": {"value": "test_tool"}, }, ) callback = MagicMock() system.set_progress_callback(callback) system.analyze_topic("test query") # Callback should have been called during search assert callback.called