fix(core): preserve an existing database default when repairing a missing config default

Follow-up to #985 (the #974 fix by @rudi193-cmd), addressing the codex P2
raised on #987: when config's default_project has no database row but the
database still holds a valid default of its own, promoting the just-added
project would silently steal that default. The repair now repoints config
at a surviving database default — matching synchronize_projects, which
treats the database default as authoritative — and promotes the added
project only when no usable database default exists (no default at all,
or one unknown to config, which set_default_project rejects and
reconciliation deletes).

Also removes an unused ProjectConfig import from the #985 regression test
(strict ruff failure; CI's --fix lint masked it).

Refs #974

Signed-off-by: phernandez <paul@basicmachines.co>
This commit is contained in:
phernandez
2026-06-12 09:07:14 -05:00
committed by Paul Hernandez
parent 25732b2fe2
commit 32a1c208b3
2 changed files with 77 additions and 9 deletions
+30 -8
View File
@@ -261,14 +261,36 @@ class ProjectService:
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,
)
# Trigger: config names a default project that has no database row
# (the fresh-config wedge from issue #974).
# Why: synchronize_projects treats an existing database default as
# authoritative, so a surviving default must win over promoting
# the just-added project. set_default_project raises for names
# absent from config — and reconciliation deletes such rows —
# so a database default unknown to config cannot be repointed to.
# Outcome: config is repointed at a usable database default when one
# exists; otherwise the added project becomes the default.
db_default = await self.repository.get_default_project()
if (
db_default is not None
and self.config_manager.get_project(db_default.name)[0] is not None
):
self.config_manager.set_default_project(db_default.name)
logger.info(
"Repointed config default from missing '%s' at existing "
"database default '%s'",
config_default,
db_default.name,
)
else:
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}")
+47 -1
View File
@@ -440,7 +440,7 @@ async def test_add_project_promotes_when_config_default_missing_from_db(
):
"""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.config import 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
@@ -480,6 +480,52 @@ async def test_add_project_promotes_when_config_default_missing_from_db(
assert await repo.get_by_name("main") is None
@pytest.mark.asyncio
async def test_add_project_preserves_existing_db_default(
project_service: ProjectService, config_manager: ConfigManager, test_project
):
"""The #974 repair must not steal an existing database default.
Drift state: config's default_project names a project with no database row,
but the database still holds a valid default of its own. synchronize_projects
resolves this by trusting the database default, so add_project's repair must
repoint config at it rather than promoting the just-added project.
"""
config_default = config_manager.default_project
assert config_default is not None
surviving_name = f"test-surviving-default-{os.urandom(4).hex()}"
added_name = f"test-no-steal-{os.urandom(4).hex()}"
with tempfile.TemporaryDirectory() as temp_dir:
surviving_path = str(Path(temp_dir) / "surviving")
os.makedirs(surviving_path, exist_ok=True)
# A normally-added project that then becomes the database default,
# while config's default_project still names the fixture project.
await project_service.add_project(surviving_name, surviving_path)
surviving = await project_service.repository.get_by_name(surviving_name)
assert surviving is not None
await project_service.repository.set_as_default(surviving.id)
# Wedge config: its named default loses its database row.
await project_service.repository.delete(test_project.id)
assert await project_service.get_project(config_default) is None
added_path = str(Path(temp_dir) / "added")
os.makedirs(added_path, exist_ok=True)
await project_service.add_project(added_name, added_path)
# The surviving database default wins: config is repointed at it and
# the newly added project is not promoted.
assert config_manager.default_project == surviving_name
db_default = await project_service.repository.get_default_project()
assert db_default is not None
assert db_default.name == surviving_name
added = await project_service.repository.get_by_name(added_name)
assert added is not None
assert added.is_default is not True
@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."""