Newer
Older
navi-1 / tests / unit / mcp / conftest.py
"""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