@@ -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