"""Tests for API research functions.""" from unittest.mock import MagicMock, patch from local_deep_research.api.research_functions import ( _init_search_system, quick_summary, generate_report, detailed_research, analyze_documents, ) class TestInitSearchSystem: """Tests for _init_search_system function.""" def test_returns_search_system(self, mock_get_llm, mock_get_search): """Test that function returns an AdvancedSearchSystem.""" with patch( "local_deep_research.api.research_functions.AdvancedSearchSystem" ) as mock_class: mock_system = MagicMock() mock_class.return_value = mock_system result = _init_search_system() assert result == mock_system mock_class.assert_called_once() def test_passes_custom_temperature(self, mock_get_llm, mock_get_search): """Test that custom temperature is passed to get_llm.""" with ( patch( "local_deep_research.api.research_functions.AdvancedSearchSystem" ), patch( "local_deep_research.api.research_functions.get_llm" ) as mock_llm, ): _init_search_system(temperature=0.5) mock_llm.assert_called_once() assert mock_llm.call_args[1]["temperature"] == 0.5 def test_passes_model_name(self, mock_get_llm, mock_get_search): """Test that model_name is passed to get_llm.""" with ( patch( "local_deep_research.api.research_functions.AdvancedSearchSystem" ), patch( "local_deep_research.api.research_functions.get_llm" ) as mock_llm, ): _init_search_system(model_name="gpt-4") assert mock_llm.call_args[1]["model_name"] == "gpt-4" def test_passes_provider(self, mock_get_llm, mock_get_search): """Test that provider is passed to get_llm.""" with ( patch( "local_deep_research.api.research_functions.AdvancedSearchSystem" ), patch( "local_deep_research.api.research_functions.get_llm" ) as mock_llm, ): _init_search_system(provider="openai") assert mock_llm.call_args[1]["provider"] == "openai" def test_creates_search_engine(self, mock_get_llm, mock_get_search): """Test that search engine is created when search_tool specified.""" with ( patch( "local_deep_research.api.research_functions.AdvancedSearchSystem" ), patch( "local_deep_research.api.research_functions.get_search" ) as mock_search, ): _init_search_system(search_tool="wikipedia") mock_search.assert_called_once() assert mock_search.call_args[0][0] == "wikipedia" def test_sets_iterations(self, mock_get_llm, mock_get_search): """Test that max_iterations is set on system.""" with patch( "local_deep_research.api.research_functions.AdvancedSearchSystem" ) as mock_class: mock_system = MagicMock() mock_class.return_value = mock_system _init_search_system(iterations=5) assert mock_system.max_iterations == 5 def test_sets_questions_per_iteration(self, mock_get_llm, mock_get_search): """Test that questions_per_iteration is set on system.""" with patch( "local_deep_research.api.research_functions.AdvancedSearchSystem" ) as mock_class: mock_system = MagicMock() mock_class.return_value = mock_system _init_search_system(questions_per_iteration=10) assert mock_system.questions_per_iteration == 10 def test_sets_progress_callback(self, mock_get_llm, mock_get_search): """Test that progress callback is set.""" callback = MagicMock() with patch( "local_deep_research.api.research_functions.AdvancedSearchSystem" ) as mock_class: mock_system = MagicMock() mock_class.return_value = mock_system _init_search_system(progress_callback=callback) mock_system.set_progress_callback.assert_called_once_with(callback) def test_registers_retrievers(self, mock_get_llm, mock_get_search): """Test that custom retrievers are registered.""" retriever = MagicMock() with ( patch( "local_deep_research.api.research_functions.AdvancedSearchSystem" ), patch( "local_deep_research.web_search_engines.retriever_registry.retriever_registry" ) as mock_registry, ): _init_search_system(retrievers={"custom": retriever}) mock_registry.register_multiple.assert_called_once_with( {"custom": retriever} ) def test_registers_llms(self, mock_get_llm, mock_get_search): """Test that custom LLMs are registered.""" custom_llm = MagicMock() with ( patch( "local_deep_research.api.research_functions.AdvancedSearchSystem" ), patch("local_deep_research.llm.register_llm") as mock_register, ): _init_search_system(llms={"custom_llm": custom_llm}) mock_register.assert_called_once_with("custom_llm", custom_llm) def test_uses_settings_snapshot( self, mock_get_llm, mock_get_search, sample_settings_snapshot ): """Test that settings_snapshot is passed through.""" with patch( "local_deep_research.api.research_functions.AdvancedSearchSystem" ) as mock_class: _init_search_system(settings_snapshot=sample_settings_snapshot) call_kwargs = mock_class.call_args[1] assert call_kwargs["settings_snapshot"] == sample_settings_snapshot class TestQuickSummary: """Tests for quick_summary function.""" def test_returns_dict(self, mock_get_llm, mock_advanced_search_system): """Test that function returns a dictionary.""" with patch( "local_deep_research.api.research_functions.create_settings_snapshot", return_value={}, ): result = quick_summary("test query") assert isinstance(result, dict) def test_returns_summary_key( self, mock_get_llm, mock_advanced_search_system ): """Test that result contains 'summary' key.""" with patch( "local_deep_research.api.research_functions.create_settings_snapshot", return_value={}, ): result = quick_summary("test query") assert "summary" in result def test_returns_findings_key( self, mock_get_llm, mock_advanced_search_system ): """Test that result contains 'findings' key.""" with patch( "local_deep_research.api.research_functions.create_settings_snapshot", return_value={}, ): result = quick_summary("test query") assert "findings" in result def test_returns_iterations_key( self, mock_get_llm, mock_advanced_search_system ): """Test that result contains 'iterations' key.""" with patch( "local_deep_research.api.research_functions.create_settings_snapshot", return_value={}, ): result = quick_summary("test query") assert "iterations" in result def test_returns_sources_key( self, mock_get_llm, mock_advanced_search_system ): """Test that result contains 'sources' key.""" with patch( "local_deep_research.api.research_functions.create_settings_snapshot", return_value={}, ): result = quick_summary("test query") assert "sources" in result def test_generates_research_id_if_none( self, mock_get_llm, mock_advanced_search_system ): """Test that research_id is generated if not provided.""" with ( patch( "local_deep_research.api.research_functions.create_settings_snapshot", return_value={}, ), patch( "local_deep_research.api.research_functions.set_search_context" ) as mock_set_context, ): quick_summary("test query") call_args = mock_set_context.call_args[0][0] assert "research_id" in call_args # Should be a valid UUID assert len(call_args["research_id"]) == 36 def test_uses_provided_research_id( self, mock_get_llm, mock_advanced_search_system ): """Test that provided research_id is used.""" with ( patch( "local_deep_research.api.research_functions.create_settings_snapshot", return_value={}, ), patch( "local_deep_research.api.research_functions.set_search_context" ) as mock_set_context, ): quick_summary("test query", research_id="custom-id") call_args = mock_set_context.call_args[0][0] assert call_args["research_id"] == "custom-id" def test_registers_custom_retrievers( self, mock_get_llm, mock_advanced_search_system ): """Test that custom retrievers are registered.""" retriever = MagicMock() with ( patch( "local_deep_research.api.research_functions.create_settings_snapshot", return_value={}, ), patch( "local_deep_research.web_search_engines.retriever_registry.retriever_registry" ) as mock_registry, ): quick_summary("test query", retrievers={"custom": retriever}) mock_registry.register_multiple.assert_called() def test_calls_analyze_topic( self, mock_get_llm, mock_advanced_search_system ): """Test that analyze_topic is called with query.""" with patch( "local_deep_research.api.research_functions.create_settings_snapshot", return_value={}, ): quick_summary("test query") mock_advanced_search_system.analyze_topic.assert_called_once_with( "test query" ) def test_handles_empty_results(self, mock_get_llm): """Test handling when analyze_topic returns empty results.""" mock_system = MagicMock() mock_system.analyze_topic.return_value = {} with ( patch( "local_deep_research.api.research_functions.AdvancedSearchSystem", return_value=mock_system, ), patch( "local_deep_research.api.research_functions.create_settings_snapshot", return_value={}, ), ): result = quick_summary("test query") assert ( result["summary"] == "Unable to generate summary for the query." ) def test_creates_settings_snapshot_when_not_provided( self, mock_get_llm, mock_advanced_search_system ): """Test that settings snapshot is created if not provided.""" with patch( "local_deep_research.api.research_functions.create_settings_snapshot", return_value={}, ) as mock_create: quick_summary("test query", provider="openai", temperature=0.5) mock_create.assert_called_once() def test_uses_provided_settings_snapshot( self, mock_get_llm, mock_advanced_search_system ): """Test that provided settings_snapshot is used.""" # Create a valid settings snapshot with required keys custom_snapshot = { # "library" is always available without extra engine config # (searxng needs an instance URL before the factory accepts it). "search.tool": {"value": "library"}, "custom": "settings", } with patch( "local_deep_research.api.research_functions.create_settings_snapshot", return_value=custom_snapshot, ) as mock_create: quick_summary("test query", settings_snapshot=custom_snapshot) # Should not call create_settings_snapshot when snapshot is provided mock_create.assert_not_called() class TestGenerateReport: """Tests for generate_report function.""" def test_returns_dict( self, mock_get_llm, mock_advanced_search_system, mock_report_generator ): """Test that function returns a dictionary.""" with ( patch( "local_deep_research.api.research_functions.IntegratedReportGenerator", return_value=mock_report_generator, ), patch( "local_deep_research.api.research_functions.create_settings_snapshot", return_value={}, ), ): result = generate_report("test query") assert isinstance(result, dict) def test_returns_content_key( self, mock_get_llm, mock_advanced_search_system, mock_report_generator ): """Test that result contains 'content' key.""" with ( patch( "local_deep_research.api.research_functions.IntegratedReportGenerator", return_value=mock_report_generator, ), patch( "local_deep_research.api.research_functions.create_settings_snapshot", return_value={}, ), ): result = generate_report("test query") assert "content" in result def test_calls_analyze_topic( self, mock_get_llm, mock_advanced_search_system, mock_report_generator ): """Test that analyze_topic is called.""" with ( patch( "local_deep_research.api.research_functions.IntegratedReportGenerator", return_value=mock_report_generator, ), patch( "local_deep_research.api.research_functions.create_settings_snapshot", return_value={}, ), ): generate_report("test query") mock_advanced_search_system.analyze_topic.assert_called_once_with( "test query" ) def test_calls_report_generator( self, mock_get_llm, mock_advanced_search_system, mock_report_generator ): """Test that report generator is called.""" with ( patch( "local_deep_research.api.research_functions.IntegratedReportGenerator", return_value=mock_report_generator, ), patch( "local_deep_research.api.research_functions.create_settings_snapshot", return_value={}, ), ): generate_report("test query") mock_report_generator.generate_report.assert_called_once() def test_saves_to_file( self, mock_get_llm, mock_advanced_search_system, mock_report_generator, tmp_path, ): """Test saving report to file.""" output_file = str(tmp_path / "report.md") with ( patch( "local_deep_research.api.research_functions.IntegratedReportGenerator", return_value=mock_report_generator, ), patch( "local_deep_research.api.research_functions.create_settings_snapshot", return_value={}, ), patch( "local_deep_research.security.file_write_verifier.write_file_verified" ) as mock_write, ): result = generate_report("test query", output_file=output_file) mock_write.assert_called_once() assert result["file_path"] == output_file def test_sets_progress_callback( self, mock_get_llm, mock_advanced_search_system, mock_report_generator ): """Test that progress callback is set.""" callback = MagicMock() with ( patch( "local_deep_research.api.research_functions.IntegratedReportGenerator", return_value=mock_report_generator, ), patch( "local_deep_research.api.research_functions.create_settings_snapshot", return_value={}, ), ): generate_report("test query", progress_callback=callback) mock_advanced_search_system.set_progress_callback.assert_called_once_with( callback ) def test_searches_per_section( self, mock_get_llm, mock_advanced_search_system, mock_report_generator ): """Test that searches_per_section is passed to generator.""" with ( patch( "local_deep_research.api.research_functions.IntegratedReportGenerator" ) as mock_gen_class, patch( "local_deep_research.api.research_functions.create_settings_snapshot", return_value={}, ), ): mock_gen_class.return_value = mock_report_generator generate_report("test query", searches_per_section=5) call_kwargs = mock_gen_class.call_args[1] assert call_kwargs["searches_per_section"] == 5 class TestDetailedResearch: """Tests for detailed_research function.""" def test_returns_dict(self, mock_get_llm, mock_advanced_search_system): """Test that function returns a dictionary.""" result = detailed_research("test query") assert isinstance(result, dict) def test_returns_query(self, mock_get_llm, mock_advanced_search_system): """Test that result contains 'query' key.""" result = detailed_research("test query") assert result["query"] == "test query" def test_returns_research_id( self, mock_get_llm, mock_advanced_search_system ): """Test that result contains 'research_id' key.""" result = detailed_research("test query") assert "research_id" in result def test_uses_provided_research_id( self, mock_get_llm, mock_advanced_search_system ): """Test that provided research_id is used.""" result = detailed_research("test query", research_id="custom-id") assert result["research_id"] == "custom-id" def test_generates_research_id( self, mock_get_llm, mock_advanced_search_system ): """Test that research_id is generated if not provided.""" result = detailed_research("test query") # Should be a valid UUID assert len(result["research_id"]) == 36 def test_returns_summary(self, mock_get_llm, mock_advanced_search_system): """Test that result contains 'summary' key.""" result = detailed_research("test query") assert "summary" in result def test_returns_findings(self, mock_get_llm, mock_advanced_search_system): """Test that result contains 'findings' key.""" result = detailed_research("test query") assert "findings" in result def test_returns_metadata(self, mock_get_llm, mock_advanced_search_system): """Test that result contains 'metadata' key.""" result = detailed_research("test query") assert "metadata" in result assert "timestamp" in result["metadata"] def test_calls_analyze_topic( self, mock_get_llm, mock_advanced_search_system ): """Test that analyze_topic is called with query.""" detailed_research("test query") mock_advanced_search_system.analyze_topic.assert_called_once_with( "test query" ) class TestAnalyzeDocuments: """Tests for analyze_documents function.""" def test_returns_dict(self, mock_get_llm, mock_get_search): """Test that function returns a dictionary.""" result = analyze_documents("test query", "test_collection") assert isinstance(result, dict) def test_returns_summary(self, mock_get_llm, mock_get_search): """Test that result contains 'summary' key.""" result = analyze_documents("test query", "test_collection") assert "summary" in result def test_returns_documents(self, mock_get_llm, mock_get_search): """Test that result contains 'documents' key.""" result = analyze_documents("test query", "test_collection") assert "documents" in result def test_collection_not_found(self, mock_get_llm): """Test handling when collection is not found.""" with patch( "local_deep_research.api.research_functions.get_search", return_value=None, ): result = analyze_documents("test query", "nonexistent_collection") assert "Error" in result["summary"] assert result["documents"] == [] def test_no_results_found(self, mock_get_llm): """Test handling when no documents are found.""" mock_search = MagicMock() mock_search.run.return_value = [] with patch( "local_deep_research.api.research_functions.get_search", return_value=mock_search, ): result = analyze_documents("test query", "test_collection") assert "No documents found" in result["summary"] assert result["documents"] == [] def test_sets_max_results(self, mock_get_llm): """Test that max_results is set on search engine.""" mock_search = MagicMock() mock_search.run.return_value = [] with patch( "local_deep_research.api.research_functions.get_search", return_value=mock_search, ): analyze_documents("test query", "test_collection", max_results=50) assert mock_search.max_results == 50 def test_uses_custom_temperature(self, mock_get_search): """Test that custom temperature is passed to get_llm.""" with patch( "local_deep_research.api.research_functions.get_llm" ) as mock_llm: # Create a mock LLM that returns a proper response mock_llm_instance = MagicMock() mock_llm_instance.invoke.return_value = MagicMock( content="Summary text" ) mock_llm.return_value = mock_llm_instance analyze_documents("test query", "test_collection", temperature=0.3) mock_llm.assert_called_once() assert mock_llm.call_args[1]["temperature"] == 0.3 def test_saves_to_file(self, mock_get_llm, tmp_path): """Test saving analysis to file.""" output_file = str(tmp_path / "analysis.md") mock_search = MagicMock() mock_search.run.return_value = [ { "title": "Doc 1", "link": "https://example.com", "content": "Content", } ] with ( patch( "local_deep_research.api.research_functions.get_search", return_value=mock_search, ), patch( "local_deep_research.security.file_write_verifier.write_file_verified" ) as mock_write, ): result = analyze_documents( "test query", "test_collection", output_file=output_file ) mock_write.assert_called_once() assert result["file_path"] == output_file def test_returns_collection_name(self, mock_get_llm, mock_get_search): """Test that result includes collection name.""" result = analyze_documents("test query", "my_collection") assert result["collection"] == "my_collection" def test_returns_document_count(self, mock_get_llm, mock_get_search): """Test that result includes document count.""" result = analyze_documents("test query", "test_collection") assert "document_count" in result