"""Tests for destructive-operation safety in mempalace.migrate.""" import os from types import SimpleNamespace from unittest.mock import MagicMock, patch from mempalace.migrate import _restore_stale_palace, migrate def test_migrate_requires_palace_database(tmp_path, capsys): palace_dir = tmp_path / "palace" palace_dir.mkdir() result = migrate(str(palace_dir)) out = capsys.readouterr().out assert result is False assert "No palace database found" in out def test_migrate_aborts_without_confirmation(tmp_path, capsys): palace_dir = tmp_path / "palace" palace_dir.mkdir() # Presence of chroma.sqlite3 is the safety gate; validity is mocked below. (palace_dir / "chroma.sqlite3").write_text("db") mock_chromadb = SimpleNamespace( __version__="0.6.0", PersistentClient=MagicMock(side_effect=Exception("unreadable")), ) with ( patch.dict("sys.modules", {"chromadb": mock_chromadb}), patch("mempalace.migrate.detect_chromadb_version", return_value="0.5.x"), patch( "mempalace.migrate.extract_drawers_from_sqlite", return_value=[{"id": "id1", "document": "doc", "metadata": {"wing": "w", "room": "r"}}], ), patch("builtins.input", return_value="n"), patch("mempalace.migrate.shutil.copytree") as mock_copytree, patch("mempalace.migrate.shutil.rmtree") as mock_rmtree, ): result = migrate(str(palace_dir)) out = capsys.readouterr().out assert result is False assert "Aborted." in out mock_copytree.assert_not_called() mock_rmtree.assert_not_called() def test_restore_stale_palace_with_clean_destination(tmp_path): """Rollback when no partial copy exists at palace_path.""" palace_path = tmp_path / "palace" stale_path = tmp_path / "palace.old" stale_path.mkdir() (stale_path / "chroma.sqlite3").write_bytes(b"original") _restore_stale_palace(str(palace_path), str(stale_path)) assert palace_path.is_dir() assert (palace_path / "chroma.sqlite3").read_bytes() == b"original" assert not stale_path.exists() def test_restore_stale_palace_clears_partial_copy(tmp_path): """Rollback must remove a partially-copied palace_path before restoring. Simulates the Qodo-reported hazard: shutil.move() began creating palace_path, then failed. A bare os.replace(stale, palace_path) would trip on the existing destination; _restore_stale_palace must clear it. """ palace_path = tmp_path / "palace" stale_path = tmp_path / "palace.old" stale_path.mkdir() (stale_path / "chroma.sqlite3").write_bytes(b"original") palace_path.mkdir() (palace_path / "half-copied.bin").write_bytes(b"garbage") _restore_stale_palace(str(palace_path), str(stale_path)) assert palace_path.is_dir() assert (palace_path / "chroma.sqlite3").read_bytes() == b"original" assert not (palace_path / "half-copied.bin").exists() assert not stale_path.exists() def test_restore_stale_palace_logs_and_swallows_on_failure(tmp_path, capsys): """If restore itself fails, log both paths — don't raise from rollback.""" palace_path = tmp_path / "palace" stale_path = tmp_path / "palace.old" stale_path.mkdir() # Force os.replace to fail deterministically. with patch("mempalace.migrate.os.replace", side_effect=OSError("boom")): _restore_stale_palace(str(palace_path), str(stale_path)) out = capsys.readouterr().out assert "CRITICAL" in out assert os.fspath(palace_path) in out assert os.fspath(stale_path) in out