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
782 lines
28 KiB
Python
782 lines
28 KiB
Python
"""
|
|
Tests for the MCP server tools.
|
|
|
|
These tests use mocked research functions to verify the MCP tool implementations
|
|
without actually running research (which would take minutes).
|
|
"""
|
|
|
|
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 TestQuickResearch:
|
|
"""Tests for the quick_research tool."""
|
|
|
|
def test_quick_research_success(self, mock_quick_summary):
|
|
"""Test successful quick research."""
|
|
from local_deep_research.mcp.server import quick_research
|
|
|
|
result = quick_research(query="What is quantum computing?")
|
|
|
|
assert result["status"] == "success"
|
|
assert "summary" in result
|
|
assert (
|
|
result["summary"]
|
|
== "This is a test summary about quantum computing."
|
|
)
|
|
assert "findings" in result
|
|
assert "sources" in result
|
|
assert result["iterations"] == 1
|
|
mock_quick_summary.assert_called_once()
|
|
|
|
def test_quick_research_with_overrides(self, mock_quick_summary):
|
|
"""Test quick research with parameter overrides."""
|
|
from local_deep_research.mcp.server import quick_research
|
|
|
|
result = quick_research(
|
|
query="Test query",
|
|
search_engine="wikipedia",
|
|
strategy="source-based",
|
|
iterations=2,
|
|
)
|
|
|
|
assert result["status"] == "success"
|
|
mock_quick_summary.assert_called_once()
|
|
# Verify settings_snapshot was passed
|
|
call_kwargs = mock_quick_summary.call_args[1]
|
|
assert "settings_snapshot" in call_kwargs
|
|
|
|
def test_quick_research_error_handling(self):
|
|
"""Test error handling in quick research."""
|
|
from local_deep_research.mcp.server import quick_research
|
|
|
|
with patch(
|
|
"local_deep_research.mcp.server.ldr_quick_summary",
|
|
side_effect=Exception("API key invalid"),
|
|
):
|
|
result = quick_research(query="Test query")
|
|
|
|
assert result["status"] == "error"
|
|
assert "error" in result
|
|
assert result["error_type"] == "auth_error"
|
|
|
|
|
|
class TestDetailedResearch:
|
|
"""Tests for the detailed_research tool."""
|
|
|
|
def test_detailed_research_success(self, mock_detailed_research):
|
|
"""Test successful detailed research."""
|
|
from local_deep_research.mcp.server import detailed_research
|
|
|
|
result = detailed_research(query="quantum computing applications")
|
|
|
|
assert result["status"] == "success"
|
|
assert result["query"] == "quantum computing applications"
|
|
assert result["research_id"] == "test-research-123"
|
|
assert "summary" in result
|
|
assert "findings" in result
|
|
assert len(result["findings"]) == 2
|
|
assert "metadata" in result
|
|
|
|
def test_detailed_research_error_handling(self):
|
|
"""Test error handling in detailed research."""
|
|
from local_deep_research.mcp.server import detailed_research
|
|
|
|
with patch(
|
|
"local_deep_research.mcp.server.ldr_detailed_research",
|
|
side_effect=Exception("Service unavailable: 503"),
|
|
):
|
|
result = detailed_research(query="Test query")
|
|
|
|
assert result["status"] == "error"
|
|
assert result["error_type"] == "service_unavailable"
|
|
|
|
|
|
class TestGenerateReport:
|
|
"""Tests for the generate_report tool."""
|
|
|
|
def test_generate_report_success(self, mock_generate_report):
|
|
"""Test successful report generation."""
|
|
from local_deep_research.mcp.server import generate_report
|
|
|
|
result = generate_report(query="quantum computing")
|
|
|
|
assert result["status"] == "success"
|
|
assert "content" in result
|
|
assert result["content"].startswith("# Research Report")
|
|
assert "metadata" in result
|
|
|
|
def test_generate_report_with_options(self, mock_generate_report):
|
|
"""Test report generation with options."""
|
|
from local_deep_research.mcp.server import generate_report
|
|
|
|
result = generate_report(
|
|
query="Test query",
|
|
search_engine="arxiv",
|
|
searches_per_section=3,
|
|
)
|
|
|
|
assert result["status"] == "success"
|
|
call_kwargs = mock_generate_report.call_args[1]
|
|
assert call_kwargs["searches_per_section"] == 3
|
|
|
|
def test_generate_report_preserves_assembled_sources_block(self):
|
|
"""MCP surfaces the generator's in-memory assembled content verbatim,
|
|
including the ``## Sources`` block — it never DB-reads or strips it.
|
|
|
|
Pins the MCP-keeps-assembled-shape invariant the #3665 audit
|
|
confirmed (in-memory generator output, never a DB read).
|
|
"""
|
|
from local_deep_research.mcp.server import generate_report
|
|
|
|
assembled = (
|
|
"# Research Report\n\n## Introduction\n\nBody.\n\n"
|
|
"## Sources\n\n[1] https://src.example\n\n"
|
|
"## Research Metrics\n\nSearch Iterations: 2\n"
|
|
)
|
|
with patch(
|
|
"local_deep_research.mcp.server.ldr_generate_report",
|
|
return_value={"content": assembled, "metadata": {"query": "q"}},
|
|
):
|
|
result = generate_report(query="q")
|
|
|
|
assert result["status"] == "success"
|
|
# Verbatim passthrough: the assembled content (incl. ## Sources and the
|
|
# trailing ## Research Metrics) survives unmodified. Full equality —
|
|
# substring presence alone wouldn't catch truncation after ## Sources.
|
|
assert result["content"] == assembled
|
|
assert result["metadata"] == {"query": "q"}
|
|
|
|
|
|
class TestAnalyzeDocuments:
|
|
"""Tests for the analyze_documents tool."""
|
|
|
|
def test_analyze_documents_success(self, mock_analyze_documents):
|
|
"""Test successful document analysis."""
|
|
from local_deep_research.mcp.server import analyze_documents
|
|
|
|
result = analyze_documents(
|
|
query="test query",
|
|
collection_name="test_collection",
|
|
)
|
|
|
|
assert result["status"] == "success"
|
|
assert result["collection"] == "test_collection"
|
|
assert result["document_count"] == 1
|
|
assert "summary" in result
|
|
assert "documents" in result
|
|
|
|
def test_analyze_documents_error_handling(self):
|
|
"""Test error handling in document analysis."""
|
|
from local_deep_research.mcp.server import analyze_documents
|
|
|
|
with patch(
|
|
"local_deep_research.mcp.server.ldr_analyze_documents",
|
|
side_effect=Exception("Collection not found"),
|
|
):
|
|
result = analyze_documents(
|
|
query="test query",
|
|
collection_name="nonexistent",
|
|
)
|
|
|
|
assert result["status"] == "error"
|
|
assert result["error_type"] == "model_not_found"
|
|
|
|
|
|
class TestDiscoveryTools:
|
|
"""Tests for discovery tools (list_search_engines, list_strategies, get_configuration)."""
|
|
|
|
def test_list_strategies(self):
|
|
"""Test listing available strategies."""
|
|
from local_deep_research.mcp.server import list_strategies
|
|
|
|
result = list_strategies()
|
|
|
|
assert result["status"] == "success"
|
|
assert "strategies" in result
|
|
assert len(result["strategies"]) > 0
|
|
# Check that each strategy has required fields
|
|
for strategy in result["strategies"]:
|
|
assert "name" in strategy
|
|
assert "description" in strategy
|
|
|
|
def test_list_search_engines(self):
|
|
"""Test listing available search engines."""
|
|
from local_deep_research.mcp.server import list_search_engines
|
|
|
|
# Mock the search_config function at the right location
|
|
mock_engines = {
|
|
"wikipedia": {
|
|
"description": "Wikipedia search",
|
|
"strengths": ["Free", "Reliable"],
|
|
"weaknesses": ["Limited depth"],
|
|
"requires_api_key": False,
|
|
},
|
|
"arxiv": {
|
|
"description": "arXiv academic papers",
|
|
"strengths": ["Academic sources"],
|
|
"weaknesses": ["Science only"],
|
|
"requires_api_key": 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"
|
|
assert "engines" in result
|
|
assert len(result["engines"]) == 2
|
|
|
|
def test_get_configuration(self, mock_settings_snapshot):
|
|
"""Test getting server configuration."""
|
|
from local_deep_research.mcp.server import get_configuration
|
|
|
|
result = get_configuration()
|
|
|
|
assert result["status"] == "success"
|
|
assert "config" in result
|
|
config = result["config"]
|
|
assert "llm" in config
|
|
assert "search" in config
|
|
|
|
|
|
class TestSearch:
|
|
"""Tests for the search tool."""
|
|
|
|
def test_search_success(self):
|
|
"""Test successful raw search."""
|
|
from local_deep_research.mcp.server import search
|
|
|
|
mock_engine = type(
|
|
"MockEngine",
|
|
(),
|
|
{
|
|
"run": lambda self, q: [
|
|
{
|
|
"title": "Result 1",
|
|
"link": "https://example.com/1",
|
|
"snippet": "Snippet 1",
|
|
},
|
|
{
|
|
"title": "Result 2",
|
|
"link": "https://example.com/2",
|
|
"snippet": "Snippet 2",
|
|
},
|
|
]
|
|
},
|
|
)()
|
|
mock_engines_config = {
|
|
"wikipedia": {
|
|
"description": "Wikipedia",
|
|
"requires_api_key": False,
|
|
},
|
|
}
|
|
|
|
with (
|
|
patch(
|
|
"local_deep_research.mcp.server.create_settings_snapshot",
|
|
return_value={},
|
|
),
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engines_config.search_config",
|
|
return_value=mock_engines_config,
|
|
),
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.create_search_engine",
|
|
return_value=mock_engine,
|
|
),
|
|
):
|
|
result = search(query="quantum computing", engine="wikipedia")
|
|
|
|
assert result["status"] == "success"
|
|
assert result["query"] == "quantum computing"
|
|
assert result["engine"] == "wikipedia"
|
|
assert result["result_count"] == 2
|
|
assert len(result["results"]) == 2
|
|
assert result["results"][0]["title"] == "Result 1"
|
|
|
|
def test_search_with_max_results(self):
|
|
"""Test that max_results is passed to factory."""
|
|
from local_deep_research.mcp.server import search
|
|
|
|
mock_engine = type("MockEngine", (), {"run": lambda self, q: []})()
|
|
mock_engines_config = {
|
|
"arxiv": {"description": "arXiv", "requires_api_key": False},
|
|
}
|
|
|
|
with (
|
|
patch(
|
|
"local_deep_research.mcp.server.create_settings_snapshot",
|
|
return_value={},
|
|
),
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engines_config.search_config",
|
|
return_value=mock_engines_config,
|
|
),
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.create_search_engine",
|
|
return_value=mock_engine,
|
|
) as mock_factory,
|
|
):
|
|
result = search(query="test", engine="arxiv", max_results=25)
|
|
|
|
assert result["status"] == "success"
|
|
mock_factory.assert_called_once()
|
|
call_kwargs = mock_factory.call_args[1]
|
|
assert call_kwargs["max_results"] == 25
|
|
|
|
def test_search_empty_query_error(self):
|
|
"""Test that empty query returns validation error."""
|
|
from local_deep_research.mcp.server import search
|
|
|
|
result = search(query="", engine="wikipedia")
|
|
|
|
assert result["status"] == "error"
|
|
assert result["error_type"] == "validation_error"
|
|
assert "empty" in result["error"].lower()
|
|
|
|
def test_search_invalid_engine_error(self):
|
|
"""Test that unknown engine name returns validation error with available engines."""
|
|
from local_deep_research.mcp.server import search
|
|
|
|
mock_engines_config = {
|
|
"wikipedia": {"description": "Wikipedia"},
|
|
"arxiv": {"description": "arXiv"},
|
|
}
|
|
|
|
with (
|
|
patch(
|
|
"local_deep_research.mcp.server.create_settings_snapshot",
|
|
return_value={},
|
|
),
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engines_config.search_config",
|
|
return_value=mock_engines_config,
|
|
),
|
|
):
|
|
result = search(query="test", engine="nonexistent")
|
|
|
|
assert result["status"] == "error"
|
|
assert result["error_type"] == "validation_error"
|
|
assert "nonexistent" in result["error"]
|
|
assert "arxiv" in result["error"]
|
|
assert "wikipedia" in result["error"]
|
|
|
|
def test_search_engine_creation_failure(self):
|
|
"""Test error when factory returns None."""
|
|
from local_deep_research.mcp.server import search
|
|
|
|
mock_engines_config = {
|
|
"searxng": {"description": "SearXNG", "requires_api_key": False},
|
|
}
|
|
|
|
with (
|
|
patch(
|
|
"local_deep_research.mcp.server.create_settings_snapshot",
|
|
return_value={},
|
|
),
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engines_config.search_config",
|
|
return_value=mock_engines_config,
|
|
),
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.create_search_engine",
|
|
return_value=None,
|
|
),
|
|
):
|
|
result = search(query="test", engine="searxng")
|
|
|
|
assert result["status"] == "error"
|
|
assert "configuration_error" in result["error_type"]
|
|
|
|
def test_search_empty_results(self):
|
|
"""Test success with empty results list."""
|
|
from local_deep_research.mcp.server import search
|
|
|
|
mock_engine = type("MockEngine", (), {"run": lambda self, q: []})()
|
|
mock_engines_config = {
|
|
"arxiv": {"description": "arXiv", "requires_api_key": False},
|
|
}
|
|
|
|
with (
|
|
patch(
|
|
"local_deep_research.mcp.server.create_settings_snapshot",
|
|
return_value={},
|
|
),
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engines_config.search_config",
|
|
return_value=mock_engines_config,
|
|
),
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.create_search_engine",
|
|
return_value=mock_engine,
|
|
),
|
|
):
|
|
result = search(query="obscure topic", engine="arxiv")
|
|
|
|
assert result["status"] == "success"
|
|
assert result["result_count"] == 0
|
|
assert result["results"] == []
|
|
|
|
def test_search_missing_api_key_error(self):
|
|
"""Test error when engine requires API key but none is configured."""
|
|
from local_deep_research.mcp.server import search
|
|
|
|
mock_engines_config = {
|
|
"brave": {"description": "Brave Search", "requires_api_key": True},
|
|
}
|
|
|
|
with (
|
|
patch(
|
|
"local_deep_research.mcp.server.create_settings_snapshot",
|
|
return_value={},
|
|
),
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engines_config.search_config",
|
|
return_value=mock_engines_config,
|
|
),
|
|
):
|
|
result = search(query="test", engine="brave")
|
|
|
|
assert result["status"] == "error"
|
|
assert result["error_type"] == "validation_error"
|
|
assert "api key" in result["error"].lower()
|
|
assert "brave" in result["error"].lower()
|
|
|
|
def test_search_requires_llm_engine_without_llm(self):
|
|
"""Test that engines with requires_llm=True work without LLM (degraded mode)."""
|
|
from local_deep_research.mcp.server import search
|
|
|
|
mock_engine = type(
|
|
"MockEngine",
|
|
(),
|
|
{
|
|
"run": lambda self, q: [
|
|
{
|
|
"title": "PubMed Result",
|
|
"link": "https://pubmed.ncbi.nlm.nih.gov/123",
|
|
"snippet": "CRISPR gene therapy results",
|
|
},
|
|
]
|
|
},
|
|
)()
|
|
mock_engines_config = {
|
|
"pubmed": {
|
|
"description": "PubMed biomedical literature",
|
|
"requires_api_key": False,
|
|
"requires_llm": True,
|
|
},
|
|
}
|
|
|
|
with (
|
|
patch(
|
|
"local_deep_research.mcp.server.create_settings_snapshot",
|
|
return_value={},
|
|
),
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engines_config.search_config",
|
|
return_value=mock_engines_config,
|
|
),
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.create_search_engine",
|
|
return_value=mock_engine,
|
|
) as mock_factory,
|
|
):
|
|
result = search(query="CRISPR gene therapy", engine="pubmed")
|
|
|
|
assert result["status"] == "success"
|
|
assert result["engine"] == "pubmed"
|
|
assert result["result_count"] == 1
|
|
assert result["results"][0]["title"] == "PubMed Result"
|
|
# Verify factory was called with llm=None (search tool doesn't use LLM)
|
|
call_kwargs = mock_factory.call_args[1]
|
|
assert call_kwargs["llm"] is None
|
|
|
|
def test_search_normalizes_body_to_snippet(self):
|
|
"""Test that results with 'body' key get 'snippet' alias."""
|
|
from local_deep_research.mcp.server import search
|
|
|
|
mock_engine = type(
|
|
"MockEngine",
|
|
(),
|
|
{
|
|
"run": lambda self, q: [
|
|
{
|
|
"title": "Result",
|
|
"link": "https://example.com",
|
|
"body": "Body text",
|
|
},
|
|
]
|
|
},
|
|
)()
|
|
mock_engines_config = {
|
|
"wikipedia": {
|
|
"description": "Wikipedia",
|
|
"requires_api_key": False,
|
|
},
|
|
}
|
|
|
|
with (
|
|
patch(
|
|
"local_deep_research.mcp.server.create_settings_snapshot",
|
|
return_value={},
|
|
),
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engines_config.search_config",
|
|
return_value=mock_engines_config,
|
|
),
|
|
patch(
|
|
"local_deep_research.web_search_engines.search_engine_factory.create_search_engine",
|
|
return_value=mock_engine,
|
|
),
|
|
):
|
|
result = search(query="test", engine="wikipedia")
|
|
|
|
assert result["status"] == "success"
|
|
assert result["results"][0]["snippet"] == "Body text"
|
|
|
|
|
|
class TestErrorClassification:
|
|
"""Tests for error classification logic."""
|
|
|
|
def test_classify_service_unavailable(self):
|
|
"""Test classification of service unavailable errors."""
|
|
from local_deep_research.mcp.server import _classify_error
|
|
|
|
assert (
|
|
_classify_error("Error 503: Service unavailable")
|
|
== "service_unavailable"
|
|
)
|
|
assert (
|
|
_classify_error("The service is unavailable")
|
|
== "service_unavailable"
|
|
)
|
|
|
|
def test_classify_model_not_found(self):
|
|
"""Test classification of model not found errors."""
|
|
from local_deep_research.mcp.server import _classify_error
|
|
|
|
assert (
|
|
_classify_error("Error 404: Model not found") == "model_not_found"
|
|
)
|
|
assert _classify_error("Model llama3 not found") == "model_not_found"
|
|
|
|
def test_classify_auth_error(self):
|
|
"""Test classification of authentication errors."""
|
|
from local_deep_research.mcp.server import _classify_error
|
|
|
|
assert _classify_error("Invalid API key") == "auth_error"
|
|
assert _classify_error("Authentication failed") == "auth_error"
|
|
assert _classify_error("Unauthorized access") == "auth_error"
|
|
|
|
def test_classify_timeout(self):
|
|
"""Test classification of timeout errors."""
|
|
from local_deep_research.mcp.server import _classify_error
|
|
|
|
assert _classify_error("Request timeout") == "timeout"
|
|
assert _classify_error("Connection timeout after 30s") == "timeout"
|
|
|
|
def test_classify_rate_limit(self):
|
|
"""Test classification of rate limit errors."""
|
|
from local_deep_research.mcp.server import _classify_error
|
|
|
|
assert _classify_error("Rate limit exceeded") == "rate_limit"
|
|
|
|
def test_classify_unknown(self):
|
|
"""Test classification of unknown errors."""
|
|
from local_deep_research.mcp.server import _classify_error
|
|
|
|
assert _classify_error("Something weird happened") == "unknown"
|
|
|
|
|
|
class TestSettingsOverrides:
|
|
"""Tests for settings override building."""
|
|
|
|
def test_build_settings_overrides_empty(self):
|
|
"""Test building overrides with no parameters."""
|
|
from local_deep_research.mcp.server import _build_settings_overrides
|
|
|
|
result = _build_settings_overrides()
|
|
assert result == {}
|
|
|
|
def test_build_settings_overrides_all_params(self):
|
|
"""Test building overrides with all parameters."""
|
|
from local_deep_research.mcp.server import _build_settings_overrides
|
|
|
|
result = _build_settings_overrides(
|
|
search_engine="wikipedia",
|
|
strategy="source-based",
|
|
iterations=3,
|
|
questions_per_iteration=5,
|
|
temperature=0.5,
|
|
)
|
|
|
|
assert result["search.tool"] == "wikipedia"
|
|
assert result["search.search_strategy"] == "source-based"
|
|
assert result["search.iterations"] == 3
|
|
assert result["search.questions_per_iteration"] == 5
|
|
assert result["llm.temperature"] == 0.5
|
|
|
|
def test_build_settings_overrides_partial(self):
|
|
"""Test building overrides with some parameters."""
|
|
from local_deep_research.mcp.server import _build_settings_overrides
|
|
|
|
result = _build_settings_overrides(search_engine="arxiv", iterations=2)
|
|
|
|
assert result["search.tool"] == "arxiv"
|
|
assert result["search.iterations"] == 2
|
|
assert "search.search_strategy" not in result
|
|
assert "llm.temperature" not in result
|
|
|
|
|
|
class TestMcpToolToResearchFunctionContext:
|
|
"""Contract tests: every MCP tool that calls a research function must
|
|
build a settings_snapshot via create_settings_snapshot() and thread it
|
|
to the underlying call. Without this, the user's stored settings (LLM
|
|
provider/model, API keys, embedding model, etc.) are silently ignored
|
|
and the tool falls back to JSON defaults + LDR_* env vars.
|
|
|
|
Pattern: capture-list mock (assertions run on a captured kwargs dict
|
|
after the response, not inside the mock itself — the MCP tool's broad
|
|
``except Exception`` would otherwise swallow AssertionError).
|
|
"""
|
|
|
|
def test_quick_research_threads_settings_snapshot(self):
|
|
"""quick_research → ldr_quick_summary must include settings_snapshot."""
|
|
from local_deep_research.mcp.server import quick_research
|
|
|
|
captured = {}
|
|
|
|
def _capture(*args, **kwargs):
|
|
captured["args"] = args
|
|
captured["kwargs"] = kwargs
|
|
return {
|
|
"summary": "ok",
|
|
"findings": [],
|
|
"sources": [],
|
|
"iterations": 0,
|
|
"formatted_findings": "",
|
|
}
|
|
|
|
with patch(
|
|
"local_deep_research.mcp.server.ldr_quick_summary",
|
|
side_effect=_capture,
|
|
):
|
|
result = quick_research(query="test query")
|
|
|
|
assert result["status"] == "success"
|
|
assert "settings_snapshot" in captured["kwargs"], (
|
|
f"ldr_quick_summary called without settings_snapshot. "
|
|
f"kwargs: {list(captured['kwargs'].keys())}"
|
|
)
|
|
assert captured["kwargs"]["settings_snapshot"] is not None
|
|
|
|
def test_detailed_research_threads_settings_snapshot(self):
|
|
"""detailed_research → ldr_detailed_research must include settings_snapshot."""
|
|
from local_deep_research.mcp.server import detailed_research
|
|
|
|
captured = {}
|
|
|
|
def _capture(*args, **kwargs):
|
|
captured["args"] = args
|
|
captured["kwargs"] = kwargs
|
|
return {
|
|
"query": "test",
|
|
"research_id": "test-123",
|
|
"summary": "ok",
|
|
"findings": [],
|
|
"iterations": 0,
|
|
"questions": {},
|
|
"formatted_findings": "",
|
|
"sources": [],
|
|
"metadata": {},
|
|
}
|
|
|
|
with patch(
|
|
"local_deep_research.mcp.server.ldr_detailed_research",
|
|
side_effect=_capture,
|
|
):
|
|
result = detailed_research(query="test query")
|
|
|
|
assert result["status"] == "success"
|
|
assert "settings_snapshot" in captured["kwargs"], (
|
|
f"ldr_detailed_research called without settings_snapshot. "
|
|
f"kwargs: {list(captured['kwargs'].keys())}"
|
|
)
|
|
assert captured["kwargs"]["settings_snapshot"] is not None
|
|
|
|
def test_generate_report_threads_settings_snapshot(self):
|
|
"""generate_report → ldr_generate_report must include settings_snapshot."""
|
|
from local_deep_research.mcp.server import generate_report
|
|
|
|
captured = {}
|
|
|
|
def _capture(*args, **kwargs):
|
|
captured["args"] = args
|
|
captured["kwargs"] = kwargs
|
|
return {"content": "# Report", "metadata": {}}
|
|
|
|
with patch(
|
|
"local_deep_research.mcp.server.ldr_generate_report",
|
|
side_effect=_capture,
|
|
):
|
|
result = generate_report(query="test query")
|
|
|
|
assert result["status"] == "success"
|
|
assert "settings_snapshot" in captured["kwargs"], (
|
|
f"ldr_generate_report called without settings_snapshot. "
|
|
f"kwargs: {list(captured['kwargs'].keys())}"
|
|
)
|
|
assert captured["kwargs"]["settings_snapshot"] is not None
|
|
|
|
def test_analyze_documents_threads_settings_snapshot(self):
|
|
"""analyze_documents → ldr_analyze_documents must include
|
|
settings_snapshot. This is the regression fence for the bug that
|
|
previously omitted user context from the call."""
|
|
from local_deep_research.mcp.server import analyze_documents
|
|
|
|
captured = {}
|
|
|
|
def _capture(*args, **kwargs):
|
|
captured["args"] = args
|
|
captured["kwargs"] = kwargs
|
|
return {
|
|
"summary": "ok",
|
|
"documents": [],
|
|
"collection": "test",
|
|
"document_count": 0,
|
|
}
|
|
|
|
with patch(
|
|
"local_deep_research.mcp.server.ldr_analyze_documents",
|
|
side_effect=_capture,
|
|
):
|
|
result = analyze_documents(
|
|
query="test query",
|
|
collection_name="test_collection",
|
|
)
|
|
|
|
assert result["status"] == "success"
|
|
assert "settings_snapshot" in captured["kwargs"], (
|
|
f"ldr_analyze_documents called without settings_snapshot — "
|
|
f"user-configured embedding model / LLM provider are ignored. "
|
|
f"kwargs: {list(captured['kwargs'].keys())}"
|
|
)
|
|
assert captured["kwargs"]["settings_snapshot"] is not None
|