Skip to content

Commit ccb5740

Browse files
phernandezclaude
andcommitted
fix: create backup before config migration overwrites old format (#637)
When load_config() detects a legacy config format and resaves it, the old config.json was overwritten in-place with no recovery path. Users switching between dev and released versions would lose their config. Now creates config.json.bak before the migration resave so users can revert if needed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> Signed-off-by: phernandez <paul@basicmachines.co>
1 parent 373893a commit ccb5740

2 files changed

Lines changed: 70 additions & 1 deletion

File tree

src/basic_memory/config.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import importlib.util
44
import json
55
import os
6+
import shutil
67
from dataclasses import dataclass
78
from datetime import datetime
89
from pathlib import Path
@@ -705,7 +706,12 @@ def load_config(self) -> BasicMemoryConfig:
705706

706707
# Re-save to normalize legacy config into current format
707708
if needs_resave:
708-
logger.info("Migrating config to current format")
709+
# Create backup before overwriting so users can revert if needed
710+
backup_path = self.config_file.with_suffix(".json.bak")
711+
shutil.copy2(self.config_file, backup_path)
712+
logger.info(
713+
f"Migrating config to current format (backup: {backup_path})"
714+
)
709715
save_basic_memory_config(self.config_file, _CONFIG_CACHE)
710716

711717
return _CONFIG_CACHE

tests/test_config.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,69 @@ def test_legacy_cloud_mode_key_is_stripped_on_normalization_save(self):
625625
assert "cloud_mode" not in raw
626626

627627

628+
def test_migration_creates_backup_of_old_config(self):
629+
"""Config migration should create a .bak backup before overwriting."""
630+
with tempfile.TemporaryDirectory() as temp_dir:
631+
temp_path = Path(temp_dir)
632+
633+
config_manager = ConfigManager()
634+
config_manager.config_dir = temp_path / "basic-memory"
635+
config_manager.config_file = config_manager.config_dir / "config.json"
636+
config_manager.config_dir.mkdir(parents=True, exist_ok=True)
637+
638+
import json
639+
640+
old_config_data = {
641+
"env": "dev",
642+
"projects": {"main": str(temp_path / "main")},
643+
"default_project": "main",
644+
}
645+
config_manager.config_file.write_text(json.dumps(old_config_data, indent=2))
646+
original_content = config_manager.config_file.read_text()
647+
648+
import basic_memory.config
649+
650+
basic_memory.config._CONFIG_CACHE = None
651+
652+
config_manager.load_config()
653+
654+
# Backup should exist with the original content
655+
backup_path = config_manager.config_file.with_suffix(".json.bak")
656+
assert backup_path.exists(), "Migration should create a backup file"
657+
assert backup_path.read_text() == original_content
658+
659+
def test_no_backup_when_config_is_current_format(self):
660+
"""No backup should be created when config is already in the current format."""
661+
with tempfile.TemporaryDirectory() as temp_dir:
662+
temp_path = Path(temp_dir)
663+
664+
config_manager = ConfigManager()
665+
config_manager.config_dir = temp_path / "basic-memory"
666+
config_manager.config_file = config_manager.config_dir / "config.json"
667+
config_manager.config_dir.mkdir(parents=True, exist_ok=True)
668+
669+
import json
670+
671+
# Write config in the current ProjectEntry format — no migration needed
672+
current_config_data = {
673+
"env": "dev",
674+
"projects": {
675+
"main": {"path": str(temp_path / "main"), "mode": "local"}
676+
},
677+
"default_project": "main",
678+
}
679+
config_manager.config_file.write_text(json.dumps(current_config_data, indent=2))
680+
681+
import basic_memory.config
682+
683+
basic_memory.config._CONFIG_CACHE = None
684+
685+
config_manager.load_config()
686+
687+
backup_path = config_manager.config_file.with_suffix(".json.bak")
688+
assert not backup_path.exists(), "No backup should be created for current-format config"
689+
690+
628691
class TestPlatformNativePathSeparators:
629692
"""Test that config uses platform-native path separators."""
630693

0 commit comments

Comments
 (0)