|
| 1 | +"""Shared fixtures for the MCP test suite. |
| 2 | +
|
| 3 | +Every per-tool integration test from T4 onward reuses the |
| 4 | +``indexed_fixture`` fixture below: it indexes ``fixtures/sample_project`` |
| 5 | +into a uniquely named FalkorDB graph once per test session and yields a |
| 6 | +descriptor (project name, branch, graph name) the test can pass straight |
| 7 | +to the MCP tool under test. |
| 8 | +
|
| 9 | +The integration fixture is opt-in — it requires a reachable FalkorDB |
| 10 | +(see ``api/graph.py``) and the optional language analyzers. Tests that |
| 11 | +only need the static contract (counts, named callers / callees / paths) |
| 12 | +can depend on ``expected_contract`` alone, which is pure-Python and |
| 13 | +always available. |
| 14 | +""" |
| 15 | + |
| 16 | +from __future__ import annotations |
| 17 | + |
| 18 | +import os |
| 19 | +import uuid |
| 20 | +from dataclasses import dataclass |
| 21 | +from pathlib import Path |
| 22 | +from typing import Any |
| 23 | + |
| 24 | +import pytest |
| 25 | +import yaml |
| 26 | + |
| 27 | + |
| 28 | +FIXTURE_DIR = Path(__file__).parent / "fixtures" |
| 29 | +SAMPLE_PROJECT = FIXTURE_DIR / "sample_project" |
| 30 | +EXPECTED_PATH = FIXTURE_DIR / "expected.yaml" |
| 31 | + |
| 32 | + |
| 33 | +# --------------------------------------------------------------------------- |
| 34 | +# Pure-Python contract (no FalkorDB required) |
| 35 | +# --------------------------------------------------------------------------- |
| 36 | + |
| 37 | + |
| 38 | +@dataclass(frozen=True) |
| 39 | +class IndexedFixture: |
| 40 | + """Descriptor for an indexed fixture graph.""" |
| 41 | + |
| 42 | + project: str |
| 43 | + branch: str |
| 44 | + graph_name: str |
| 45 | + path: Path |
| 46 | + |
| 47 | + |
| 48 | +@pytest.fixture(scope="session") |
| 49 | +def expected_contract() -> dict[str, Any]: |
| 50 | + """Load ``fixtures/expected.yaml`` once per session.""" |
| 51 | + with EXPECTED_PATH.open() as fh: |
| 52 | + return yaml.safe_load(fh) |
| 53 | + |
| 54 | + |
| 55 | +@pytest.fixture(scope="session") |
| 56 | +def sample_project_path() -> Path: |
| 57 | + """Filesystem path to the fixture project.""" |
| 58 | + return SAMPLE_PROJECT |
| 59 | + |
| 60 | + |
| 61 | +# --------------------------------------------------------------------------- |
| 62 | +# Integration fixture — indexes into a real FalkorDB |
| 63 | +# --------------------------------------------------------------------------- |
| 64 | + |
| 65 | + |
| 66 | +def _falkordb_reachable() -> bool: |
| 67 | + """Cheap probe so the integration fixture can self-skip in dev.""" |
| 68 | + try: |
| 69 | + import socket |
| 70 | + |
| 71 | + host = os.getenv("FALKORDB_HOST", "localhost") |
| 72 | + port = int(os.getenv("FALKORDB_PORT", 6379)) |
| 73 | + with socket.create_connection((host, port), timeout=1): |
| 74 | + return True |
| 75 | + except OSError: |
| 76 | + return False |
| 77 | + |
| 78 | + |
| 79 | +@pytest.fixture(scope="session") |
| 80 | +def indexed_fixture(sample_project_path: Path) -> IndexedFixture: |
| 81 | + """Index the sample project into a unique per-session graph. |
| 82 | +
|
| 83 | + Each test session creates a new graph named |
| 84 | + ``code:sample_project:test-<uuid>`` so parallel CI shards never |
| 85 | + contend on the same graph. The graph is intentionally **not** |
| 86 | + cleaned up — short-lived CI runners discard the FalkorDB volume, |
| 87 | + and keeping it around helps post-mortem debugging on developer |
| 88 | + machines. |
| 89 | +
|
| 90 | + Uses :class:`api.analyzers.SourceAnalyzer` directly (instead of |
| 91 | + ``Project.from_local_repository``) so the fixture doesn't need to |
| 92 | + be a git repository — analyzing a plain directory is exactly the |
| 93 | + code path the ``index_repo`` MCP tool exercises for non-git |
| 94 | + folders. |
| 95 | + """ |
| 96 | + |
| 97 | + if not _falkordb_reachable(): |
| 98 | + pytest.skip("FalkorDB not reachable on $FALKORDB_HOST:$FALKORDB_PORT") |
| 99 | + |
| 100 | + # Import locally so unit-only tests don't pay the import cost. |
| 101 | + from api.analyzers.source_analyzer import SourceAnalyzer |
| 102 | + from api.graph import Graph |
| 103 | + |
| 104 | + project_name = sample_project_path.name # "sample_project" |
| 105 | + branch = f"test-{uuid.uuid4().hex[:8]}" |
| 106 | + graph = Graph(project_name, branch=branch) |
| 107 | + |
| 108 | + analyzer = SourceAnalyzer() |
| 109 | + analyzer.analyze_local_folder(str(sample_project_path), graph) |
| 110 | + |
| 111 | + return IndexedFixture( |
| 112 | + project=project_name, |
| 113 | + branch=branch, |
| 114 | + graph_name=graph.name, |
| 115 | + path=sample_project_path, |
| 116 | + ) |
0 commit comments