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
279 lines
9.6 KiB
Python
279 lines
9.6 KiB
Python
"""
|
|
MCP protocol tests for the MCP server.
|
|
|
|
Tests for server setup, tool registration, and logging configuration.
|
|
"""
|
|
|
|
import sys
|
|
import io
|
|
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 TestMCPServerSetup:
|
|
"""Tests for MCP server initialization and configuration."""
|
|
|
|
def test_mcp_server_instance_created(self):
|
|
"""Verify FastMCP server instance is created correctly."""
|
|
from local_deep_research.mcp.server import mcp
|
|
|
|
assert mcp is not None
|
|
assert mcp.name == "local-deep-research"
|
|
|
|
def test_mcp_server_instructions_set(self):
|
|
"""Verify server instructions/description is set."""
|
|
from local_deep_research.mcp.server import mcp
|
|
|
|
# FastMCP stores instructions
|
|
assert mcp.instructions is not None
|
|
assert "research" in mcp.instructions.lower()
|
|
|
|
def test_run_server_function_exists(self):
|
|
"""Verify run_server function is exported."""
|
|
from local_deep_research.mcp import run_server
|
|
|
|
assert callable(run_server)
|
|
|
|
|
|
class TestMCPToolRegistration:
|
|
"""Tests for MCP tool decorators and registration."""
|
|
|
|
def test_all_expected_tools_are_defined(self):
|
|
"""Verify all 7 expected tools are defined as functions."""
|
|
from local_deep_research.mcp.server import (
|
|
quick_research,
|
|
detailed_research,
|
|
generate_report,
|
|
analyze_documents,
|
|
list_search_engines,
|
|
list_strategies,
|
|
get_configuration,
|
|
)
|
|
|
|
# All tools should be callable
|
|
assert callable(quick_research)
|
|
assert callable(detailed_research)
|
|
assert callable(generate_report)
|
|
assert callable(analyze_documents)
|
|
assert callable(list_search_engines)
|
|
assert callable(list_strategies)
|
|
assert callable(get_configuration)
|
|
|
|
def test_quick_research_has_docstring(self):
|
|
"""Verify quick_research has documentation."""
|
|
from local_deep_research.mcp.server import quick_research
|
|
|
|
assert quick_research.__doc__ is not None
|
|
assert len(quick_research.__doc__) > 50 # Substantial docstring
|
|
assert "query" in quick_research.__doc__.lower()
|
|
|
|
def test_detailed_research_has_docstring(self):
|
|
"""Verify detailed_research has documentation."""
|
|
from local_deep_research.mcp.server import detailed_research
|
|
|
|
assert detailed_research.__doc__ is not None
|
|
assert "detailed" in detailed_research.__doc__.lower()
|
|
|
|
def test_generate_report_has_docstring(self):
|
|
"""Verify generate_report has documentation."""
|
|
from local_deep_research.mcp.server import generate_report
|
|
|
|
assert generate_report.__doc__ is not None
|
|
assert "report" in generate_report.__doc__.lower()
|
|
|
|
def test_analyze_documents_has_docstring(self):
|
|
"""Verify analyze_documents has documentation."""
|
|
from local_deep_research.mcp.server import analyze_documents
|
|
|
|
assert analyze_documents.__doc__ is not None
|
|
assert "document" in analyze_documents.__doc__.lower()
|
|
|
|
def test_list_search_engines_has_docstring(self):
|
|
"""Verify list_search_engines has documentation."""
|
|
from local_deep_research.mcp.server import list_search_engines
|
|
|
|
assert list_search_engines.__doc__ is not None
|
|
assert "search" in list_search_engines.__doc__.lower()
|
|
|
|
def test_list_strategies_has_docstring(self):
|
|
"""Verify list_strategies has documentation."""
|
|
from local_deep_research.mcp.server import list_strategies
|
|
|
|
assert list_strategies.__doc__ is not None
|
|
assert "strateg" in list_strategies.__doc__.lower()
|
|
|
|
def test_get_configuration_has_docstring(self):
|
|
"""Verify get_configuration has documentation."""
|
|
from local_deep_research.mcp.server import get_configuration
|
|
|
|
assert get_configuration.__doc__ is not None
|
|
assert "config" in get_configuration.__doc__.lower()
|
|
|
|
|
|
class TestMCPLogging:
|
|
"""Tests for MCP logging configuration (critical for STDIO)."""
|
|
|
|
def test_no_stdout_pollution_from_list_strategies(self):
|
|
"""Verify list_strategies doesn't write to stdout."""
|
|
from local_deep_research.mcp.server import list_strategies
|
|
|
|
# Capture stdout
|
|
old_stdout = sys.stdout
|
|
sys.stdout = captured_stdout = io.StringIO()
|
|
|
|
try:
|
|
result = list_strategies()
|
|
stdout_output = captured_stdout.getvalue()
|
|
finally:
|
|
sys.stdout = old_stdout
|
|
|
|
# Should have no stdout output (MCP uses stdout for JSON-RPC)
|
|
assert stdout_output == "", f"Unexpected stdout output: {stdout_output}"
|
|
assert result["status"] == "success"
|
|
|
|
def test_no_stdout_pollution_from_get_configuration(self):
|
|
"""Verify get_configuration doesn't write to stdout."""
|
|
from local_deep_research.mcp.server import get_configuration
|
|
|
|
# Capture stdout
|
|
old_stdout = sys.stdout
|
|
sys.stdout = captured_stdout = io.StringIO()
|
|
|
|
try:
|
|
result = get_configuration()
|
|
stdout_output = captured_stdout.getvalue()
|
|
finally:
|
|
sys.stdout = old_stdout
|
|
|
|
# Should have no stdout output
|
|
assert stdout_output == "", f"Unexpected stdout output: {stdout_output}"
|
|
assert result["status"] == "success"
|
|
|
|
def test_no_stdout_pollution_during_error(self):
|
|
"""Verify error handling doesn't write to stdout."""
|
|
from local_deep_research.mcp.server import quick_research
|
|
|
|
# Capture stdout
|
|
old_stdout = sys.stdout
|
|
sys.stdout = captured_stdout = io.StringIO()
|
|
|
|
try:
|
|
with patch(
|
|
"local_deep_research.mcp.server.ldr_quick_summary",
|
|
side_effect=Exception("Test error"),
|
|
):
|
|
result = quick_research(query="test")
|
|
stdout_output = captured_stdout.getvalue()
|
|
finally:
|
|
sys.stdout = old_stdout
|
|
|
|
# Should have no stdout output even during errors
|
|
assert stdout_output == "", f"Unexpected stdout output: {stdout_output}"
|
|
assert result["status"] == "error"
|
|
|
|
|
|
class TestAvailableStrategies:
|
|
"""Tests for the get_available_strategies function.
|
|
|
|
MCP server exposes all strategies (show_all=True), so these tests
|
|
validate the full list.
|
|
"""
|
|
|
|
def test_available_strategies_is_list(self):
|
|
"""Verify get_available_strategies returns a list."""
|
|
from local_deep_research.search_system_factory import (
|
|
get_available_strategies,
|
|
)
|
|
|
|
strategies = get_available_strategies()
|
|
assert isinstance(strategies, list)
|
|
|
|
def test_available_strategies_has_entries(self):
|
|
"""Verify get_available_strategies has multiple entries."""
|
|
from local_deep_research.search_system_factory import (
|
|
get_available_strategies,
|
|
)
|
|
|
|
strategies = get_available_strategies()
|
|
assert len(strategies) >= 5
|
|
|
|
def test_available_strategies_have_required_fields(self):
|
|
"""Verify each strategy has name and description."""
|
|
from local_deep_research.search_system_factory import (
|
|
get_available_strategies,
|
|
)
|
|
|
|
strategies = get_available_strategies()
|
|
for strategy in strategies:
|
|
assert "name" in strategy, f"Strategy missing 'name': {strategy}"
|
|
assert "description" in strategy, (
|
|
f"Strategy missing 'description': {strategy}"
|
|
)
|
|
assert isinstance(strategy["name"], str)
|
|
assert isinstance(strategy["description"], str)
|
|
assert len(strategy["name"]) > 0
|
|
assert len(strategy["description"]) > 0
|
|
|
|
def test_source_based_strategy_exists(self):
|
|
"""Verify source-based strategy is in the list."""
|
|
from local_deep_research.search_system_factory import (
|
|
get_available_strategies,
|
|
)
|
|
|
|
strategies = get_available_strategies()
|
|
strategy_names = [s["name"] for s in strategies]
|
|
assert "source-based" in strategy_names
|
|
|
|
def test_focused_iteration_strategy_exists(self):
|
|
"""Verify focused-iteration strategy is in the list."""
|
|
from local_deep_research.search_system_factory import (
|
|
get_available_strategies,
|
|
)
|
|
|
|
strategies = get_available_strategies()
|
|
strategy_names = [s["name"] for s in strategies]
|
|
assert "focused-iteration" in strategy_names
|
|
|
|
|
|
class TestHelperFunctions:
|
|
"""Tests for helper functions in server.py."""
|
|
|
|
def test_classify_error_is_callable(self):
|
|
"""Verify _classify_error function exists and is callable."""
|
|
from local_deep_research.mcp.server import _classify_error
|
|
|
|
assert callable(_classify_error)
|
|
|
|
def test_build_settings_overrides_is_callable(self):
|
|
"""Verify _build_settings_overrides function exists and is callable."""
|
|
from local_deep_research.mcp.server import _build_settings_overrides
|
|
|
|
assert callable(_build_settings_overrides)
|
|
|
|
def test_classify_error_returns_string(self):
|
|
"""Verify _classify_error returns a string."""
|
|
from local_deep_research.mcp.server import _classify_error
|
|
|
|
result = _classify_error("Some error")
|
|
assert isinstance(result, str)
|
|
|
|
def test_build_settings_overrides_returns_dict(self):
|
|
"""Verify _build_settings_overrides returns a dict."""
|
|
from local_deep_research.mcp.server import _build_settings_overrides
|
|
|
|
result = _build_settings_overrides()
|
|
assert isinstance(result, dict)
|