"""Load `mcp-servers/<server>/app` for tests.
The MCP servers are separate processes with their own venv, not packages of
this project, and `pyproject.toml` collects `tests/` only — so a test reaches
them by path. A relative import inside them (`from .config import Settings`)
needs a real package entry in `sys.modules` with a search path, which is what
``_load`` builds.
"""
from __future__ import annotations
import importlib.util
import sys
from pathlib import Path
from types import ModuleType
import pytest
_SERVERS = Path(__file__).resolve().parents[3] / "mcp-servers"
def _load(server: str, module: str | None = None) -> ModuleType:
app_dir = _SERVERS / server / "app"
package = "mcp_" + server.replace("-", "_") + "_app"
if package not in sys.modules:
spec = importlib.util.spec_from_file_location(
package, app_dir / "__init__.py", submodule_search_locations=[str(app_dir)]
)
assert spec and spec.loader
package_module = importlib.util.module_from_spec(spec)
sys.modules[package] = package_module
spec.loader.exec_module(package_module)
if module is None:
return sys.modules[package]
name = f"{package}.{module}"
if name not in sys.modules:
spec = importlib.util.spec_from_file_location(name, app_dir / f"{module}.py")
assert spec and spec.loader
module_object = importlib.util.module_from_spec(spec)
sys.modules[name] = module_object
spec.loader.exec_module(module_object)
return sys.modules[name]
@pytest.fixture(scope="session")
def navi_web_search():
return _load("navi-web", "search")
@pytest.fixture(scope="session")
def navi_web_browse():
return _load("navi-web", "browse")
@pytest.fixture(scope="session")
def navi_web_extract():
"""Page-to-markdown rendering — the part that needs no browser."""
return _load("navi-web", "extract")
@pytest.fixture(scope="session")
def navi_web_server():
"""The FastMCP tool layer — where ``isError`` is decided."""
return _load("navi-web", "mcp_server")
@pytest.fixture
def navi_web_settings():
"""Settings with every optional backend off, so each test enables its own."""
settings = _load("navi-web", "config").Settings()
settings.searxng_url = ""
settings.brave_api_key = ""
settings.ddg_region = "ru-ru"
return settings