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

407 lines
13 KiB
Python

"""Tests for pure validation functions in local_deep_research.mcp.server."""
from unittest.mock import patch
import pytest
# Skip all tests if MCP package is not installed
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"
)
def _import_server():
"""Lazy import to avoid collection-time failures when mcp is missing."""
from local_deep_research.mcp.server import (
ValidationError,
_COLLECTION_NAME_RE,
_build_settings_overrides,
_classify_error,
_validate_iterations,
_validate_max_results,
_validate_query,
_validate_questions_per_iteration,
_validate_search_engine,
)
return {
"ValidationError": ValidationError,
"_COLLECTION_NAME_RE": _COLLECTION_NAME_RE,
"_build_settings_overrides": _build_settings_overrides,
"_classify_error": _classify_error,
"_validate_iterations": _validate_iterations,
"_validate_max_results": _validate_max_results,
"_validate_query": _validate_query,
"_validate_questions_per_iteration": _validate_questions_per_iteration,
"_validate_search_engine": _validate_search_engine,
}
# ---------------------------------------------------------------------------
# _classify_error
# ---------------------------------------------------------------------------
class TestClassifyError:
"""Tests for _classify_error."""
@pytest.mark.parametrize(
"msg, expected",
[
("503 Service Unavailable", "service_unavailable"),
("server unavailable", "service_unavailable"),
("404 Not Found", "model_not_found"),
("resource not found", "model_not_found"),
("API key invalid", "auth_error"),
("authentication failed", "auth_error"),
("unauthorized access", "auth_error"),
("401 Unauthorized", "auth_error"),
("connection timeout", "timeout"),
("request timed out", "timeout"),
("rate limit exceeded", "rate_limit"),
("429 Too Many", "rate_limit"),
("connection refused", "connection_error"),
("validation failed", "validation_error"),
("invalid parameter", "validation_error"),
("some other error", "unknown"),
("", "unknown"),
],
)
def test_classification(self, msg, expected):
srv = _import_server()
assert srv["_classify_error"](msg) == expected
# ---------------------------------------------------------------------------
# _validate_query
# ---------------------------------------------------------------------------
class TestValidateQuery:
"""Tests for _validate_query."""
def test_empty_string_raises(self):
srv = _import_server()
with pytest.raises(srv["ValidationError"], match="empty"):
srv["_validate_query"]("")
def test_whitespace_only_raises(self):
srv = _import_server()
with pytest.raises(srv["ValidationError"], match="empty"):
srv["_validate_query"](" ")
def test_none_raises(self):
srv = _import_server()
with pytest.raises((AttributeError, srv["ValidationError"])):
srv["_validate_query"](None)
def test_valid_query_returned(self):
srv = _import_server()
assert srv["_validate_query"]("valid query") == "valid query"
def test_strips_whitespace(self):
srv = _import_server()
assert srv["_validate_query"](" padded ") == "padded"
def test_exceeds_max_length(self):
srv = _import_server()
with pytest.raises(srv["ValidationError"], match="exceeds maximum"):
srv["_validate_query"]("x" * 10001)
def test_exactly_at_max_length(self):
srv = _import_server()
query = "x" * 10000
assert srv["_validate_query"](query) == query
# ---------------------------------------------------------------------------
# _validate_iterations
# ---------------------------------------------------------------------------
class TestValidateIterations:
"""Tests for _validate_iterations."""
def test_none_returns_none(self):
srv = _import_server()
assert srv["_validate_iterations"](None) is None
def test_valid_value(self):
srv = _import_server()
assert srv["_validate_iterations"](5) == 5
def test_zero_raises(self):
srv = _import_server()
with pytest.raises(srv["ValidationError"], match="positive integer"):
srv["_validate_iterations"](0)
def test_negative_raises(self):
srv = _import_server()
with pytest.raises(srv["ValidationError"], match="positive integer"):
srv["_validate_iterations"](-1)
def test_exceeds_default_max(self):
srv = _import_server()
with pytest.raises(srv["ValidationError"], match="cannot exceed 20"):
srv["_validate_iterations"](21)
def test_at_default_max(self):
srv = _import_server()
assert srv["_validate_iterations"](20) == 20
def test_minimum_value(self):
srv = _import_server()
assert srv["_validate_iterations"](1) == 1
def test_string_raises(self):
srv = _import_server()
with pytest.raises(srv["ValidationError"]):
srv["_validate_iterations"]("5")
def test_float_raises(self):
srv = _import_server()
with pytest.raises(srv["ValidationError"]):
srv["_validate_iterations"](1.5)
def test_custom_max_val(self):
srv = _import_server()
assert srv["_validate_iterations"](10, max_val=10) == 10
with pytest.raises(srv["ValidationError"], match="cannot exceed 10"):
srv["_validate_iterations"](11, max_val=10)
# ---------------------------------------------------------------------------
# _validate_questions_per_iteration
# ---------------------------------------------------------------------------
class TestValidateQuestionsPerIteration:
"""Tests for _validate_questions_per_iteration."""
def test_none_returns_none(self):
srv = _import_server()
assert srv["_validate_questions_per_iteration"](None) is None
def test_valid_value(self):
srv = _import_server()
assert srv["_validate_questions_per_iteration"](5) == 5
def test_zero_raises(self):
srv = _import_server()
with pytest.raises(srv["ValidationError"]):
srv["_validate_questions_per_iteration"](0)
def test_exceeds_max(self):
srv = _import_server()
with pytest.raises(srv["ValidationError"], match="cannot exceed 10"):
srv["_validate_questions_per_iteration"](11)
def test_at_max(self):
srv = _import_server()
assert srv["_validate_questions_per_iteration"](10) == 10
# ---------------------------------------------------------------------------
# _validate_max_results
# ---------------------------------------------------------------------------
class TestValidateMaxResults:
"""Tests for _validate_max_results."""
def test_valid_value(self):
srv = _import_server()
assert srv["_validate_max_results"](10) == 10
def test_zero_raises(self):
srv = _import_server()
with pytest.raises(srv["ValidationError"]):
srv["_validate_max_results"](0)
def test_negative_raises(self):
srv = _import_server()
with pytest.raises(srv["ValidationError"]):
srv["_validate_max_results"](-1)
def test_exceeds_max(self):
srv = _import_server()
with pytest.raises(srv["ValidationError"], match="cannot exceed 100"):
srv["_validate_max_results"](101)
def test_at_max(self):
srv = _import_server()
assert srv["_validate_max_results"](100) == 100
def test_string_raises(self):
srv = _import_server()
with pytest.raises(srv["ValidationError"]):
srv["_validate_max_results"]("10")
# ---------------------------------------------------------------------------
# _build_settings_overrides
# ---------------------------------------------------------------------------
class TestBuildSettingsOverrides:
"""Tests for _build_settings_overrides."""
@patch(
"local_deep_research.mcp.server._validate_strategy",
return_value="standard",
)
@patch(
"local_deep_research.mcp.server._validate_search_engine",
return_value="google",
)
def test_all_overrides(self, mock_engine, mock_strategy):
srv = _import_server()
result = srv["_build_settings_overrides"](
search_engine="google",
strategy="standard",
iterations=5,
questions_per_iteration=3,
temperature=0.7,
)
assert result["search.tool"] == "google"
assert result["search.search_strategy"] == "standard"
assert result["search.iterations"] == 5
assert result["search.questions_per_iteration"] == 3
assert result["llm.temperature"] == 0.7
@patch(
"local_deep_research.mcp.server._validate_strategy",
return_value=None,
)
@patch(
"local_deep_research.mcp.server._validate_search_engine",
return_value=None,
)
def test_all_none_returns_empty(self, mock_engine, mock_strategy):
srv = _import_server()
result = srv["_build_settings_overrides"]()
assert result == {}
@patch(
"local_deep_research.mcp.server._validate_strategy",
return_value=None,
)
@patch(
"local_deep_research.mcp.server._validate_search_engine",
return_value=None,
)
def test_only_some_params(self, mock_engine, mock_strategy):
srv = _import_server()
result = srv["_build_settings_overrides"](iterations=3, temperature=0.5)
assert result == {
"search.iterations": 3,
"llm.temperature": 0.5,
}
assert "search.tool" not in result
assert "search.search_strategy" not in result
assert "search.questions_per_iteration" not in result
@patch(
"local_deep_research.mcp.server._validate_strategy",
return_value=None,
)
@patch(
"local_deep_research.mcp.server._validate_search_engine",
return_value=None,
)
def test_engine_validates_to_none_not_added(
self, mock_engine, mock_strategy
):
srv = _import_server()
result = srv["_build_settings_overrides"](search_engine="")
assert "search.tool" not in result
# ---------------------------------------------------------------------------
# _COLLECTION_NAME_RE
# ---------------------------------------------------------------------------
class TestCollectionNameRegex:
"""Tests for _COLLECTION_NAME_RE collection name validation."""
def test_path_traversal_rejected(self):
srv = _import_server()
assert srv["_COLLECTION_NAME_RE"].match("../etc/passwd") is None
def test_too_long_rejected(self):
srv = _import_server()
assert srv["_COLLECTION_NAME_RE"].match("a" * 101) is None
def test_valid_name_with_hyphens_and_underscores(self):
srv = _import_server()
assert (
srv["_COLLECTION_NAME_RE"].match("my-collection_name") is not None
)
def test_valid_alphanumeric_with_spaces(self):
srv = _import_server()
assert srv["_COLLECTION_NAME_RE"].match("My Collection 42") is not None
def test_exactly_100_chars_accepted(self):
srv = _import_server()
assert srv["_COLLECTION_NAME_RE"].match("a" * 100) is not None
def test_empty_string_rejected(self):
srv = _import_server()
assert srv["_COLLECTION_NAME_RE"].match("") is None
def test_special_chars_rejected(self):
srv = _import_server()
assert srv["_COLLECTION_NAME_RE"].match("col;drop table") is None
def test_tab_rejected(self):
srv = _import_server()
assert srv["_COLLECTION_NAME_RE"].match("col\tname") is None
def test_newline_rejected(self):
srv = _import_server()
assert srv["_COLLECTION_NAME_RE"].match("col\nname") is None
# ---------------------------------------------------------------------------
# _validate_search_engine
# ---------------------------------------------------------------------------
class TestValidateSearchEngine:
"""Tests for _validate_search_engine."""
def test_none_returns_none(self):
srv = _import_server()
assert srv["_validate_search_engine"](None) is None
def test_empty_string_returns_none(self):
srv = _import_server()
assert srv["_validate_search_engine"]("") is None
def test_whitespace_only_returns_none(self):
srv = _import_server()
assert srv["_validate_search_engine"](" ") is None
@patch(
"local_deep_research.web_search_engines.search_engines_config.search_config",
side_effect=RuntimeError("config unavailable"),
)
@patch("local_deep_research.mcp.server.create_settings_snapshot")
def test_config_load_failure_raises_validation_error(
self, mock_settings, mock_config
):
srv = _import_server()
with pytest.raises(
srv["ValidationError"], match="engine configuration unavailable"
):
srv["_validate_search_engine"]("some_engine")