Merge pull request #1312 from mjc/stale-chroma-reconnect
fix: use configured collection in recovery paths
This commit is contained in:
@@ -1194,3 +1194,26 @@ def test_chroma_backend_requarantines_after_inode_replacement(tmp_path, monkeypa
|
||||
("invalid", str(palace)),
|
||||
("stale", str(palace)),
|
||||
]
|
||||
|
||||
|
||||
def test_palace_get_collection_uses_configured_collection_name(monkeypatch):
|
||||
from mempalace import palace
|
||||
|
||||
captured = {}
|
||||
|
||||
def fake_get_collection(palace_path, collection_name=None, create=False):
|
||||
captured["palace_path"] = palace_path
|
||||
captured["collection_name"] = collection_name
|
||||
captured["create"] = create
|
||||
return object()
|
||||
|
||||
monkeypatch.setattr(palace._DEFAULT_BACKEND, "get_collection", fake_get_collection)
|
||||
monkeypatch.setattr("mempalace.config.get_configured_collection_name", lambda: "custom_drawers")
|
||||
|
||||
palace.get_collection("/palace", create=False)
|
||||
|
||||
assert captured == {
|
||||
"palace_path": "/palace",
|
||||
"collection_name": "custom_drawers",
|
||||
"create": False,
|
||||
}
|
||||
|
||||
@@ -776,6 +776,7 @@ def test_cmd_repair_error_reading(mock_config_cls, tmp_path, capsys):
|
||||
palace_dir.mkdir()
|
||||
(palace_dir / "chroma.sqlite3").write_text("db")
|
||||
mock_config_cls.return_value.palace_path = str(palace_dir)
|
||||
mock_config_cls.return_value.collection_name = "mempalace_drawers"
|
||||
args = argparse.Namespace(palace=None)
|
||||
mock_backend = MagicMock()
|
||||
mock_backend.get_collection.side_effect = Exception("corrupt db")
|
||||
@@ -791,6 +792,7 @@ def test_cmd_repair_zero_drawers(mock_config_cls, tmp_path, capsys):
|
||||
palace_dir.mkdir()
|
||||
(palace_dir / "chroma.sqlite3").write_text("db")
|
||||
mock_config_cls.return_value.palace_path = str(palace_dir)
|
||||
mock_config_cls.return_value.collection_name = "mempalace_drawers"
|
||||
args = argparse.Namespace(palace=None)
|
||||
mock_col = MagicMock()
|
||||
mock_col.count.return_value = 0
|
||||
@@ -807,6 +809,7 @@ def test_cmd_repair_success(mock_config_cls, tmp_path, capsys):
|
||||
palace_dir.mkdir()
|
||||
(palace_dir / "chroma.sqlite3").write_text("db")
|
||||
mock_config_cls.return_value.palace_path = str(palace_dir)
|
||||
mock_config_cls.return_value.collection_name = "mempalace_drawers"
|
||||
args = argparse.Namespace(palace=None, yes=True)
|
||||
mock_col = MagicMock()
|
||||
mock_col.count.return_value = 2
|
||||
@@ -836,12 +839,52 @@ def test_cmd_repair_success(mock_config_cls, tmp_path, capsys):
|
||||
mock_new_col.add.assert_not_called()
|
||||
|
||||
|
||||
@patch("mempalace.cli.MempalaceConfig")
|
||||
def test_cmd_repair_uses_configured_collection(mock_config_cls, tmp_path, capsys):
|
||||
palace_dir = tmp_path / "palace"
|
||||
palace_dir.mkdir()
|
||||
(palace_dir / "chroma.sqlite3").write_text("db")
|
||||
mock_config_cls.return_value.palace_path = str(palace_dir)
|
||||
mock_config_cls.return_value.collection_name = "custom_drawers"
|
||||
args = argparse.Namespace(palace=None, yes=True)
|
||||
mock_col = MagicMock()
|
||||
mock_col.count.return_value = 2
|
||||
mock_col.get.return_value = {
|
||||
"ids": ["id1", "id2"],
|
||||
"documents": ["doc1", "doc2"],
|
||||
"metadatas": [{"wing": "a"}, {"wing": "b"}],
|
||||
}
|
||||
mock_temp_col = MagicMock()
|
||||
mock_temp_col.count.return_value = 2
|
||||
mock_new_col = MagicMock()
|
||||
mock_new_col.count.return_value = 2
|
||||
mock_backend = _mock_backend_for(col=mock_col, new_col=mock_new_col)
|
||||
mock_backend.create_collection.side_effect = [mock_temp_col, mock_new_col]
|
||||
|
||||
with patch("mempalace.backends.chroma.ChromaBackend", return_value=mock_backend):
|
||||
cmd_repair(args)
|
||||
|
||||
out = capsys.readouterr().out
|
||||
assert "Repair complete" in out
|
||||
mock_backend.get_collection.assert_called_once_with(str(palace_dir), "custom_drawers")
|
||||
assert mock_backend.create_collection.call_args_list == [
|
||||
call(str(palace_dir), "custom_drawers__repair_tmp"),
|
||||
call(str(palace_dir), "custom_drawers"),
|
||||
]
|
||||
assert mock_backend.delete_collection.call_args_list == [
|
||||
call(str(palace_dir), "custom_drawers__repair_tmp"),
|
||||
call(str(palace_dir), "custom_drawers"),
|
||||
call(str(palace_dir), "custom_drawers__repair_tmp"),
|
||||
]
|
||||
|
||||
|
||||
@patch("mempalace.cli.MempalaceConfig")
|
||||
def test_cmd_repair_restores_backup_on_live_rebuild_failure(mock_config_cls, tmp_path, capsys):
|
||||
palace_dir = tmp_path / "palace"
|
||||
palace_dir.mkdir()
|
||||
(palace_dir / "chroma.sqlite3").write_text("db")
|
||||
mock_config_cls.return_value.palace_path = str(palace_dir)
|
||||
mock_config_cls.return_value.collection_name = "mempalace_drawers"
|
||||
args = argparse.Namespace(palace=None, yes=True)
|
||||
mock_col = MagicMock()
|
||||
mock_col.count.return_value = 2
|
||||
@@ -875,6 +918,7 @@ def test_cmd_repair_aborts_without_confirmation(mock_config_cls, tmp_path, capsy
|
||||
palace_dir.mkdir()
|
||||
(palace_dir / "chroma.sqlite3").write_text("db")
|
||||
mock_config_cls.return_value.palace_path = str(palace_dir)
|
||||
mock_config_cls.return_value.collection_name = "mempalace_drawers"
|
||||
args = argparse.Namespace(palace=None)
|
||||
mock_col = MagicMock()
|
||||
mock_col.count.return_value = 1
|
||||
|
||||
@@ -260,6 +260,7 @@ def test_mcp_probe_does_not_disable_vectors_for_unflushed_metadata(tmp_path, mon
|
||||
|
||||
class _Cfg:
|
||||
palace_path = str(tmp_path)
|
||||
collection_name = "mempalace_drawers"
|
||||
|
||||
monkeypatch.setattr(mcp_server, "_config", _Cfg())
|
||||
monkeypatch.setattr(mcp_server, "_vector_disabled", True)
|
||||
@@ -625,6 +626,7 @@ def test_tool_status_via_sqlite_returns_breakdown(palace_with_drawers, monkeypat
|
||||
# MempalaceConfig.
|
||||
class _Cfg:
|
||||
palace_path = str(palace_with_drawers)
|
||||
collection_name = "mempalace_drawers"
|
||||
|
||||
monkeypatch.setattr(mcp_server, "_config", _Cfg())
|
||||
monkeypatch.setattr(mcp_server, "_vector_disabled", True)
|
||||
|
||||
@@ -484,6 +484,26 @@ class TestWriteTools:
|
||||
assert result2["success"] is True
|
||||
assert result2["reason"] == "already_exists"
|
||||
|
||||
def test_add_drawer_fails_when_readback_misses(self, monkeypatch, config, kg):
|
||||
_patch_mcp_server(monkeypatch, config, kg)
|
||||
from mempalace import mcp_server
|
||||
|
||||
class _FakeGetResult:
|
||||
ids = []
|
||||
|
||||
class _FakeCol:
|
||||
def get(self, **kwargs):
|
||||
return _FakeGetResult()
|
||||
|
||||
def upsert(self, **kwargs):
|
||||
return None
|
||||
|
||||
monkeypatch.setattr(mcp_server, "_get_collection", lambda create=False: _FakeCol())
|
||||
|
||||
result = mcp_server.tool_add_drawer("w", "r", "content")
|
||||
assert result["success"] is False
|
||||
assert "not readable" in result["error"]
|
||||
|
||||
def test_add_drawer_shared_header_no_collision(self, monkeypatch, config, palace_path, kg):
|
||||
"""Documents sharing a >100-char header must get distinct IDs (full-content hash)."""
|
||||
_patch_mcp_server(monkeypatch, config, kg)
|
||||
@@ -1158,6 +1178,25 @@ class TestCacheInvalidation:
|
||||
assert "Reconnected" in result["message"]
|
||||
assert isinstance(result["drawers"], int)
|
||||
|
||||
def test_reconnect_closes_shared_backend(self, monkeypatch, config, kg):
|
||||
_patch_mcp_server(monkeypatch, config, kg)
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from mempalace import mcp_server, palace
|
||||
|
||||
close_palace = MagicMock()
|
||||
monkeypatch.setattr(palace._DEFAULT_BACKEND, "close_palace", close_palace)
|
||||
|
||||
class _FakeCol:
|
||||
def count(self):
|
||||
return 7
|
||||
|
||||
monkeypatch.setattr(mcp_server, "_get_collection", lambda create=False: _FakeCol())
|
||||
|
||||
result = mcp_server.tool_reconnect()
|
||||
assert result["success"] is True
|
||||
close_palace.assert_called_once_with(config.palace_path)
|
||||
|
||||
def test_get_collection_create_true_avoids_get_or_create_on_reopen(
|
||||
self, monkeypatch, config, palace_path, kg
|
||||
):
|
||||
|
||||
@@ -28,6 +28,16 @@ def test_get_palace_path_fallback():
|
||||
assert ".mempalace" in result
|
||||
|
||||
|
||||
def test_get_collection_name_from_config():
|
||||
from mempalace.config import get_configured_collection_name
|
||||
|
||||
get_configured_collection_name.cache_clear()
|
||||
with patch("mempalace.config.MempalaceConfig") as mock_config_cls:
|
||||
mock_config_cls.return_value.collection_name = "custom_drawers"
|
||||
assert repair._drawers_collection_name() == "custom_drawers"
|
||||
get_configured_collection_name.cache_clear()
|
||||
|
||||
|
||||
# ── _paginate_ids ─────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -330,6 +340,21 @@ def test_check_extraction_safety_passes_when_counts_match(tmp_path):
|
||||
repair.check_extraction_safety(str(tmp_path), 500)
|
||||
|
||||
|
||||
def test_check_extraction_safety_uses_configured_collection(tmp_path):
|
||||
with patch("mempalace.repair.sqlite_drawer_count", return_value=500) as count:
|
||||
repair.check_extraction_safety(str(tmp_path), 500, collection_name="custom_drawers")
|
||||
count.assert_called_once_with(str(tmp_path), "custom_drawers")
|
||||
|
||||
|
||||
def test_check_extraction_safety_default_uses_configured_collection(tmp_path):
|
||||
with (
|
||||
patch("mempalace.repair._drawers_collection_name", return_value="custom_drawers"),
|
||||
patch("mempalace.repair.sqlite_drawer_count", return_value=500) as count,
|
||||
):
|
||||
repair.check_extraction_safety(str(tmp_path), 500)
|
||||
count.assert_called_once_with(str(tmp_path), "custom_drawers")
|
||||
|
||||
|
||||
def test_check_extraction_safety_passes_when_sqlite_unreadable_and_under_cap(tmp_path):
|
||||
"""SQLite check fails (None) but extraction is well under the cap → safe."""
|
||||
with patch("mempalace.repair.sqlite_drawer_count", return_value=None):
|
||||
@@ -384,6 +409,73 @@ def test_sqlite_drawer_count_returns_none_on_unreadable_schema(tmp_path):
|
||||
assert repair.sqlite_drawer_count(str(tmp_path)) is None
|
||||
|
||||
|
||||
@patch("mempalace.repair.shutil")
|
||||
@patch("mempalace.repair.ChromaBackend")
|
||||
def test_rebuild_index_default_uses_configured_collection(mock_backend_cls, mock_shutil, tmp_path):
|
||||
sqlite_path = tmp_path / "chroma.sqlite3"
|
||||
sqlite_path.write_text("fake")
|
||||
mock_col = MagicMock()
|
||||
mock_col.count.return_value = 2
|
||||
mock_col.get.return_value = {
|
||||
"ids": ["id1", "id2"],
|
||||
"documents": ["doc1", "doc2"],
|
||||
"metadatas": [{"wing": "a"}, {"wing": "b"}],
|
||||
}
|
||||
mock_temp_col = MagicMock()
|
||||
mock_temp_col.count.return_value = 2
|
||||
mock_new_col = MagicMock()
|
||||
mock_new_col.count.return_value = 2
|
||||
mock_backend = _install_mock_backend(mock_backend_cls, mock_col)
|
||||
mock_backend.create_collection.side_effect = [mock_temp_col, mock_new_col]
|
||||
|
||||
with (
|
||||
patch("mempalace.repair._drawers_collection_name", return_value="custom_drawers"),
|
||||
patch("mempalace.repair.sqlite_drawer_count", return_value=2) as count,
|
||||
):
|
||||
repair.rebuild_index(palace_path=str(tmp_path))
|
||||
|
||||
mock_backend.get_collection.assert_called_once_with(str(tmp_path), "custom_drawers")
|
||||
count.assert_called_once_with(str(tmp_path), "custom_drawers")
|
||||
assert mock_backend.create_collection.call_args_list == [
|
||||
call(str(tmp_path), "custom_drawers__repair_tmp"),
|
||||
call(str(tmp_path), "custom_drawers"),
|
||||
]
|
||||
assert mock_backend.delete_collection.call_args_list == [
|
||||
call(str(tmp_path), "custom_drawers__repair_tmp"),
|
||||
call(str(tmp_path), "custom_drawers"),
|
||||
call(str(tmp_path), "custom_drawers__repair_tmp"),
|
||||
]
|
||||
|
||||
|
||||
def test_status_default_uses_configured_drawer_collection(tmp_path):
|
||||
with (
|
||||
patch("mempalace.repair._drawers_collection_name", return_value="custom_drawers"),
|
||||
patch("mempalace.repair.hnsw_capacity_status") as capacity_status,
|
||||
):
|
||||
capacity_status.side_effect = [
|
||||
{
|
||||
"sqlite_count": 1,
|
||||
"hnsw_count": 1,
|
||||
"divergence": 0,
|
||||
"diverged": False,
|
||||
"status": "ok",
|
||||
"message": "",
|
||||
},
|
||||
{
|
||||
"sqlite_count": 0,
|
||||
"hnsw_count": 0,
|
||||
"divergence": 0,
|
||||
"diverged": False,
|
||||
"status": "ok",
|
||||
"message": "",
|
||||
},
|
||||
]
|
||||
repair.status(palace_path=str(tmp_path))
|
||||
|
||||
assert capacity_status.call_args_list[0].args == (str(tmp_path), "custom_drawers")
|
||||
assert capacity_status.call_args_list[1].args == (str(tmp_path), "mempalace_closets")
|
||||
|
||||
|
||||
@patch("mempalace.repair.shutil")
|
||||
@patch("mempalace.repair.ChromaBackend")
|
||||
def test_rebuild_index_aborts_on_truncation_signal(mock_backend_cls, mock_shutil, tmp_path):
|
||||
|
||||
+19
-1
@@ -84,6 +84,24 @@ class TestSearchMemories:
|
||||
assert "error" in result
|
||||
assert "query failed" in result["error"]
|
||||
|
||||
def test_search_memories_vector_path_uses_explicit_collection_name(self):
|
||||
mock_col = MagicMock()
|
||||
mock_col.query.return_value = {
|
||||
"documents": [[]],
|
||||
"metadatas": [[]],
|
||||
"distances": [[]],
|
||||
"ids": [[]],
|
||||
}
|
||||
|
||||
with patch("mempalace.searcher.get_collection", return_value=mock_col) as get_collection:
|
||||
search_memories("test", "/fake/path", collection_name="custom_drawers")
|
||||
|
||||
get_collection.assert_called_once_with(
|
||||
"/fake/path",
|
||||
collection_name="custom_drawers",
|
||||
create=False,
|
||||
)
|
||||
|
||||
def test_search_memories_filters_in_result(self, palace_path, seeded_collection):
|
||||
result = search_memories("test", palace_path, wing="project", room="backend")
|
||||
assert result["filters"]["wing"] == "project"
|
||||
@@ -102,7 +120,7 @@ class TestSearchMemories:
|
||||
"ids": [["d1", "d2"]],
|
||||
}
|
||||
|
||||
def mock_get_collection(path, create=False):
|
||||
def mock_get_collection(path, collection_name=None, create=False):
|
||||
# First call: drawers. Second call: closets — raise so hybrid
|
||||
# degrades to pure drawer search (the catch block covers it).
|
||||
if not hasattr(mock_get_collection, "_called"):
|
||||
|
||||
Reference in New Issue
Block a user