diff --git a/src/basic_memory/api/v2/routers/project_router.py b/src/basic_memory/api/v2/routers/project_router.py index 5844c874..bfec8303 100644 --- a/src/basic_memory/api/v2/routers/project_router.py +++ b/src/basic_memory/api/v2/routers/project_router.py @@ -193,7 +193,7 @@ async def add_project( return ProjectStatusResponse( # pyright: ignore [reportCallIssue] message=f"Project '{new_project.name}' added successfully", status="success", - default=project_data.set_default, + default=new_project.is_default or False, new_project=ProjectItem( id=new_project.id, external_id=new_project.external_id, diff --git a/src/basic_memory/services/project_service.py b/src/basic_memory/services/project_service.py index 5e5df6b9..c962d6c0 100644 --- a/src/basic_memory/services/project_service.py +++ b/src/basic_memory/services/project_service.py @@ -256,6 +256,19 @@ async def add_project(self, name: str, path: str, set_default: bool = False) -> await self.repository.set_as_default(created_project.id) self.config_manager.set_default_project(name) logger.info(f"Project '{name}' set as default") + else: + config_default = self.config_manager.default_project + if config_default is not None: + db_default_project = await self.repository.get_by_name(config_default) + if db_default_project is None: + await self.repository.set_as_default(created_project.id) + self.config_manager.set_default_project(name) + logger.info( + "Promoted project '%s' to default because configured default '%s' " + "is missing from database", + name, + config_default, + ) logger.info(f"Project '{name}' added at {resolved_path}") diff --git a/tests/api/v2/test_project_router.py b/tests/api/v2/test_project_router.py index ceccebf4..763fc0bb 100644 --- a/tests/api/v2/test_project_router.py +++ b/tests/api/v2/test_project_router.py @@ -6,6 +6,7 @@ import pytest from httpx import AsyncClient +from basic_memory.config import ProjectEntry from basic_memory.models import Project from basic_memory.schemas.project_info import ProjectItem, ProjectStatusResponse from basic_memory.schemas.v2 import ProjectResolveResponse @@ -54,6 +55,46 @@ async def test_get_project_by_id_not_found(client: AsyncClient, v2_projects_url) assert "not found" in response.json()["detail"].lower() +@pytest.mark.asyncio +async def test_add_project_response_reflects_promoted_default( + client: AsyncClient, + v2_projects_url, + app_config, + config_manager, + config_home, + project_repository, +): + """Regression #974/#985: POST response should echo persisted default promotion.""" + main_home = config_home / "basic-memory" + main_home.mkdir(parents=True, exist_ok=True) + qa_path = config_home / "qa-notes" + qa_path.mkdir(parents=True, exist_ok=True) + + fresh_config = app_config.model_copy( + update={ + "projects": {"main": ProjectEntry(path=str(main_home))}, + "default_project": "main", + } + ) + config_manager.save_config(fresh_config) + + for project in await project_repository.find_all(): + await project_repository.delete(project.id) + + response = await client.post( + f"{v2_projects_url}/", + json={"name": "qa", "path": str(qa_path), "set_default": False}, + ) + + assert response.status_code == 201 + status_response = ProjectStatusResponse.model_validate(response.json()) + assert status_response.status == "success" + assert status_response.default is True + new_project = _project_item(status_response.new_project) + assert new_project.name == "qa" + assert new_project.is_default is True + + @pytest.mark.asyncio async def test_update_project_path_by_id( client: AsyncClient, test_project: Project, v2_projects_url diff --git a/tests/services/test_project_service.py b/tests/services/test_project_service.py index 57063f18..43942d07 100644 --- a/tests/services/test_project_service.py +++ b/tests/services/test_project_service.py @@ -434,6 +434,52 @@ async def test_add_project_with_set_default_false(project_service: ProjectServic await project_service.remove_project(test_project_name) +@pytest.mark.asyncio +async def test_add_project_promotes_when_config_default_missing_from_db( + config_home, app_config, config_manager, engine_factory +): + """Regression #974: config default exists only in config, not DB — promote on add.""" + from basic_memory import config as config_module + from basic_memory.config import ProjectConfig, ProjectEntry + from basic_memory.markdown.entity_parser import EntityParser + from basic_memory.markdown.markdown_processor import MarkdownProcessor + from basic_memory.repository.project_repository import ProjectRepository + from basic_memory.services.file_service import FileService + + config_module._CONFIG_CACHE = None + config_module._CONFIG_MTIME = None + config_module._CONFIG_SIZE = None + + main_home = config_home / "basic-memory" + main_home.mkdir(parents=True, exist_ok=True) + qa_path = config_home / "qa-notes" + qa_path.mkdir(parents=True, exist_ok=True) + + fresh_config = app_config.model_copy( + update={ + "projects": {"main": ProjectEntry(path=str(main_home))}, + "default_project": "main", + } + ) + config_manager.save_config(fresh_config) + + _, session_maker = engine_factory + repo = ProjectRepository(session_maker) + for project in await repo.find_all(): + await repo.delete(project.id) + + file_service = FileService(qa_path, MarkdownProcessor(EntityParser(qa_path))) + service = ProjectService(repository=repo, file_service=file_service) + + await service.add_project("qa", str(qa_path), set_default=False) + + assert service.default_project == "qa" + qa_project = await repo.get_by_name("qa") + assert qa_project is not None + assert qa_project.is_default is True + assert await repo.get_by_name("main") is None + + @pytest.mark.asyncio async def test_add_project_default_parameter_omitted(project_service: ProjectService): """Test adding a project without set_default parameter defaults to False behavior."""