Compare commits

..

3 Commits

Author SHA1 Message Date
phernandez 9d98892570 chore: update version to 0.17.9 for v0.17.9 release 2026-01-24 12:55:58 -06:00
phernandez 3be4495723 docs: add CHANGELOG entry for v0.17.9
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Signed-off-by: phernandez <paul@basicmachines.co>
2026-01-24 12:55:27 -06:00
Paul Hernandez 17c0e0a29b 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>
2026-01-24 12:53:44 -06:00
5 changed files with 200 additions and 5 deletions
+9
View File
@@ -1,5 +1,14 @@
# CHANGELOG
## v0.17.9 (2026-01-24)
### Bug Fixes
- **#523**: Fix `remove_project()` checking stale config in cloud mode
([`17c0e0a`](https://github.com/basicmachines-co/basic-memory/commit/17c0e0a))
- In cloud mode, only check database `is_default` field (source of truth)
- Config file can become stale when users set default project via v2 API
## v0.17.8 (2026-01-24)
### Bug Fixes
+2 -2
View File
@@ -6,12 +6,12 @@
"url": "https://github.com/basicmachines-co/basic-memory.git",
"source": "github"
},
"version": "0.17.8",
"version": "0.17.9",
"packages": [
{
"registryType": "pypi",
"identifier": "basic-memory",
"version": "0.17.8",
"version": "0.17.9",
"runtimeHint": "uvx",
"runtimeArguments": [
{"type": "positional", "value": "basic-memory"},
+1 -1
View File
@@ -1,7 +1,7 @@
"""basic-memory - Local-first knowledge management combining Zettelkasten with knowledge graphs"""
# Package version - updated by release automation
__version__ = "0.17.8"
__version__ = "0.17.9"
# API version for FastAPI - independent of package version
__api_version__ = "v0"
+7 -2
View File
@@ -241,8 +241,13 @@ class ProjectService:
project_path = project.path
# Check if project is default (in cloud mode, check database; in local mode, check config)
if project.is_default or name == self.config_manager.config.default_project:
# Check if project is default
# In cloud mode: database is source of truth
# In local mode: also check config file
is_default = project.is_default
if not self.config_manager.config.cloud_mode:
is_default = is_default or name == self.config_manager.config.default_project
if is_default:
raise ValueError(f"Cannot remove the default project '{name}'") # pragma: no cover
# Remove from config if it exists there (may not exist in cloud mode)
+181
View File
@@ -1348,3 +1348,184 @@ async def test_remove_project_delete_notes_missing_directory(project_service: Pr
project_service.config_manager.remove_project(test_project_name)
except Exception:
pass
@pytest.mark.asyncio
async def test_remove_project_cloud_mode_uses_database_not_config(project_service: ProjectService):
"""Test that in cloud mode, remove_project only checks database for default status.
Regression test for bug where cloud mode checked config file (stale) instead of
database (source of truth) when determining if a project is the default.
"""
test_project_name = f"test-cloud-default-{os.urandom(4).hex()}"
test_project_path = f"/tmp/test-cloud-{os.urandom(8).hex()}"
# Save original cloud_mode setting
config = project_service.config_manager.config
original_cloud_mode = config.cloud_mode
original_default = config.default_project
try:
# Add a test project (not default)
await project_service.add_project(test_project_name, test_project_path, set_default=False)
# Verify project exists and is NOT default in database
db_project = await project_service.repository.get_by_name(test_project_name)
assert db_project is not None
assert db_project.is_default is not True # Should be None or False
# Simulate stale config: manually set this project as default in config only
# (This simulates what happens when config isn't updated after API calls)
config.default_project = test_project_name
# Enable cloud mode
config.cloud_mode = True
project_service.config_manager.save_config(config)
# In cloud mode, should be able to remove the project because database says it's not default
# (even though stale config says it is) - this should NOT raise ValueError
await project_service.remove_project(test_project_name, delete_notes=False)
# Verify project was removed from database
db_project = await project_service.repository.get_by_name(test_project_name)
assert db_project is None
finally:
# Restore original settings
config = project_service.config_manager.config
config.cloud_mode = original_cloud_mode
config.default_project = original_default
project_service.config_manager.save_config(config)
# Cleanup from config if test failed partway
try:
project_service.config_manager.remove_project(test_project_name)
except (ValueError, KeyError):
pass # Project may not be in config
@pytest.mark.asyncio
async def test_remove_project_local_mode_checks_both_config_and_database(
project_service: ProjectService,
):
"""Test that in local mode, remove_project checks both config AND database for default status.
In local mode, we check both sources to be safe - if either says the project is default,
we prevent deletion.
"""
test_project_name = f"test-local-default-{os.urandom(4).hex()}"
test_project_path = f"/tmp/test-local-{os.urandom(8).hex()}"
# Save original settings
config = project_service.config_manager.config
original_cloud_mode = config.cloud_mode
original_default = config.default_project
try:
# Ensure we're in local mode before adding project
config.cloud_mode = False
project_service.config_manager.save_config(config)
# Add a test project (not default) - this will add to both DB and config in local mode
await project_service.add_project(test_project_name, test_project_path, set_default=False)
# Verify project exists and is NOT default in database
db_project = await project_service.repository.get_by_name(test_project_name)
assert db_project is not None
assert db_project.is_default is not True
# Re-read config to get the updated version (after add_project added the project)
config = project_service.config_manager.config
# Set this project as default in config only (not in DB)
config.default_project = test_project_name
project_service.config_manager.save_config(config)
# In local mode, should NOT be able to remove because config says it's default
with pytest.raises(ValueError, match="Cannot remove the default project"):
await project_service.remove_project(test_project_name, delete_notes=False)
# Verify project still exists in database
db_project = await project_service.repository.get_by_name(test_project_name)
assert db_project is not None
finally:
# Restore original settings
config = project_service.config_manager.config
config.cloud_mode = original_cloud_mode
config.default_project = original_default
project_service.config_manager.save_config(config)
# Cleanup
try:
project_service.config_manager.remove_project(test_project_name)
except (ValueError, KeyError):
pass
@pytest.mark.asyncio
async def test_remove_project_rejects_database_default_in_both_modes(
project_service: ProjectService,
):
"""Test that remove_project rejects deletion when project is default in database.
This should be blocked in BOTH cloud mode and local mode.
"""
test_project_name = f"test-db-default-{os.urandom(4).hex()}"
test_project_path = f"/tmp/test-db-default-{os.urandom(8).hex()}"
# Save original settings
original_cloud_mode = project_service.config_manager.config.cloud_mode
original_default = project_service.config_manager.config.default_project
try:
# Add a test project and set it as default
await project_service.add_project(test_project_name, test_project_path, set_default=True)
# Verify project is default in database
db_project = await project_service.repository.get_by_name(test_project_name)
assert db_project is not None
assert db_project.is_default is True
# Test in cloud mode - should reject
config = project_service.config_manager.config
config.cloud_mode = True
project_service.config_manager.save_config(config)
with pytest.raises(ValueError, match="Cannot remove the default project"):
await project_service.remove_project(test_project_name, delete_notes=False)
# Test in local mode - should also reject
config.cloud_mode = False
project_service.config_manager.save_config(config)
with pytest.raises(ValueError, match="Cannot remove the default project"):
await project_service.remove_project(test_project_name, delete_notes=False)
# Verify project still exists in both cases
assert test_project_name in project_service.projects
finally:
# Restore original settings
config = project_service.config_manager.config
config.cloud_mode = original_cloud_mode
config.default_project = original_default
project_service.config_manager.save_config(config)
# Set original default back in database so we can clean up
if original_default:
original_project = await project_service.repository.get_by_name(original_default)
if original_project:
await project_service.repository.set_as_default(original_project.id)
# Cleanup test project
if test_project_name in project_service.projects:
try:
# Clear default in DB first
db_project = await project_service.repository.get_by_name(test_project_name)
if db_project and db_project.is_default:
# Find another project to make default
pass # Let the config_manager handle it
project_service.config_manager.remove_project(test_project_name)
except Exception:
pass