From 195229f78e4559f7c20ef0a6ccf524761f4e692a Mon Sep 17 00:00:00 2001 From: phernandez Date: Wed, 4 Mar 2026 11:57:15 -0600 Subject: [PATCH] fix: resolve default_project returning null in cloud mode (#644) In cloud mode, ConfigManager has no local config file so default_project always returned None. Add async get_default_project_name() on ProjectService that falls back to the database is_default flag when ConfigManager returns None. Co-Authored-By: Claude Opus 4.6 Signed-off-by: phernandez --- .../api/v2/routers/project_router.py | 2 +- src/basic_memory/services/project_service.py | 15 ++++++++++++++ tests/api/v2/test_project_router.py | 20 +++++++++++++++++-- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/basic_memory/api/v2/routers/project_router.py b/src/basic_memory/api/v2/routers/project_router.py index bd67256e..77cfb01b 100644 --- a/src/basic_memory/api/v2/routers/project_router.py +++ b/src/basic_memory/api/v2/routers/project_router.py @@ -48,7 +48,7 @@ async def list_projects( A list of all projects with metadata """ projects = await project_service.list_projects() - default_project = project_service.default_project + default_project = await project_service.get_default_project_name() project_items = [ ProjectItem( diff --git a/src/basic_memory/services/project_service.py b/src/basic_memory/services/project_service.py index 2d8413de..018187ea 100644 --- a/src/basic_memory/services/project_service.py +++ b/src/basic_memory/services/project_service.py @@ -82,6 +82,21 @@ class ProjectService: """ return self.config_manager.default_project + async def get_default_project_name(self) -> str: + """Get the default project name, falling back to the database. + + ConfigManager reads from the local config file, which doesn't exist + in cloud mode. When it returns None, fall back to the is_default + flag stored in the database. + """ + default = self.config_manager.default_project + if default is not None: + return default + db_default = await self.repository.get_default_project() + if db_default is not None: + return db_default.name + raise ValueError("No default project configured") + @property def current_project(self) -> Optional[str]: """Get the name of the currently active project. diff --git a/tests/api/v2/test_project_router.py b/tests/api/v2/test_project_router.py index 2bbdf5c3..dec6e343 100644 --- a/tests/api/v2/test_project_router.py +++ b/tests/api/v2/test_project_router.py @@ -11,6 +11,21 @@ from basic_memory.schemas.project_info import ProjectItem, ProjectStatusResponse from basic_memory.schemas.v2 import ProjectResolveResponse +@pytest.mark.asyncio +async def test_list_projects(client: AsyncClient, test_project: Project, v2_projects_url): + """Test listing projects returns default_project from the database.""" + response = await client.get(f"{v2_projects_url}/") + + assert response.status_code == 200 + data = response.json() + + # default_project must be populated from the is_default flag in the database + assert data["default_project"] == test_project.name + + project_names = [p["name"] for p in data["projects"]] + assert test_project.name in project_names + + @pytest.mark.asyncio async def test_get_project_by_id(client: AsyncClient, test_project: Project, v2_projects_url): """Test getting a project by its external_id UUID.""" @@ -361,9 +376,10 @@ async def test_legacy_v1_list_projects_endpoint(client: AsyncClient, test_projec assert response.status_code == 200 data = response.json() assert "projects" in data - assert "default_project" in data - # Verify the test project is in the list + # default_project must be populated, not null + assert data["default_project"] == test_project.name + project_names = [p["name"] for p in data["projects"]] assert test_project.name in project_names