7a0da7932b
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Has been cancelled
Docker Tests (Consolidated) / Accessibility Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Unit Tests (push) Has been cancelled
Docker Tests (Consolidated) / LLM Example Tests (push) Has been cancelled
Docker Tests (Consolidated) / Production Image Smoke Test (push) Has been cancelled
Docker Tests (Consolidated) / Infrastructure Tests (push) Has been cancelled
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Has been cancelled
Backwards Compatibility / Verify Encryption Constants (push) Has been cancelled
Backwards Compatibility / PyPI Version Compatibility (push) Has been cancelled
Backwards Compatibility / Database Migration Tests (push) Has been cancelled
CodeQL Advanced / Analyze (python) (push) Has been cancelled
Docker Tests (Consolidated) / detect-changes (push) Has been cancelled
Docker Tests (Consolidated) / Build Test Image (push) Has been cancelled
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Has been cancelled
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Has been cancelled
542 lines
20 KiB
Python
542 lines
20 KiB
Python
"""
|
|
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
|