Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 19 additions & 11 deletions src/basic_memory/api/v2/routers/project_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -559,12 +559,10 @@ async def set_default_project_by_id(
logger.info(f"API v2 request: set_default_project_by_id for project_id={project_id}")

try:
# Get the old default project from database
# Get the old default project from database. It may be absent during
# bootstrap/recovery (no default row yet); that is a valid state, not an
# error, so we only echo it back when one exists.
default_project = await project_repository.get_default_project()
if not default_project:
raise HTTPException( # pragma: no cover
status_code=404, detail="No default project is currently set"
)

# Get the new default project by external_id
new_default_project = await project_repository.get_by_external_id(project_id)
Expand All @@ -576,17 +574,27 @@ async def set_default_project_by_id(
# Set as default using project name (service layer still uses names internally)
await project_service.set_default_project(new_default_project.name)

return ProjectStatusResponse(
message=f"Project '{new_default_project.name}' set as default successfully",
status="success",
default=True,
old_project=ProjectItem(
# Trigger: a previous default existed
# Why: ProjectStatusResponse.old_project is Optional; the no-default
# bootstrap case must succeed with old_project=None
# Outcome: response echoes the prior default only when there was one
old_project = (
ProjectItem(
id=default_project.id,
external_id=default_project.external_id,
name=default_project.name,
path=default_project.path,
is_default=False,
),
)
if default_project
else None
)

return ProjectStatusResponse(
message=f"Project '{new_default_project.name}' set as default successfully",
status="success",
default=True,
old_project=old_project,
new_project=ProjectItem(
id=new_default_project.id,
external_id=new_default_project.external_id,
Expand Down
31 changes: 31 additions & 0 deletions tests/api/v2/test_project_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,37 @@ async def test_set_default_project_by_id(
assert old_project.is_default is False


@pytest.mark.asyncio
async def test_set_default_project_when_none_is_set(
client: AsyncClient, test_project: Project, v2_projects_url, project_repository
):
"""Regression for #975: setting a default must succeed when none is set.

This is the bootstrap/recovery case: `bm project default <name>` is exactly
the command reached for when no default exists, so the endpoint must not 404.
"""
# Clear any existing default so no row has is_default set.
await project_repository.update(test_project.id, {"is_default": None})
assert await project_repository.get_default_project() is None

response = await client.put(f"{v2_projects_url}/{test_project.external_id}/default")

assert response.status_code == 200
status_response = ProjectStatusResponse.model_validate(response.json())
assert status_response.status == "success"
assert status_response.default is True
# No previous default existed, so old_project must be None.
assert status_response.old_project is None
new_project = _project_item(status_response.new_project)
assert new_project.external_id == test_project.external_id
assert new_project.is_default is True

# A follow-up read-back must now return the newly set default.
default_project = await project_repository.get_default_project()
assert default_project is not None
assert default_project.external_id == test_project.external_id


@pytest.mark.asyncio
async def test_set_default_project_not_found(client: AsyncClient, v2_projects_url):
"""Test setting a non-existent project as default returns 404."""
Expand Down
Loading