"""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