From f1b161ac00fa60b4fb248d1f7ddf2fd41229a88e Mon Sep 17 00:00:00 2001 From: spelech <28486500+spelech@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:23:03 +0000 Subject: [PATCH 1/4] fix(vector-store): fail remote connection tests on unreachable endpoints - Prevent automatic local fallback in Qdrant and Chroma vector stores when explicit remote targets fail, unless opt-in allow_fallback=True is passed. - Report configured_mode, effective_mode, fallback_active, and fallback_reason in vector store stats, config, and health checks. - Return explicit error messages on POST /admin/api/vector-store/test when remote Qdrant or Chroma endpoints cannot be reached. - Add test coverage for remote connection failures and opt-in degraded fallbacks. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- REQUIREMENTS.md | 14 ++++-- app/services/vector_store/chroma_store.py | 61 +++++++++++++++++------ app/services/vector_store/manager.py | 11 +++- app/services/vector_store/qdrant_store.py | 60 ++++++++++++++++------ tests/backend/test_api_vector_store.py | 34 +++++++++++++ tests/backend/test_vector_store_chroma.py | 28 ++++++++++- tests/backend/test_vector_store_qdrant.py | 32 +++++++++++- 7 files changed, 201 insertions(+), 39 deletions(-) diff --git a/REQUIREMENTS.md b/REQUIREMENTS.md index 59d9c13..9e64290 100644 --- a/REQUIREMENTS.md +++ b/REQUIREMENTS.md @@ -2,7 +2,7 @@ > **Note:** This document is automatically generated and verified against the live test suite by `scripts/generate_requirements.py` and `tests/backend/test_requirements_sync.py`. -**Test Verification Baseline:** **1052 Automated Tests** (699 Pytest Backend + 303 Vitest Frontend + 50 Playwright E2E). +**Test Verification Baseline:** **1058 Automated Tests** (705 Pytest Backend + 303 Vitest Frontend + 50 Playwright E2E). --- @@ -431,13 +431,15 @@ classDiagram - `test_api_embedding_settings` - `test_api_embedding_settings_errors` -#### `tests/backend/test_api_vector_store.py` (14 tests) +#### `tests/backend/test_api_vector_store.py` (16 tests) - `test_api_get_vector_store_success` - _Test retrieving active vector store configuration and stats._ - `test_api_get_vector_store_error` - _Test GET /admin/api/vector-store error handling._ - `test_api_test_vector_store_valid_embedded` - _Test dry-run validation with valid embedded Qdrant configuration._ - `test_api_test_vector_store_valid_chroma` - _Test dry-run validation with valid embedded Chroma configuration._ - `test_api_test_vector_store_invalid_provider` - _Test dry-run validation with unsupported provider._ - `test_api_test_vector_store_remote_missing_url` - _Test dry-run validation with remote mode but missing URL._ +- `test_api_test_vector_store_unreachable_remote_qdrant` - _Test POST /admin/api/vector-store/test fails for unreachable remote Qdrant._ +- `test_api_test_vector_store_unreachable_remote_chroma` - _Test POST /admin/api/vector-store/test fails for unreachable remote Chroma._ - `test_api_test_vector_store_exception` - _Test dry-run validation when unexpected exception occurs._ - `test_api_switch_vector_store_success` - _Test switching vector store backend and triggering re-indexing._ - `test_api_switch_vector_store_invalid_provider` - _Test switching to an unsupported provider returns 400._ @@ -738,10 +740,11 @@ Asserts both Markdown and PDF docs are matched under doc_type='doc'._ - `test_vector_document_validation` - _Verify required field validation in VectorDocument._ - `test_vector_search_result_creation_and_payload` - _Verify VectorSearchResult creation and payload access._ -#### `tests/backend/test_vector_store_chroma.py` (34 tests) +#### `tests/backend/test_vector_store_chroma.py` (36 tests) - `TestChromaVectorStoreInit::test_init_in_memory` - `TestChromaVectorStoreInit::test_init_persistent_disk` - `TestChromaVectorStoreInit::test_init_remote_success` +- `TestChromaVectorStoreInit::test_init_remote_unreachable_raises_error` - `TestChromaVectorStoreInit::test_init_remote_fallback_to_persistent_on_error` - `TestChromaVectorStoreInit::test_custom_injected_client` - `TestChromaVectorStoreOperations::test_ensure_collection` @@ -759,6 +762,7 @@ Asserts both Markdown and PDF docs are matched under doc_type='doc'._ - `test_init_in_memory` - `test_init_persistent_disk` - `test_init_remote_success` +- `test_init_remote_unreachable_raises_error` - `test_init_remote_fallback_to_persistent_on_error` - `test_custom_injected_client` - `test_ensure_collection` @@ -814,9 +818,10 @@ Asserts both Markdown and PDF docs are matched under doc_type='doc'._ - `test_test_connection_active_embedded` - _Verify test_connection succeeds on active embedded Qdrant store without file lock conflict._ - `test_switch_same_embedded_directory` - _Verify switch_vector_store succeeds when switching collection on the same embedded Qdrant directory._ -#### `tests/backend/test_vector_store_qdrant.py` (40 tests) +#### `tests/backend/test_vector_store_qdrant.py` (42 tests) - `TestQdrantVectorStoreInit::test_init_in_memory_or_embedded` - `TestQdrantVectorStoreInit::test_init_remote_success` +- `TestQdrantVectorStoreInit::test_init_remote_unreachable_raises_error` - `TestQdrantVectorStoreInit::test_init_remote_fallback_to_embedded_on_connection_error` - `TestQdrantVectorStoreInit::test_custom_injected_client` - `TestQdrantVectorStoreOperations::test_ensure_collection_recreates_on_schema_mismatch` @@ -837,6 +842,7 @@ Asserts both Markdown and PDF docs are matched under doc_type='doc'._ - `TestQdrantVectorStoreOperations::test_get_stats_and_health_check` - `test_init_in_memory_or_embedded` - `test_init_remote_success` +- `test_init_remote_unreachable_raises_error` - `test_init_remote_fallback_to_embedded_on_connection_error` - `test_custom_injected_client` - `test_ensure_collection_recreates_on_schema_mismatch` diff --git a/app/services/vector_store/chroma_store.py b/app/services/vector_store/chroma_store.py index 33d4929..781411c 100644 --- a/app/services/vector_store/chroma_store.py +++ b/app/services/vector_store/chroma_store.py @@ -75,6 +75,7 @@ def __init__( url: Optional[str] = None, client: Optional[Any] = None, auto_init: bool = True, + allow_fallback: Optional[bool] = None, ): self.collection_name = collection_name or os.getenv("COLLECTION_NAME", "knowledge_rag_v1") self.collection = None @@ -94,17 +95,25 @@ def __init__( target_ssl = ssl if ssl is not None else os.getenv("CHROMA_SSL", "false").lower() in ("true", "1", "yes") target_storage = storage_path or get_default_chroma_storage_path() - if prefer_remote is None: prefer_remote = os.getenv("CHROMA_PREFER_REMOTE", "false").lower() in ("true", "1", "yes") if host is not None: prefer_remote = True + if allow_fallback is None: + allow_fallback = os.getenv("CHROMA_ALLOW_FALLBACK", "false").lower() in ("true", "1", "yes") + + self.fallback_active = False + self.fallback_reason = None + if client is not None: self.client = client + self.configured_mode = "custom" + self.effective_mode = "custom" self.mode = "custom" self.location = "injected_client" elif prefer_remote: + self.configured_mode = "remote" try: remote_client = chromadb.HttpClient( host=target_host, @@ -115,32 +124,45 @@ def __init__( remote_client.heartbeat() self.client = remote_client self.mode = "remote" + self.effective_mode = "remote" self.location = f"{'https' if target_ssl else 'http'}://{target_host}:{target_port}" logger.info(f"Connected to remote Chroma server at {self.location}") except Exception as e: + if not allow_fallback: + logger.error(f"Failed to connect to remote Chroma at {target_host}:{target_port}: {e}") + raise ConnectionError(f"Failed to connect to remote Chroma at {target_host}:{target_port}: {e}") from e + + self.fallback_active = True + self.fallback_reason = str(e) logger.warning( f"Failed to connect to remote Chroma at {target_host}:{target_port}: {e}. " - f"Falling back to persistent disk storage at {target_storage}" + f"Falling back to persistent disk storage at {target_storage} (opt-in fallback active)" ) if target_storage == ":memory:": self.client = chromadb.EphemeralClient() self.mode = "memory" + self.effective_mode = "memory" self.location = target_storage else: clean_storage = validate_chroma_storage_path(target_storage) os.makedirs(clean_storage, exist_ok=True) self.client = chromadb.PersistentClient(path=clean_storage) self.mode = "persistent" + self.effective_mode = "persistent" self.location = clean_storage else: if target_storage == ":memory:": self.client = chromadb.EphemeralClient() + self.configured_mode = "memory" + self.effective_mode = "memory" self.mode = "memory" self.location = target_storage else: clean_storage = validate_chroma_storage_path(target_storage) os.makedirs(clean_storage, exist_ok=True) self.client = chromadb.PersistentClient(path=clean_storage) + self.configured_mode = "persistent" + self.effective_mode = "persistent" self.mode = "persistent" self.location = clean_storage logger.info(f"Initialized local Chroma client at {self.location}") @@ -331,31 +353,39 @@ def search( def get_stats(self) -> Dict[str, Any]: """Returns collection info including point count, backend, and mode.""" try: + base_stats = { + "backend": "chroma", + "mode": self.mode, + "configured_mode": getattr(self, "configured_mode", self.mode), + "effective_mode": getattr(self, "effective_mode", self.mode), + "fallback_active": getattr(self, "fallback_active", False), + "fallback_reason": getattr(self, "fallback_reason", None), + "collection_name": self.collection_name, + "location": self.location, + } if self.collection is None: - return { - "backend": "chroma", - "mode": self.mode, - "collection_name": self.collection_name, + base_stats.update({ "exists": False, "points_count": 0, "status": "not_initialized", - "location": self.location, - } + }) + return base_stats count = self.collection.count() - return { - "backend": "chroma", - "mode": self.mode, - "collection_name": self.collection_name, + base_stats.update({ "exists": True, "points_count": count, "status": "ok", - "location": self.location, - } + }) + return base_stats except Exception as e: logger.error(f"Error getting stats for Chroma collection '{self.collection_name}': {e}") return { "backend": "chroma", "mode": self.mode, + "configured_mode": getattr(self, "configured_mode", self.mode), + "effective_mode": getattr(self, "effective_mode", self.mode), + "fallback_active": getattr(self, "fallback_active", False), + "fallback_reason": getattr(self, "fallback_reason", None), "collection_name": self.collection_name, "exists": False, "error": str(e), @@ -367,7 +397,8 @@ def health_check(self) -> Tuple[bool, str]: try: hb = self.client.heartbeat() count = self.collection.count() if self.collection is not None else 0 - return True, f"Chroma ({self.mode} @ {self.location}) is healthy; heartbeat: {hb}, collection '{self.collection_name}' count: {count}" + fallback_note = f" [DEGRADED: Fallback to {self.effective_mode} from {self.configured_mode} active; reason: {self.fallback_reason}]" if getattr(self, "fallback_active", False) else "" + return True, f"Chroma ({self.effective_mode} @ {self.location}) is healthy; heartbeat: {hb}, collection '{self.collection_name}' count: {count}{fallback_note}" except Exception as e: logger.warning(f"Chroma ({self.mode}) health check failed: {e}") return False, f"Chroma ({self.mode}) health check failed." diff --git a/app/services/vector_store/manager.py b/app/services/vector_store/manager.py index 94f7abc..6ef81b8 100644 --- a/app/services/vector_store/manager.py +++ b/app/services/vector_store/manager.py @@ -146,9 +146,16 @@ def get_vector_store_config(cls) -> Dict[str, Any]: health_msg = "Error connecting to vector store." stats = {"error": "Error retrieving vector store statistics."} + configured_mode = cfg["mode"] + effective_mode = stats.get("effective_mode", stats.get("mode", configured_mode)) + fallback_active = stats.get("fallback_active", False) + return { "provider": cfg["provider"], - "mode": cfg["mode"], + "mode": configured_mode, + "configured_mode": configured_mode, + "effective_mode": effective_mode, + "fallback_active": fallback_active, "storage_path": cfg["storage_path"], "url": cfg["url"], "collection": cfg["collection"], @@ -327,7 +334,7 @@ def test_connection( return is_healthy, health_msg except Exception as e: logger.error(f"Connection test failed for {prov}: {e}") - return False, f"Connection test failed for {prov}." + return False, f"Connection test failed for {prov}: {e}" finally: if test_store is not None: try: diff --git a/app/services/vector_store/qdrant_store.py b/app/services/vector_store/qdrant_store.py index 0307625..b6624e9 100644 --- a/app/services/vector_store/qdrant_store.py +++ b/app/services/vector_store/qdrant_store.py @@ -44,6 +44,7 @@ def __init__( timeout: float = 5.0, client: Optional[QdrantClient] = None, auto_init: bool = True, + allow_fallback: Optional[bool] = None, ): self.collection_name = collection_name or os.getenv("COLLECTION_NAME", "knowledge_rag_v1") self.timeout = timeout @@ -54,11 +55,20 @@ def __init__( if prefer_remote is None: prefer_remote = os.getenv("QDRANT_PREFER_REMOTE", "true").lower() in ("true", "1", "yes") + if allow_fallback is None: + allow_fallback = os.getenv("QDRANT_ALLOW_FALLBACK", "false").lower() in ("true", "1", "yes") + + self.fallback_active = False + self.fallback_reason = None + if client is not None: self.client = client + self.configured_mode = "custom" + self.effective_mode = "custom" self.mode = "custom" self.location = "injected_client" elif prefer_remote and (storage_path is None or url is not None): + self.configured_mode = "remote" target_url = url or url_env target_storage = storage_path or storage_path_env try: @@ -66,33 +76,46 @@ def __init__( remote_client.get_collections() self.client = remote_client self.mode = "remote" + self.effective_mode = "remote" self.location = target_url logger.info(f"Connected to remote Qdrant server at {target_url}") except Exception as e: + if not allow_fallback: + logger.error(f"Failed to connect to remote Qdrant at {target_url}: {e}") + raise ConnectionError(f"Failed to connect to remote Qdrant at {target_url}: {e}") from e + + self.fallback_active = True + self.fallback_reason = str(e) logger.warning( f"Failed to connect to remote Qdrant at {target_url}: {e}. " - f"Falling back to embedded disk storage at {target_storage}" + f"Falling back to embedded disk storage at {target_storage} (opt-in fallback active)" ) if target_storage == ":memory:": self.client = QdrantClient(location=":memory:") self.mode = "memory" + self.effective_mode = "memory" self.location = target_storage else: clean_storage = validate_qdrant_storage_path(target_storage) os.makedirs(clean_storage, exist_ok=True) self.client = QdrantClient(path=clean_storage) self.mode = "embedded" + self.effective_mode = "embedded" self.location = clean_storage else: target_storage = storage_path or storage_path_env if target_storage == ":memory:": self.client = QdrantClient(location=":memory:") + self.configured_mode = "memory" + self.effective_mode = "memory" self.mode = "memory" self.location = target_storage else: clean_storage = validate_qdrant_storage_path(target_storage) os.makedirs(clean_storage, exist_ok=True) self.client = QdrantClient(path=clean_storage) + self.configured_mode = "embedded" + self.effective_mode = "embedded" self.mode = "embedded" self.location = clean_storage logger.info(f"Initialized embedded Qdrant client at {self.location}") @@ -489,34 +512,42 @@ def search( def get_stats(self) -> Dict[str, Any]: """Returns collection info including point count, backend, and mode.""" try: + base_stats = { + "backend": "qdrant", + "mode": self.mode, + "configured_mode": getattr(self, "configured_mode", self.mode), + "effective_mode": getattr(self, "effective_mode", self.mode), + "fallback_active": getattr(self, "fallback_active", False), + "fallback_reason": getattr(self, "fallback_reason", None), + "collection_name": self.collection_name, + "location": self.location, + } if not self.client.collection_exists(self.collection_name): - return { - "backend": "qdrant", - "mode": self.mode, - "collection_name": self.collection_name, + base_stats.update({ "exists": False, "points_count": 0, "status": "not_found", - "location": self.location, - } + }) + return base_stats info = self.client.get_collection(self.collection_name) points_count = info.points_count or 0 vectors_count = getattr(info, "vectors_count", points_count) - return { - "backend": "qdrant", - "mode": self.mode, - "collection_name": self.collection_name, + base_stats.update({ "exists": True, "points_count": points_count, "vectors_count": vectors_count, "status": str(info.status), - "location": self.location, - } + }) + return base_stats except Exception as e: logger.error(f"Error getting stats for Qdrant collection '{self.collection_name}': {e}") return { "backend": "qdrant", "mode": self.mode, + "configured_mode": getattr(self, "configured_mode", self.mode), + "effective_mode": getattr(self, "effective_mode", self.mode), + "fallback_active": getattr(self, "fallback_active", False), + "fallback_reason": getattr(self, "fallback_reason", None), "collection_name": self.collection_name, "exists": False, "error": str(e), @@ -528,7 +559,8 @@ def health_check(self) -> Tuple[bool, str]: try: self.client.get_collections() exists = self.client.collection_exists(self.collection_name) - return True, f"Qdrant ({self.mode} @ {self.location}) is healthy; collection '{self.collection_name}' exists: {exists}" + fallback_note = f" [DEGRADED: Fallback to {self.effective_mode} from {self.configured_mode} active; reason: {self.fallback_reason}]" if getattr(self, "fallback_active", False) else "" + return True, f"Qdrant ({self.effective_mode} @ {self.location}) is healthy; collection '{self.collection_name}' exists: {exists}{fallback_note}" except Exception as e: logger.warning(f"Qdrant ({self.mode}) health check failed: {e}") return False, f"Qdrant ({self.mode}) health check failed." diff --git a/tests/backend/test_api_vector_store.py b/tests/backend/test_api_vector_store.py index ebe07cf..8017d9a 100644 --- a/tests/backend/test_api_vector_store.py +++ b/tests/backend/test_api_vector_store.py @@ -139,6 +139,40 @@ def test_api_test_vector_store_remote_missing_url(): assert "url" in data["message"].lower() +def test_api_test_vector_store_unreachable_remote_qdrant(setup_test_environment): + """Test POST /admin/api/vector-store/test fails for unreachable remote Qdrant.""" + payload = { + "provider": "qdrant", + "mode": "remote", + "url": "http://127.0.0.1:1", + "collection": "feasibility_unreachable" + } + res = client.post("/admin/api/vector-store/test", json=payload) + assert res.status_code == 200 + data = res.json() + assert data["success"] is False + assert data["provider"] == "qdrant" + assert data["mode"] == "remote" + assert "connection test failed" in data["message"].lower() or "failed to connect" in data["message"].lower() + + +def test_api_test_vector_store_unreachable_remote_chroma(setup_test_environment): + """Test POST /admin/api/vector-store/test fails for unreachable remote Chroma.""" + payload = { + "provider": "chroma", + "mode": "remote", + "url": "http://127.0.0.1:1", + "collection": "feasibility_unreachable" + } + res = client.post("/admin/api/vector-store/test", json=payload) + assert res.status_code == 200 + data = res.json() + assert data["success"] is False + assert data["provider"] == "chroma" + assert data["mode"] == "remote" + assert "connection test failed" in data["message"].lower() or "failed to connect" in data["message"].lower() + + def test_api_test_vector_store_exception(): """Test dry-run validation when unexpected exception occurs.""" with patch("app.services.vector_store.test_vector_store_connection", side_effect=RuntimeError("Test crash")): diff --git a/tests/backend/test_vector_store_chroma.py b/tests/backend/test_vector_store_chroma.py index 001f377..a184f8a 100644 --- a/tests/backend/test_vector_store_chroma.py +++ b/tests/backend/test_vector_store_chroma.py @@ -58,6 +58,18 @@ def test_init_remote_success(self): assert "remote-chroma" in store.location mock_client.heartbeat.assert_called_once() + def test_init_remote_unreachable_raises_error(self, tmp_path): + with patch("app.services.vector_store.chroma_store.chromadb.HttpClient", side_effect=ValueError("Could not connect")): + with pytest.raises(ConnectionError, match="Failed to connect to remote Chroma"): + ChromaVectorStore( + host="broken-chroma", + port=8000, + storage_path=str(tmp_path / "fallback_chroma"), + collection_name="test_fb_coll", + prefer_remote=True, + allow_fallback=False + ) + def test_init_remote_fallback_to_persistent_on_error(self, tmp_path): fallback_path = str(tmp_path / "fallback_chroma") coll_name = f"test_fb_{uuid.uuid4().hex[:8]}" @@ -68,13 +80,25 @@ def test_init_remote_fallback_to_persistent_on_error(self, tmp_path): port=8000, storage_path=fallback_path, collection_name=coll_name, - prefer_remote=True + prefer_remote=True, + allow_fallback=True ) - assert store.mode == "persistent" + assert store.configured_mode == "remote" + assert store.effective_mode == "persistent" + assert store.fallback_active is True assert store.location == fallback_path assert store.client is not None assert store.collection is not None + stats = store.get_stats() + assert stats["configured_mode"] == "remote" + assert stats["effective_mode"] == "persistent" + assert stats["fallback_active"] is True + + healthy, msg = store.health_check() + assert healthy is True + assert "[DEGRADED:" in msg + def test_custom_injected_client(self): custom_client = chromadb.EphemeralClient() coll_name = f"test_cust_{uuid.uuid4().hex[:8]}" diff --git a/tests/backend/test_vector_store_qdrant.py b/tests/backend/test_vector_store_qdrant.py index c073bf9..9622fd2 100644 --- a/tests/backend/test_vector_store_qdrant.py +++ b/tests/backend/test_vector_store_qdrant.py @@ -39,6 +39,22 @@ def test_init_remote_success(self): assert store.location == "http://remote-qdrant:6333" mock_client.get_collections.assert_called() + def test_init_remote_unreachable_raises_error(self, tmp_path): + def side_effect(*args, **kwargs): + if "url" in kwargs or (args and isinstance(args[0], str) and args[0].startswith("http")): + raise ConnectionError("Cannot reach remote server") + return QdrantClient(location=":memory:") + + with patch("app.services.vector_store.qdrant_store.QdrantClient", side_effect=side_effect): + with pytest.raises(ConnectionError, match="Failed to connect to remote Qdrant"): + QdrantVectorStore( + url="http://broken-remote:6333", + storage_path=str(tmp_path / "fallback_qdrant"), + collection_name="test_fallback_coll", + prefer_remote=True, + allow_fallback=False + ) + def test_init_remote_fallback_to_embedded_on_connection_error(self, tmp_path): fallback_path = str(tmp_path / "fallback_qdrant") @@ -52,12 +68,24 @@ def side_effect(*args, **kwargs): url="http://broken-remote:6333", storage_path=fallback_path, collection_name="test_fallback_coll", - prefer_remote=True + prefer_remote=True, + allow_fallback=True ) - assert store.mode in ("embedded", "memory") + assert store.configured_mode == "remote" + assert store.effective_mode in ("embedded", "memory") + assert store.fallback_active is True assert store.location == fallback_path assert store.client is not None + stats = store.get_stats() + assert stats["configured_mode"] == "remote" + assert stats["effective_mode"] == store.effective_mode + assert stats["fallback_active"] is True + + healthy, msg = store.health_check() + assert healthy is True + assert "[DEGRADED:" in msg + def test_custom_injected_client(self): custom_client = QdrantClient(location=":memory:") store = QdrantVectorStore( From ce0b3f40833dd24181f6dba5742ced4fe2994364 Mon Sep 17 00:00:00 2001 From: spelech <28486500+spelech@users.noreply.github.com> Date: Tue, 27 Oct 2026 23:30:54 +0000 Subject: [PATCH 2/4] fix(vector-store): fail remote connection tests on unreachable endpoints - Prevent automatic local fallback in Qdrant and Chroma vector stores when explicit remote targets fail, unless opt-in allow_fallback=True is passed. - Report configured_mode, effective_mode, fallback_active, and fallback_reason in vector store stats, config, and health checks. - Return explicit error messages on POST /admin/api/vector-store/test when remote Qdrant or Chroma endpoints cannot be reached. - Add test coverage for remote connection failures and opt-in degraded fallbacks. - Ignore deprecation warnings for asyncio_default_fixture_loop_scope in pytest.ini. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> --- pytest.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pytest.ini b/pytest.ini index 00b9ce4..caa4385 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,7 +1,10 @@ [pytest] pythonpath = . +asyncio_mode = auto filterwarnings = error + ignore:.*asyncio_default_fixture_loop_scope.*:pytest.PytestDeprecationWarning + ignore:.*asyncio_default_fixture_loop_scope.*:DeprecationWarning ignore:.*Field 'lifespan' has an incomplete definition.*:UserWarning ignore:.*Field 'lifespan' has an incomplete definition.*:Warning ignore:.*unauthenticated requests to the HF Hub.*:UserWarning From 1f7adc7e05d3cfd41f7896e700f469cbe40f3af6 Mon Sep 17 00:00:00 2001 From: spelech <28486500+spelech@users.noreply.github.com> Date: Wed, 28 Oct 2026 01:56:12 +0000 Subject: [PATCH 3/4] fix(vector-store): fail remote connection tests on unreachable endpoints - Prevent automatic local fallback in Qdrant and Chroma vector stores when explicit remote targets fail, unless opt-in allow_fallback=True is passed. - Report configured_mode, effective_mode, fallback_active, and fallback_reason in vector store stats, config, and health checks. - Return explicit error messages on POST /admin/api/vector-store/test when remote Qdrant or Chroma endpoints cannot be reached. - Add test coverage for remote connection failures and opt-in degraded fallbacks. - Configure pytest.ini asyncio_mode and filterwarnings to prevent deprecation warning failures. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> From 4a91995978e244290ba8db9f149c7c1bbd41c7f9 Mon Sep 17 00:00:00 2001 From: Antigravity Agent Date: Tue, 27 Oct 2026 21:31:03 -0500 Subject: [PATCH 4/4] fix(vector-store): explicitly disable fallback in test_vector_store_connection --- app/services/vector_store/manager.py | 1 + 1 file changed, 1 insertion(+) diff --git a/app/services/vector_store/manager.py b/app/services/vector_store/manager.py index 6ef81b8..161e787 100644 --- a/app/services/vector_store/manager.py +++ b/app/services/vector_store/manager.py @@ -313,6 +313,7 @@ def test_connection( "storage_path": target_storage, "url": target_url.strip(), "collection": target_collection, + "allow_fallback": False, } # Check if this matches active store to prevent locked embedded directory collisions