From df9907c09fa218935dcaa4877d9f6e71a92ae610 Mon Sep 17 00:00:00 2001 From: Solanki Sumit <125974181+YAMRAJ13y@users.noreply.github.com> Date: Sun, 28 Jun 2026 02:55:13 +0530 Subject: [PATCH] fix(health): report unhealthy memory vector store as degraded Keep an unhealthy MemoryVectorStore instance available for health reporting instead of discarding it as disabled. This lets health checks report a degraded/down vector-store state while preserving focused regression coverage for initializer behavior. --- src/app_initializer.py | 4 +- ..._app_initializer_memory_vector_degraded.py | 71 +++++++++++++++++++ 2 files changed, 74 insertions(+), 1 deletion(-) create mode 100644 tests/test_app_initializer_memory_vector_degraded.py diff --git a/src/app_initializer.py b/src/app_initializer.py index 7d6b8c2ff..b438b2b17 100644 --- a/src/app_initializer.py +++ b/src/app_initializer.py @@ -68,8 +68,10 @@ def initialize_managers(base_dir: str, rag_manager=None) -> Dict[str, Any]: logger.info(f"Rebuilt memory vector index from {len(existing)} existing entries") logger.info("MemoryVectorStore initialized") else: + # Keep the unhealthy object (do NOT reset to None): consumers gate on + # `.healthy`, and service_health.chromadb_health() needs a present + # object to report DEGRADED/DOWN instead of DISABLED ("not configured"). logger.warning("MemoryVectorStore DEGRADED: ChromaDB vector memory unavailable") - memory_vector = None except Exception as e: logger.warning(f"MemoryVectorStore DEGRADED: {e}") memory_vector = None diff --git a/tests/test_app_initializer_memory_vector_degraded.py b/tests/test_app_initializer_memory_vector_degraded.py new file mode 100644 index 000000000..082bc99f1 --- /dev/null +++ b/tests/test_app_initializer_memory_vector_degraded.py @@ -0,0 +1,71 @@ +"""Regression: a present-but-unhealthy MemoryVectorStore must survive initialization. + +When MemoryVectorStore._initialize() fails (ChromaDB unavailable / embeddings not +installed) it swallows the exception and leaves `.healthy == False` — the object +exists but is unhealthy. app_initializer.initialize_managers() previously reset that +object to ``None`` in the ``else`` branch, so service_health.chromadb_health() saw +``memory_vector is None`` and reported the vector memory as DISABLED ("not +configured") instead of DEGRADED/DOWN ("initialization failed") — losing the +diagnostic distinction the /api/diagnostics/services probe is built to surface. + +This test fails before the fix (memory_vector is None) and passes after it. +""" +from unittest.mock import MagicMock + +import src.app_initializer as app_init +import src.memory_vector as memory_vector_mod +import src.service_health as sh + + +class _UnhealthyVectorStore: + """Stand-in for a MemoryVectorStore whose init failed: present but inert.""" + healthy = False + + def count(self): + return 0 + + def search(self, *a, **k): + return [] + + +def _neutralize_collaborators(monkeypatch): + """Stub out everything initialize_managers() builds except the vector store, + so the test isolates the memory_vector health-handling branch.""" + for name in [ + "MemoryManager", "SkillsManager", "SessionManager", "UploadHandler", + "PersonalDocsManager", "APIKeyManager", "PresetManager", + "MemoryProviderRegistry", "NativeMemoryProvider", "ChatProcessor", + "ResearchHandler", "ChatHandler", "ModelDiscovery", + ]: + monkeypatch.setattr(app_init, name, lambda *a, **k: MagicMock()) + monkeypatch.setattr(app_init, "set_session_manager", lambda *a, **k: None) + monkeypatch.setattr(app_init, "update_search_config", lambda *a, **k: None) + monkeypatch.setattr(app_init, "create_directories", lambda: None) + + +def test_failed_memory_vector_init_is_kept_not_discarded(monkeypatch, tmp_path): + _neutralize_collaborators(monkeypatch) + # initialize_managers does `from src.memory_vector import MemoryVectorStore` + # at call time, so patch it on the source module. + monkeypatch.setattr( + memory_vector_mod, "MemoryVectorStore", + lambda *a, **k: _UnhealthyVectorStore(), + ) + + result = app_init.initialize_managers(str(tmp_path), rag_manager=None) + + mv = result["memory_vector"] + assert mv is not None, "unhealthy MemoryVectorStore was discarded (reported as DISABLED, not DEGRADED/DOWN)" + assert mv.healthy is False + + +def test_chromadb_health_reports_down_for_unhealthy_vector_store(): + # Pins the downstream taxonomy the fix feeds: a present-but-unhealthy vector + # store (rag absent) is DOWN, not DISABLED; with a healthy rag it is DEGRADED; + # only when both are absent is it DISABLED. + store = _UnhealthyVectorStore() + healthy_rag = MagicMock(healthy=True) + + assert sh.chromadb_health(None, None)["status"] == sh.DISABLED + assert sh.chromadb_health(None, store)["status"] == sh.DOWN + assert sh.chromadb_health(healthy_rag, store)["status"] == sh.DEGRADED