Initial version
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user