import tempfile import pytest import os from config import config # Ensure importing database at test-collection time doesn't try to open the real # application DB. Point the app config to a temporary DB file under the # system tempdir so the module-level MotionDatabase() in database.py can # initialize without conflicting with a running instance. _tmp_dir = tempfile.mkdtemp(prefix="tests_db_") config.DATABASE_PATH = os.path.join(_tmp_dir, "motions.db") class FakeEmbedder: """Real callable that returns deterministic embeddings. No network calls. Raises RuntimeError for any call where `fail_indices` are triggered. fail_indices is the set of positions (0-based) within the texts batch passed to a single __call__ invocation. """ def __init__(self, fail_indices=None, vector_size=8): self.fail_indices = set(fail_indices or []) self.vector_size = vector_size self.call_count = 0 self.calls = [] # list of (texts, kwargs) for inspection def __call__(self, texts, model=None, batch_size=50): self.call_count += 1 self.calls.append((list(texts), {"model": model, "batch_size": batch_size})) results = [] for i, text in enumerate(texts): if i in self.fail_indices: raise RuntimeError( f"Simulated embedding failure for index {i}: {text!r}" ) results.append([0.1 * (i + 1)] * self.vector_size) return results @pytest.fixture def mem_db(tmp_path): """In-memory MotionDatabase with full schema. No filesystem side effects. MotionDatabase(':memory:') may raise when os.path.dirname(':memory:') is empty. Try in-memory first, fall back to a tmp file if that fails. """ from database import ( MotionDatabase, ) # lazy import — database module not imported at module level try: db = MotionDatabase(":memory:") except Exception: db = MotionDatabase(str(tmp_path / "test.db")) yield db @pytest.fixture def fake_embedder(): """FakeEmbedder with no failures by default.""" return FakeEmbedder() # Load test fixtures from the utils package so pytest can discover them. pytest_plugins = ["tests.utils.migration_fixtures"] @pytest.fixture def tmp_duckdb_path(tmp_path): p = tmp_path / "test.db" return str(p) @pytest.fixture def tmp_duckdb_conn(tmp_duckdb_path): # Import duckdb lazily so running pytest doesn't fail on machines # where duckdb is not installed (CI / contributor machines that don't # need the duckdb-based fixtures). If duckdb is missing, skip this # fixture at runtime when it's requested. try: import duckdb except Exception: pytest.skip("duckdb not installed, skipping duckdb fixtures") conn = duckdb.connect(database=tmp_duckdb_path) yield conn try: conn.close() except Exception: pass @pytest.fixture def monkeypatch_ai_provider(monkeypatch): """Patch ai_provider.get_embedding to return deterministic 16-dim vector.""" import ai_provider fake = [0.01] * 16 monkeypatch.setattr(ai_provider, "get_embedding", lambda text, model=None: fake) return fake @pytest.fixture def mock_odata_client(monkeypatch): """ Patch requests.Session.get for OData calls. Returns a configurable mock — set mock_odata_client.response to override. """ import requests from unittest.mock import MagicMock mock_response = MagicMock() mock_response.raise_for_status.return_value = None mock_response.json.return_value = {"value": []} class MockSession: response = mock_response def get(self, *args, **kwargs): return self.response monkeypatch.setattr(requests, "Session", MockSession) return mock_response