Files
2026-08-30 20:44:00 +02:00

165 lines
5.5 KiB
Python

"""Shared fixtures for the LabVoice MCP server test suite.
Conventions pinned by this suite (adjust here if the implementation diverges):
1. Workflows and services receive their collaborators via constructor
arguments (``client``, ``resolver``, ``journal``, ``settings``) exactly as
declared in the scaffolds — tests inject ``FakeElabftwClient`` /
``StubResolver`` and a real ``SagaJournal`` against a temp SQLite file.
2. ``SagaJournal`` has an explicit ``connect()``/``close()`` lifecycle.
3. The REST app and MCP tools resolve their read services through
``mcp_server.server.build_services(settings)`` (the wiring container from
the scaffold); tests monkeypatch that factory. The service container is a
dict with the keys implied by the scaffold's build_services comment:
``client``, ``resolver``, ``journal``, ``protocol``, ``inventory``,
``setup``, ``templates``.
4. MCP-level tests drive the real ``FastMCP`` server (from ``mcp`` 2.1.x)
in-process via ``mcp.client.Client``.
"""
from __future__ import annotations
import sys
from pathlib import Path
import pytest
# Make the vendored elabapi_python client importable for tests (it is vendored,
# not installed). Its deps (six, urllib3, certifi) come from the dev group.
_VENDOR = Path(__file__).resolve().parent.parent / "vendor" / "elabapi-python"
if str(_VENDOR) not in sys.path:
sys.path.insert(0, str(_VENDOR))
from mcp_server.config import Settings
from mcp_server.journal import SagaJournal
from mcp_server.resolve import ResourceResolver
from mcp_server.templates import TemplateService
from mcp_server.workflows.inventory import InventoryWorkflow
from mcp_server.workflows.protocol import ProtocolWorkflow
from mcp_server.workflows.setup import SetupWorkflow
from fakes import (
FakeElabftwClient,
StubResolver,
make_experiment,
make_item,
)
@pytest.fixture
def settings(tmp_path, monkeypatch) -> Settings:
"""Isolated settings: temp DB, no env/.env interference, distinctive API key."""
for var in (
"ELABFTW_URL",
"ELABFTW_API_KEY",
"LABVOICE_DB_PATH",
"LABVOICE_REST_TOKEN",
"LABVOICE_TEMPLATE_DESC_LIMIT",
"LABVOICE_EMBED_MODEL",
"LABVOICE_UNIT_WHITELIST",
"LABVOICE_EXPIRY_FIELD",
):
monkeypatch.delenv(var, raising=False)
return Settings(
_env_file=None,
elabftw_url="https://eln.example.org",
elabftw_api_key="labvoice-secret-key-do-not-leak",
elabftw_timeout=5.0,
elabftw_retries=0,
labvoice_db_path=tmp_path / "journal.sqlite",
)
@pytest.fixture
def client() -> FakeElabftwClient:
"""Standard scenario: experiment 123 (steps 9, 10) and ethanol item 12."""
return FakeElabftwClient(experiments=[make_experiment()], items=[make_item()])
@pytest.fixture
def resolver_stub() -> StubResolver:
"""ethanol_absolute → item 12; other keys raise clarification."""
return StubResolver(mappings={"ethanol_absolute": 12})
@pytest.fixture
async def journal(settings) -> SagaJournal:
journal = SagaJournal(settings.labvoice_db_path)
await journal.connect()
yield journal
await journal.close()
@pytest.fixture
def protocol(client, resolver_stub, journal, settings) -> ProtocolWorkflow:
return ProtocolWorkflow(
client=client, resolver=resolver_stub, journal=journal, settings=settings
)
@pytest.fixture
def inventory(client, resolver_stub, journal, settings) -> InventoryWorkflow:
return InventoryWorkflow(
client=client, resolver=resolver_stub, journal=journal, settings=settings
)
@pytest.fixture
def setup(client, resolver_stub, settings) -> SetupWorkflow:
return SetupWorkflow(client=client, resolver=resolver_stub, settings=settings)
@pytest.fixture
def templates_service(client, settings) -> TemplateService:
return TemplateService(client=client, settings=settings)
@pytest.fixture
async def real_resolver(settings):
"""Real ResourceResolver over a temp DB — resolver/mapping-store tests only."""
resolver = ResourceResolver(settings)
if hasattr(resolver, "connect"):
await resolver.connect()
yield resolver
if hasattr(resolver, "close"):
await resolver.close()
@pytest.fixture
def service_container(client, resolver_stub, journal, settings, templates_service) -> dict:
"""Fake ``build_services`` container (keys per scaffold comment)."""
return {
"client": client,
"resolver": resolver_stub,
"journal": journal,
"protocol": ProtocolWorkflow(
client=client, resolver=resolver_stub, journal=journal, settings=settings
),
"inventory": InventoryWorkflow(
client=client, resolver=resolver_stub, journal=journal, settings=settings
),
"setup": SetupWorkflow(client=client, resolver=resolver_stub, settings=settings),
"templates": templates_service,
}
@pytest.fixture
def patch_build_services(monkeypatch, service_container):
"""Intercept ``build_services`` wherever a transport layer imports it."""
def _patch():
import mcp_server.rest as rest_module
import mcp_server.server as server_module
import mcp_server.tools as tools_module
for module in (server_module, rest_module, tools_module):
monkeypatch.setattr(
module,
"build_services",
lambda _settings, _container=service_container: _container,
raising=False,
)
return service_container
return _patch