From a341d7a7c88ffb69d7f245498748d65d5b6155dc Mon Sep 17 00:00:00 2001 From: Ed_ Date: Tue, 9 Jun 2026 10:37:14 -0400 Subject: [PATCH] test: ensure sentence-transformers is in test env + conftest gate --- pyproject.toml | 1 + tests/conftest.py | 47 +++++++++++++++++ tests/test_rag_engine_ready_status_bug.py | 58 +++++++++------------ tests/test_required_test_dependencies.py | 62 +++++++++++++++++++++++ 4 files changed, 135 insertions(+), 33 deletions(-) create mode 100644 tests/test_required_test_dependencies.py diff --git a/pyproject.toml b/pyproject.toml index 4d770e4f..838a587b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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] diff --git a/tests/conftest.py b/tests/conftest.py index 24b7dcdf..faf234b1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -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) diff --git a/tests/test_rag_engine_ready_status_bug.py b/tests/test_rag_engine_ready_status_bug.py index 56790938..211e3d7b 100644 --- a/tests/test_rag_engine_ready_status_bug.py +++ b/tests/test_rag_engine_ready_status_bug.py @@ -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=".") diff --git a/tests/test_required_test_dependencies.py b/tests/test_required_test_dependencies.py new file mode 100644 index 00000000..fca16e77 --- /dev/null +++ b/tests/test_required_test_dependencies.py @@ -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