""" Additional tests for MCP server to increase coverage. Tests for: - Falsy value handling in _build_settings_overrides - Discovery tools error paths - Entry point functions - Temperature parameter in research tools - Import smoke tests - formatted_findings field """ from unittest.mock import patch import pytest # Skip all tests if MCP is not available try: import mcp # noqa: F401 MCP_AVAILABLE = True except ImportError: MCP_AVAILABLE = False pytestmark = pytest.mark.skipif( not MCP_AVAILABLE, reason="MCP package not installed" ) class TestBuildSettingsOverridesFalsyValues: """Tests for falsy value handling in _build_settings_overrides. Note: The implementation uses 'is not None' checks, so falsy values like 0, 0.0, and "" ARE included in the result (they are valid values). """ def test_iterations_zero_is_added(self): """Test that iterations=0 IS added (is not None check).""" from local_deep_research.mcp.server import _build_settings_overrides # Implementation uses 'is not None', so 0 is added result = _build_settings_overrides(iterations=0) assert result.get("search.iterations") == 0 def test_questions_per_iteration_zero_is_added(self): """Test that questions_per_iteration=0 IS added.""" from local_deep_research.mcp.server import _build_settings_overrides result = _build_settings_overrides(questions_per_iteration=0) assert result.get("search.questions_per_iteration") == 0 def test_temperature_zero_is_added(self): """Test that temperature=0.0 IS added (is not None check).""" from local_deep_research.mcp.server import _build_settings_overrides result = _build_settings_overrides(temperature=0.0) assert result.get("llm.temperature") == 0.0 def test_empty_string_search_engine_is_stripped(self): """Test that empty string search_engine is stripped and not added.""" from local_deep_research.mcp.server import _build_settings_overrides result = _build_settings_overrides(search_engine="") assert result == {} def test_empty_string_strategy_is_stripped(self): """Test that empty string strategy is stripped and not added.""" from local_deep_research.mcp.server import _build_settings_overrides result = _build_settings_overrides(strategy="") assert result == {} def test_positive_temperature_added(self): """Test that positive temperature is correctly added.""" from local_deep_research.mcp.server import _build_settings_overrides result = _build_settings_overrides(temperature=0.5) assert result["llm.temperature"] == 0.5 def test_temperature_one_added(self): """Test that temperature=1.0 is correctly added.""" from local_deep_research.mcp.server import _build_settings_overrides result = _build_settings_overrides(temperature=1.0) assert result["llm.temperature"] == 1.0 class TestDiscoveryToolsErrorPaths: """Tests for error handling in discovery tools.""" def test_list_search_engines_handles_import_error(self): """Test list_search_engines handles import errors gracefully.""" from local_deep_research.mcp.server import list_search_engines # The function imports create_settings_snapshot inside, so we need to patch it there with patch( "local_deep_research.api.settings_utils.create_settings_snapshot", side_effect=ImportError("Module not found"), ): result = list_search_engines() assert result["status"] == "error" assert result["error_type"] == "model_not_found" def test_list_search_engines_handles_config_error(self): """Test list_search_engines handles config errors gracefully.""" from local_deep_research.mcp.server import list_search_engines with patch( "local_deep_research.web_search_engines.search_engines_config.search_config", side_effect=Exception("Config load failed"), ): result = list_search_engines() assert result["status"] == "error" assert result["error_type"] == "unknown" def test_get_configuration_handles_settings_error(self): """Test get_configuration handles settings errors gracefully.""" from local_deep_research.mcp.server import get_configuration # Patch at the location where it's imported inside the function with patch( "local_deep_research.api.settings_utils.create_settings_snapshot", side_effect=Exception("Settings unavailable"), ): result = get_configuration() assert result["status"] == "error" assert result["error_type"] == "service_unavailable" def test_get_configuration_handles_extract_error(self): """Test get_configuration handles extraction errors gracefully.""" from local_deep_research.mcp.server import get_configuration # Patch at the location where it's imported inside the function with patch( "local_deep_research.api.settings_utils.extract_setting_value", side_effect=Exception("Extract failed"), ): result = get_configuration() assert result["status"] == "error" assert result["error_type"] == "unknown" class TestEntryPoints: """Tests for entry point functions.""" def test_run_server_function_is_callable(self): """Test that run_server is a callable function.""" from local_deep_research.mcp.server import run_server assert callable(run_server) def test_run_server_exported_from_package(self): """Test that run_server is exported from the mcp package.""" from local_deep_research.mcp import run_server assert callable(run_server) def test_mcp_instance_exported_from_package(self): """Test that mcp instance is exported from the mcp package.""" from local_deep_research.mcp import mcp assert mcp is not None assert mcp.name == "local-deep-research" def test_main_module_imports_run_server(self): """Test that __main__.py can import run_server.""" # This tests that the import in __main__.py works from local_deep_research.mcp.__main__ import run_server assert callable(run_server) def test_run_server_disables_loguru_diagnose(self): """The MCP stderr sink must pin diagnose=False (issue #4185). loguru defaults diagnose=True, which dumps repr() of every traceback frame's locals on exception. The MCP server calls logger.exception() in many request handlers whose frame locals hold credentials (api_key, Authorization headers, search-engine secrets); leaving the default on would write them to the MCP client's stderr log. """ import local_deep_research.mcp.server as server_module with patch.object(server_module, "logger") as mock_logger: with patch.object(server_module.mcp, "run"): server_module.run_server() assert mock_logger.add.call_args_list, "stderr sink was not added" for add_call in mock_logger.add.call_args_list: assert add_call.kwargs.get("diagnose") is False, ( f"MCP logger.add must pass diagnose=False; got " f"{add_call.kwargs.get('diagnose')!r}" ) class TestTemperatureParameter: """Tests for temperature parameter in research tools.""" def test_quick_research_with_temperature(self): """Test quick_research passes temperature override.""" from local_deep_research.mcp.server import quick_research with patch( "local_deep_research.mcp.server.ldr_quick_summary", return_value={ "summary": "Test", "findings": [], "sources": [], "iterations": 1, }, ): with patch( "local_deep_research.mcp.server.create_settings_snapshot" ) as mock_settings: mock_settings.return_value = {} # Note: quick_research doesn't have temperature param currently # This test documents the current behavior result = quick_research(query="test", iterations=2) assert result["status"] == "success" def test_detailed_research_with_temperature(self): """Test detailed_research passes temperature override.""" from local_deep_research.mcp.server import detailed_research with patch( "local_deep_research.mcp.server.ldr_detailed_research", return_value={ "query": "test", "research_id": "123", "summary": "Test", "findings": [], "sources": [], "iterations": 1, }, ): with patch( "local_deep_research.mcp.server.create_settings_snapshot" ) as mock_settings: mock_settings.return_value = {} result = detailed_research(query="test", iterations=2) assert result["status"] == "success" class TestFormattedFindings: """Tests for formatted_findings field in research results.""" def test_quick_research_returns_formatted_findings(self): """Test quick_research returns formatted_findings field.""" from local_deep_research.mcp.server import quick_research with patch( "local_deep_research.mcp.server.ldr_quick_summary", return_value={ "summary": "Test summary", "findings": [], "sources": [], "iterations": 1, "formatted_findings": "## Formatted Content\n\nTest formatted findings", }, ): result = quick_research(query="test") assert result["status"] == "success" assert "formatted_findings" in result assert ( result["formatted_findings"] == "## Formatted Content\n\nTest formatted findings" ) def test_quick_research_formatted_findings_defaults_empty(self): """Test formatted_findings defaults to empty string when missing.""" from local_deep_research.mcp.server import quick_research with patch( "local_deep_research.mcp.server.ldr_quick_summary", return_value={ "summary": "Test", "findings": [], "sources": [], "iterations": 1, # No formatted_findings }, ): result = quick_research(query="test") assert result["status"] == "success" assert result["formatted_findings"] == "" def test_detailed_research_returns_formatted_findings(self): """Test detailed_research returns formatted_findings field.""" from local_deep_research.mcp.server import detailed_research with patch( "local_deep_research.mcp.server.ldr_detailed_research", return_value={ "query": "test", "research_id": "123", "summary": "Test", "findings": [], "sources": [], "iterations": 1, "formatted_findings": "## Detailed Formatted\n\nContent here", }, ): result = detailed_research(query="test") assert result["status"] == "success" assert "formatted_findings" in result assert "Detailed Formatted" in result["formatted_findings"] class TestImportSmokeTests: """Smoke tests to verify module imports work correctly.""" def test_server_module_imports(self): """Test that server module imports without errors.""" from local_deep_research.mcp import server assert hasattr(server, "mcp") assert hasattr(server, "run_server") assert hasattr(server, "quick_research") assert hasattr(server, "detailed_research") assert hasattr(server, "generate_report") assert hasattr(server, "analyze_documents") assert hasattr(server, "list_search_engines") assert hasattr(server, "list_strategies") assert hasattr(server, "get_configuration") def test_all_tools_have_type_hints(self): """Test that all tool functions have return type hints.""" from local_deep_research.mcp.server import ( quick_research, detailed_research, generate_report, analyze_documents, list_search_engines, list_strategies, get_configuration, ) import typing for func in [ quick_research, detailed_research, generate_report, analyze_documents, list_search_engines, list_strategies, get_configuration, ]: hints = typing.get_type_hints(func) assert "return" in hints, ( f"{func.__name__} missing return type hint" ) def test_helper_functions_exist(self): """Test that helper functions exist and are importable.""" from local_deep_research.mcp.server import ( _classify_error, _build_settings_overrides, ) from local_deep_research.search_system_factory import ( get_available_strategies, ) assert callable(_classify_error) assert callable(_build_settings_overrides) strategies = get_available_strategies() assert isinstance(strategies, list) class TestAvailableStrategiesContent: """Tests for get_available_strategies content validation.""" def test_all_strategies_have_unique_names(self): """Test that all strategy names are unique.""" from local_deep_research.search_system_factory import ( get_available_strategies, ) strategies = get_available_strategies() names = [s["name"] for s in strategies] assert len(names) == len(set(names)), "Duplicate strategy names found" def test_all_strategies_have_non_empty_descriptions(self): """Test that all strategies have meaningful descriptions.""" from local_deep_research.search_system_factory import ( get_available_strategies, ) strategies = get_available_strategies() for strategy in strategies: assert len(strategy["description"]) > 10, ( f"Strategy {strategy['name']} has too short description" ) def test_key_strategies_included(self): """Test that key strategies are included in the full list.""" from local_deep_research.search_system_factory import ( get_available_strategies, ) strategies = get_available_strategies() names = [s["name"] for s in strategies] # These are the actual strategies defined in the system expected_strategies = [ "source-based", "focused-iteration", "focused-iteration-standard", "topic-organization", "langgraph-agent", ] for expected in expected_strategies: assert expected in names, ( f"Expected strategy '{expected}' not found" ) class TestErrorClassificationAdditional: """Additional error classification tests.""" def test_classify_error_case_insensitive(self): """Test that error classification is case insensitive.""" from local_deep_research.mcp.server import _classify_error assert _classify_error("SERVICE UNAVAILABLE") == "service_unavailable" assert _classify_error("TIMEOUT error") == "timeout" assert _classify_error("API KEY invalid") == "auth_error" def test_classify_error_partial_match(self): """Test that error classification works with partial matches.""" from local_deep_research.mcp.server import _classify_error assert ( _classify_error("The authentication system rejected the request") == "auth_error" ) # The word "timeout" must be present for timeout classification assert _classify_error("Request timeout after 30 seconds") == "timeout" assert ( _classify_error("Connection refused by server") == "connection_error" ) def test_classify_error_empty_string(self): """Test classification of empty error string.""" from local_deep_research.mcp.server import _classify_error assert _classify_error("") == "unknown" def test_classify_error_none_like(self): """Test classification handles various error strings.""" from local_deep_research.mcp.server import _classify_error # All unknown errors assert _classify_error("Something went wrong") == "unknown" assert _classify_error("Error occurred") == "unknown" assert _classify_error("null pointer exception") == "unknown" class TestListSearchEnginesOutput: """Tests for list_search_engines output structure.""" def test_engines_sorted_by_name(self): """Test that engines are sorted alphabetically by name.""" from local_deep_research.mcp.server import list_search_engines mock_engines = { "z_engine": {"description": "Z engine"}, "a_engine": {"description": "A engine"}, "m_engine": {"description": "M engine"}, } with patch( "local_deep_research.web_search_engines.search_engines_config.search_config", return_value=mock_engines, ): result = list_search_engines() assert result["status"] == "success" names = [e["name"] for e in result["engines"]] assert names == sorted(names), "Engines should be sorted alphabetically" def test_engine_info_structure(self): """Test that each engine has the expected info structure.""" from local_deep_research.mcp.server import list_search_engines mock_engines = { "test_engine": { "description": "Test engine", "strengths": ["Fast", "Free"], "weaknesses": ["Limited"], "requires_api_key": True, "is_local": False, } } with patch( "local_deep_research.web_search_engines.search_engines_config.search_config", return_value=mock_engines, ): result = list_search_engines() assert result["status"] == "success" engine = result["engines"][0] assert engine["name"] == "test_engine" assert engine["description"] == "Test engine" assert engine["strengths"] == ["Fast", "Free"] assert engine["weaknesses"] == ["Limited"] assert engine["requires_api_key"] is True assert engine["is_local"] is False def test_engine_info_defaults(self): """Test that engine info defaults work for missing fields.""" from local_deep_research.mcp.server import list_search_engines mock_engines = { "minimal_engine": {} # No fields } with patch( "local_deep_research.web_search_engines.search_engines_config.search_config", return_value=mock_engines, ): result = list_search_engines() assert result["status"] == "success" engine = result["engines"][0] assert engine["name"] == "minimal_engine" assert engine["description"] == "" assert engine["strengths"] == [] assert engine["weaknesses"] == [] assert engine["requires_api_key"] is False assert engine["is_local"] is False