You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
124 lines
3.8 KiB
124 lines
3.8 KiB
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
|
|
|