Skip to content

Commit 17c0e0a

Browse files
phernandezclaude
andauthored
fix: check config default_project only in local mode for remove_project (#523)
Signed-off-by: phernandez <paul@basicmachines.co> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 7ebf16a commit 17c0e0a

2 files changed

Lines changed: 188 additions & 2 deletions

File tree

src/basic_memory/services/project_service.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,8 +241,13 @@ async def remove_project(self, name: str, delete_notes: bool = False) -> None:
241241

242242
project_path = project.path
243243

244-
# Check if project is default (in cloud mode, check database; in local mode, check config)
245-
if project.is_default or name == self.config_manager.config.default_project:
244+
# Check if project is default
245+
# In cloud mode: database is source of truth
246+
# In local mode: also check config file
247+
is_default = project.is_default
248+
if not self.config_manager.config.cloud_mode:
249+
is_default = is_default or name == self.config_manager.config.default_project
250+
if is_default:
246251
raise ValueError(f"Cannot remove the default project '{name}'") # pragma: no cover
247252

248253
# Remove from config if it exists there (may not exist in cloud mode)

tests/services/test_project_service.py

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1348,3 +1348,184 @@ async def test_remove_project_delete_notes_missing_directory(project_service: Pr
13481348
project_service.config_manager.remove_project(test_project_name)
13491349
except Exception:
13501350
pass
1351+
1352+
1353+
@pytest.mark.asyncio
1354+
async def test_remove_project_cloud_mode_uses_database_not_config(project_service: ProjectService):
1355+
"""Test that in cloud mode, remove_project only checks database for default status.
1356+
1357+
Regression test for bug where cloud mode checked config file (stale) instead of
1358+
database (source of truth) when determining if a project is the default.
1359+
"""
1360+
test_project_name = f"test-cloud-default-{os.urandom(4).hex()}"
1361+
test_project_path = f"/tmp/test-cloud-{os.urandom(8).hex()}"
1362+
1363+
# Save original cloud_mode setting
1364+
config = project_service.config_manager.config
1365+
original_cloud_mode = config.cloud_mode
1366+
original_default = config.default_project
1367+
1368+
try:
1369+
# Add a test project (not default)
1370+
await project_service.add_project(test_project_name, test_project_path, set_default=False)
1371+
1372+
# Verify project exists and is NOT default in database
1373+
db_project = await project_service.repository.get_by_name(test_project_name)
1374+
assert db_project is not None
1375+
assert db_project.is_default is not True # Should be None or False
1376+
1377+
# Simulate stale config: manually set this project as default in config only
1378+
# (This simulates what happens when config isn't updated after API calls)
1379+
config.default_project = test_project_name
1380+
1381+
# Enable cloud mode
1382+
config.cloud_mode = True
1383+
project_service.config_manager.save_config(config)
1384+
1385+
# In cloud mode, should be able to remove the project because database says it's not default
1386+
# (even though stale config says it is) - this should NOT raise ValueError
1387+
await project_service.remove_project(test_project_name, delete_notes=False)
1388+
1389+
# Verify project was removed from database
1390+
db_project = await project_service.repository.get_by_name(test_project_name)
1391+
assert db_project is None
1392+
1393+
finally:
1394+
# Restore original settings
1395+
config = project_service.config_manager.config
1396+
config.cloud_mode = original_cloud_mode
1397+
config.default_project = original_default
1398+
project_service.config_manager.save_config(config)
1399+
1400+
# Cleanup from config if test failed partway
1401+
try:
1402+
project_service.config_manager.remove_project(test_project_name)
1403+
except (ValueError, KeyError):
1404+
pass # Project may not be in config
1405+
1406+
1407+
@pytest.mark.asyncio
1408+
async def test_remove_project_local_mode_checks_both_config_and_database(
1409+
project_service: ProjectService,
1410+
):
1411+
"""Test that in local mode, remove_project checks both config AND database for default status.
1412+
1413+
In local mode, we check both sources to be safe - if either says the project is default,
1414+
we prevent deletion.
1415+
"""
1416+
test_project_name = f"test-local-default-{os.urandom(4).hex()}"
1417+
test_project_path = f"/tmp/test-local-{os.urandom(8).hex()}"
1418+
1419+
# Save original settings
1420+
config = project_service.config_manager.config
1421+
original_cloud_mode = config.cloud_mode
1422+
original_default = config.default_project
1423+
1424+
try:
1425+
# Ensure we're in local mode before adding project
1426+
config.cloud_mode = False
1427+
project_service.config_manager.save_config(config)
1428+
1429+
# Add a test project (not default) - this will add to both DB and config in local mode
1430+
await project_service.add_project(test_project_name, test_project_path, set_default=False)
1431+
1432+
# Verify project exists and is NOT default in database
1433+
db_project = await project_service.repository.get_by_name(test_project_name)
1434+
assert db_project is not None
1435+
assert db_project.is_default is not True
1436+
1437+
# Re-read config to get the updated version (after add_project added the project)
1438+
config = project_service.config_manager.config
1439+
1440+
# Set this project as default in config only (not in DB)
1441+
config.default_project = test_project_name
1442+
project_service.config_manager.save_config(config)
1443+
1444+
# In local mode, should NOT be able to remove because config says it's default
1445+
with pytest.raises(ValueError, match="Cannot remove the default project"):
1446+
await project_service.remove_project(test_project_name, delete_notes=False)
1447+
1448+
# Verify project still exists in database
1449+
db_project = await project_service.repository.get_by_name(test_project_name)
1450+
assert db_project is not None
1451+
1452+
finally:
1453+
# Restore original settings
1454+
config = project_service.config_manager.config
1455+
config.cloud_mode = original_cloud_mode
1456+
config.default_project = original_default
1457+
project_service.config_manager.save_config(config)
1458+
1459+
# Cleanup
1460+
try:
1461+
project_service.config_manager.remove_project(test_project_name)
1462+
except (ValueError, KeyError):
1463+
pass
1464+
1465+
1466+
@pytest.mark.asyncio
1467+
async def test_remove_project_rejects_database_default_in_both_modes(
1468+
project_service: ProjectService,
1469+
):
1470+
"""Test that remove_project rejects deletion when project is default in database.
1471+
1472+
This should be blocked in BOTH cloud mode and local mode.
1473+
"""
1474+
test_project_name = f"test-db-default-{os.urandom(4).hex()}"
1475+
test_project_path = f"/tmp/test-db-default-{os.urandom(8).hex()}"
1476+
1477+
# Save original settings
1478+
original_cloud_mode = project_service.config_manager.config.cloud_mode
1479+
original_default = project_service.config_manager.config.default_project
1480+
1481+
try:
1482+
# Add a test project and set it as default
1483+
await project_service.add_project(test_project_name, test_project_path, set_default=True)
1484+
1485+
# Verify project is default in database
1486+
db_project = await project_service.repository.get_by_name(test_project_name)
1487+
assert db_project is not None
1488+
assert db_project.is_default is True
1489+
1490+
# Test in cloud mode - should reject
1491+
config = project_service.config_manager.config
1492+
config.cloud_mode = True
1493+
project_service.config_manager.save_config(config)
1494+
1495+
with pytest.raises(ValueError, match="Cannot remove the default project"):
1496+
await project_service.remove_project(test_project_name, delete_notes=False)
1497+
1498+
# Test in local mode - should also reject
1499+
config.cloud_mode = False
1500+
project_service.config_manager.save_config(config)
1501+
1502+
with pytest.raises(ValueError, match="Cannot remove the default project"):
1503+
await project_service.remove_project(test_project_name, delete_notes=False)
1504+
1505+
# Verify project still exists in both cases
1506+
assert test_project_name in project_service.projects
1507+
1508+
finally:
1509+
# Restore original settings
1510+
config = project_service.config_manager.config
1511+
config.cloud_mode = original_cloud_mode
1512+
config.default_project = original_default
1513+
project_service.config_manager.save_config(config)
1514+
1515+
# Set original default back in database so we can clean up
1516+
if original_default:
1517+
original_project = await project_service.repository.get_by_name(original_default)
1518+
if original_project:
1519+
await project_service.repository.set_as_default(original_project.id)
1520+
1521+
# Cleanup test project
1522+
if test_project_name in project_service.projects:
1523+
try:
1524+
# Clear default in DB first
1525+
db_project = await project_service.repository.get_by_name(test_project_name)
1526+
if db_project and db_project.is_default:
1527+
# Find another project to make default
1528+
pass # Let the config_manager handle it
1529+
project_service.config_manager.remove_project(test_project_name)
1530+
except Exception:
1531+
pass

0 commit comments

Comments
 (0)