Private
Public Access
0
0

test: ensure sentence-transformers is in test env + conftest gate

This commit is contained in:
2026-06-09 10:37:14 -04:00
parent 2148e79a1c
commit a341d7a7c8
4 changed files with 135 additions and 33 deletions
+1
View File
@@ -37,6 +37,7 @@ dev = [
"pytest-cov>=7.0.0",
"pytest-asyncio>=0.25.3",
"pytest-xdist>=3.6.0",
"sentence-transformers~=5.4.1",
]
[tool.pytest.ini_options]
+47
View File
@@ -110,12 +110,59 @@ if not _warmup_app_controller.wait_for_warmup(timeout=60.0):
import threading
_pytest_finished_event: threading.Event = threading.Event()
def pytest_configure(config: object) -> None:
"""
Pytest session-start hook. Runs required-dependency check before any
test is collected so the user sees a clear, actionable error if the
test environment is incomplete.
[C: tests/test_required_test_dependencies.py:test_check_succeeds_when_deps_present, tests/test_required_test_dependencies.py:test_check_raises_on_missing_sentence_transformers]
"""
_check_required_test_dependencies()
def pytest_terminal_summary(terminalreporter: object, exitstatus: int, config: object) -> None:
_pytest_finished_event.set()
def pytest_unconfigure(config: object) -> None:
_pytest_finished_event.set()
_REQUIRED_TEST_IMPORTS: list[tuple[str, str]] = [
("sentence_transformers", "sentence-transformers"),
]
def _check_required_test_dependencies() -> None:
"""
Verify that all packages required by the integration test suite are
importable. Raises pytest.UsageError with a clear fix command if a
required package is missing.
This gate is a regression test for the 2026-06-09 incident where
sentence-transformers was in [project.optional-dependencies] but the
test suite requires it unconditionally. A fresh `uv sync` (without
--extra local-rag) produced a confusing "rag_status = error: ...
Install with manual_slop[local-rag]" failure inside the live_gui
subprocess. This gate fails fast at session start with the exact
fix command instead.
To add a new required test dep, append its import to _REQUIRED_TEST_IMPORTS.
[C: tests/test_required_test_dependencies.py:test_check_succeeds_when_deps_present, tests/test_required_test_dependencies.py:test_check_raises_on_missing_sentence_transformers]
"""
missing: list[str] = []
for module_name, package_name in _REQUIRED_TEST_IMPORTS:
try:
__import__(module_name)
except ImportError:
missing.append(package_name)
if missing:
msg = (
"Required test dependencies are missing from the venv:\n"
f" - {', '.join(missing)}\n\n"
"Fix: uv sync --extra local-rag\n"
"Or, if pyproject.toml already lists the dep in [dependency-groups].dev:\n"
" uv sync\n"
)
raise pytest.UsageError(msg)
def _smart_watchdog_exit() -> None:
if not _pytest_finished_event.wait(timeout=300.0):
os._exit(2)
+25 -33
View File
@@ -101,39 +101,31 @@ def test_rag_status_remains_error_after_sync_failure() -> None:
def test_rag_engine_init_with_failing_local_embedding_leaves_engine_broken() -> None:
"""RAGEngine with local embedding + sentence-transformers missing
ends up in a broken state: self.embedding_provider is None.
The sync path should detect this and NOT set status to 'ready'.
"""When the LocalEmbeddingProvider ctor fails (e.g. sentence-transformers
raises), the RAGEngine ctor itself raises ImportError. The sync path
catches this and sets rag_status to 'error: ...' (the existing
test_rag_status_remains_error_after_sync_failure covers this).
This is the actual bug surfaced in the test_rag_phase4_final_verify
batch failure: in batch context (after the 4 sims run), the sync
reports 'ready' but the RAG engine is broken. The AI attempts to
use it, embedding fails silently, and the RAG context never
appears in the user's history.
This test verifies the precondition: that RAGEngine.__init__ actually
raises ImportError when the local embedding provider can't be built,
rather than silently swallowing the error and leaving a broken engine.
The 2026-06-08 RAG batch failure root-cause analysis showed the
failure mode was NOT "engine is created with embedding_provider=None"
(which was the original test docstring's claim) — the constructor
RAISES. The actual bug was in the sync path's fallback to 'ready'
status, which test_rag_status_remains_error_after_sync_failure
verifies. This test is the lower-level sanity check that the
precondition for the sync-path test is real.
"""
import subprocess
# Test that the actual RAGEngine (real one) leaves embedding_provider
# as None when the import fails
result = subprocess.run(
["uv", "run", "python", "-c", """
import sys
sys.path.insert(0, '.')
from src import models, rag_engine
config = models.RAGConfig(
enabled=True,
embedding_provider='local',
vector_store=models.VectorStoreConfig(provider='chroma', collection_name='t')
)
try:
engine = rag_engine.RAGEngine(config, base_dir='.')
print('engine.embedding_provider:', engine.embedding_provider)
print('engine.collection:', engine.collection)
except ImportError as e:
print('ImportError:', str(e)[:60])
"""],
capture_output=True, text=True, cwd='C:\\\\projects\\\\manual_slop'
from src import models
from src import rag_engine
config = models.RAGConfig(
enabled=True,
embedding_provider="local",
vector_store=models.VectorStoreConfig(provider="chroma", collection_name="t"),
)
print("STDOUT:", result.stdout)
print("STDERR:", result.stderr[:200])
assert "ImportError" in result.stdout
assert "sentence-transformers" in result.stdout
with patch("src.rag_engine._get_sentence_transformers",
side_effect=ImportError("Local RAG embeddings require sentence-transformers.")):
with pytest.raises(ImportError, match="sentence-transformers"):
rag_engine.RAGEngine(config, base_dir=".")
+62
View File
@@ -0,0 +1,62 @@
"""
Test that the conftest gate catches missing required test dependencies
and fails fast with a clear error message.
This is a TDD test for the regression we just hit: sentence-transformers
was in [project.optional-dependencies] but the test suite requires it
unconditionally. A fresh `uv sync` (without --extra local-rag) produced
a confusing "rag_status = error: ... Install with manual_slop[local-rag]"
failure. The gate prevents this by failing at session start with the
exact fix command.
The check itself lives in conftest._check_required_test_dependencies()
so it can be unit-tested here without a real conftest import.
"""
import sys
import pytest
def test_check_succeeds_when_deps_present() -> None:
"""
If sentence_transformers is importable, the gate is a no-op.
This is the case for the test session that actually runs (sentence-
transformers is installed via `uv sync --extra local-rag`).
"""
from tests.conftest import _check_required_test_dependencies
_check_required_test_dependencies()
def test_check_raises_on_missing_sentence_transformers(monkeypatch) -> None:
"""
Simulate sentence_transformers not being installed. The gate must
raise pytest.UsageError with a clear, actionable error message that
tells the user exactly how to fix the issue.
"""
# Block the import
monkeypatch.setitem(sys.modules, "sentence_transformers", None)
# Remove any cached successful import
for mod_name in list(sys.modules):
if mod_name == "sentence_transformers" or mod_name.startswith("sentence_transformers."):
del sys.modules[mod_name]
# Force ImportError on import
import builtins
original_import = builtins.__import__
def _blocking_import(name, *args, **kwargs):
if name == "sentence_transformers" or name.startswith("sentence_transformers."):
raise ImportError(f"No module named {name!r} (simulated)")
return original_import(name, *args, **kwargs)
monkeypatch.setattr(builtins, "__import__", _blocking_import)
# Also need to invalidate the conftest's already-cached import
from tests import conftest
# Clear the module so it re-imports and the gate re-runs
# Note: the conftest's _check function does its own import so this
# is just a sanity check that the function is callable.
from tests.conftest import _check_required_test_dependencies
with pytest.raises(pytest.UsageError) as exc_info:
_check_required_test_dependencies()
msg = str(exc_info.value)
assert "sentence-transformers" in msg
assert "uv sync" in msg
assert "local-rag" in msg