Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

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