Private
Public Access
test: ensure sentence-transformers is in test env + conftest gate
This commit is contained in:
@@ -37,6 +37,7 @@ dev = [
|
|||||||
"pytest-cov>=7.0.0",
|
"pytest-cov>=7.0.0",
|
||||||
"pytest-asyncio>=0.25.3",
|
"pytest-asyncio>=0.25.3",
|
||||||
"pytest-xdist>=3.6.0",
|
"pytest-xdist>=3.6.0",
|
||||||
|
"sentence-transformers~=5.4.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
[tool.pytest.ini_options]
|
||||||
|
|||||||
@@ -110,12 +110,59 @@ if not _warmup_app_controller.wait_for_warmup(timeout=60.0):
|
|||||||
import threading
|
import threading
|
||||||
_pytest_finished_event: threading.Event = threading.Event()
|
_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:
|
def pytest_terminal_summary(terminalreporter: object, exitstatus: int, config: object) -> None:
|
||||||
_pytest_finished_event.set()
|
_pytest_finished_event.set()
|
||||||
|
|
||||||
def pytest_unconfigure(config: object) -> None:
|
def pytest_unconfigure(config: object) -> None:
|
||||||
_pytest_finished_event.set()
|
_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:
|
def _smart_watchdog_exit() -> None:
|
||||||
if not _pytest_finished_event.wait(timeout=300.0):
|
if not _pytest_finished_event.wait(timeout=300.0):
|
||||||
os._exit(2)
|
os._exit(2)
|
||||||
|
|||||||
@@ -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:
|
def test_rag_engine_init_with_failing_local_embedding_leaves_engine_broken() -> None:
|
||||||
"""RAGEngine with local embedding + sentence-transformers missing
|
"""When the LocalEmbeddingProvider ctor fails (e.g. sentence-transformers
|
||||||
ends up in a broken state: self.embedding_provider is None.
|
raises), the RAGEngine ctor itself raises ImportError. The sync path
|
||||||
The sync path should detect this and NOT set status to 'ready'.
|
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
|
This test verifies the precondition: that RAGEngine.__init__ actually
|
||||||
batch failure: in batch context (after the 4 sims run), the sync
|
raises ImportError when the local embedding provider can't be built,
|
||||||
reports 'ready' but the RAG engine is broken. The AI attempts to
|
rather than silently swallowing the error and leaving a broken engine.
|
||||||
use it, embedding fails silently, and the RAG context never
|
|
||||||
appears in the user's history.
|
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
|
from src import models
|
||||||
# Test that the actual RAGEngine (real one) leaves embedding_provider
|
from src import rag_engine
|
||||||
# as None when the import fails
|
config = models.RAGConfig(
|
||||||
result = subprocess.run(
|
enabled=True,
|
||||||
["uv", "run", "python", "-c", """
|
embedding_provider="local",
|
||||||
import sys
|
vector_store=models.VectorStoreConfig(provider="chroma", collection_name="t"),
|
||||||
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'
|
|
||||||
)
|
)
|
||||||
print("STDOUT:", result.stdout)
|
with patch("src.rag_engine._get_sentence_transformers",
|
||||||
print("STDERR:", result.stderr[:200])
|
side_effect=ImportError("Local RAG embeddings require sentence-transformers.")):
|
||||||
assert "ImportError" in result.stdout
|
with pytest.raises(ImportError, match="sentence-transformers"):
|
||||||
assert "sentence-transformers" in result.stdout
|
rag_engine.RAGEngine(config, base_dir=".")
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user