fix formatting on files

Signed-off-by: phernandez <paul@basicmachines.co>
This commit is contained in:
phernandez
2025-06-25 22:32:05 -05:00
parent 224e4bf9e4
commit 1bf348259b
7 changed files with 65 additions and 63 deletions
+3 -1
View File
@@ -94,7 +94,9 @@ class BasicMemoryConfig(BaseSettings):
"""Ensure configuration is valid after initialization."""
# Ensure main project exists
if "main" not in self.projects: # pragma: no cover
self.projects["main"] = str(Path(os.getenv("BASIC_MEMORY_HOME", Path.home() / "basic-memory")))
self.projects["main"] = str(
Path(os.getenv("BASIC_MEMORY_HOME", Path.home() / "basic-memory"))
)
# Ensure default project is valid
if self.default_project not in self.projects: # pragma: no cover
+14 -10
View File
@@ -17,12 +17,12 @@ async def _detect_cross_project_move_attempt(
identifier: str, destination_path: str, current_project: str
) -> Optional[str]:
"""Detect potential cross-project move attempts and return guidance.
Args:
identifier: The note identifier being moved
destination_path: The destination path
current_project: The current active project
Returns:
Error message with guidance if cross-project move is detected, None otherwise
"""
@@ -31,36 +31,40 @@ async def _detect_cross_project_move_attempt(
response = await call_get(client, "/projects/projects")
project_list = ProjectList.model_validate(response.json())
project_names = [p.name.lower() for p in project_list.projects]
# Check if destination path contains any project names
dest_lower = destination_path.lower()
path_parts = dest_lower.split("/")
# Look for project names in the destination path
for part in path_parts:
if part in project_names and part != current_project.lower():
# Found a different project name in the path
matching_project = next(p.name for p in project_list.projects if p.name.lower() == part)
matching_project = next(
p.name for p in project_list.projects if p.name.lower() == part
)
return _format_cross_project_error_response(
identifier, destination_path, current_project, matching_project
)
# Check if the destination path looks like it might be trying to reference another project
# (e.g., contains common project-like patterns)
if any(keyword in dest_lower for keyword in ["project", "workspace", "repo"]):
# This might be a cross-project attempt, but we can't be sure
# Return a general guidance message
available_projects = [p.name for p in project_list.projects if p.name != current_project]
available_projects = [
p.name for p in project_list.projects if p.name != current_project
]
if available_projects:
return _format_potential_cross_project_guidance(
identifier, destination_path, current_project, available_projects
)
except Exception as e:
# If we can't detect, don't interfere with normal error handling
logger.debug(f"Could not check for cross-project move: {e}")
return None
return None
@@ -116,7 +120,7 @@ def _format_potential_cross_project_guidance(
other_projects = ", ".join(available_projects[:3]) # Show first 3 projects
if len(available_projects) > 3:
other_projects += f" (and {len(available_projects) - 3} others)"
return dedent(f"""
# Move Failed - Check Project Context
+3 -1
View File
@@ -694,7 +694,9 @@ class EntityService(BaseService[EntityModel]):
updates["permalink"] = new_permalink
if old_permalink is None:
logger.info(f"Generated permalink for entity with null permalink: {new_permalink}")
logger.info(
f"Generated permalink for entity with null permalink: {new_permalink}"
)
else:
logger.info(f"Updated permalink: {old_permalink} -> {new_permalink}")
+2 -2
View File
@@ -519,7 +519,7 @@ async def test_move_note_cross_project_detection(mcp_server, app):
"create_memory_project",
{
"project_name": "test-project-b",
"project_path": "/tmp/test-project-b",
"project_path": "/tmp/test-project-b",
"set_default": False,
},
)
@@ -598,7 +598,7 @@ async def test_move_note_potential_cross_project_guidance(mcp_server, app):
assert "switch_project" in error_message
@pytest.mark.asyncio
@pytest.mark.asyncio
async def test_move_note_normal_moves_still_work(mcp_server, app):
"""Test that normal within-project moves still work after cross-project detection."""
@@ -4,7 +4,6 @@ import pytest
from datetime import datetime, timezone
from basic_memory.models.knowledge import Entity
from basic_memory.models.project import Project
from basic_memory.repository.entity_repository import EntityRepository
from basic_memory.repository.project_repository import ProjectRepository
@@ -246,14 +245,14 @@ async def test_upsert_entity_gap_in_suffixes(entity_repository: EntityRepository
# Should get the next available suffix - our implementation finds gaps
# so it should be "test/gap-2" (filling the gap)
assert result.permalink == "test/gap-2"
assert result.permalink == "test/gap-2"
assert result.title == "Gap Filler"
@pytest.mark.asyncio
async def test_upsert_entity_project_scoping_isolation(session_maker):
"""Test that upsert_entity properly scopes entities by project_id.
This test ensures that the fix for issue #167 works correctly by verifying:
1. Entities with same permalinks/file_paths can exist in different projects
2. Upsert operations properly scope queries by project_id
@@ -261,7 +260,7 @@ async def test_upsert_entity_project_scoping_isolation(session_maker):
"""
# Create two separate projects
project_repository = ProjectRepository(session_maker)
project1_data = {
"name": "project-1",
"description": "First test project",
@@ -270,20 +269,20 @@ async def test_upsert_entity_project_scoping_isolation(session_maker):
"is_default": False,
}
project1 = await project_repository.create(project1_data)
project2_data = {
"name": "project-2",
"name": "project-2",
"description": "Second test project",
"path": "/tmp/project2",
"is_active": True,
"is_default": False,
}
project2 = await project_repository.create(project2_data)
# Create entity repositories for each project
repo1 = EntityRepository(session_maker, project_id=project1.id)
repo2 = EntityRepository(session_maker, project_id=project2.id)
# Create entities with identical permalinks and file_paths in different projects
entity1 = Entity(
project_id=project1.id,
@@ -295,22 +294,22 @@ async def test_upsert_entity_project_scoping_isolation(session_maker):
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc),
)
entity2 = Entity(
project_id=project2.id,
title="Shared Entity",
entity_type="note",
entity_type="note",
permalink="docs/shared-name", # Same permalink
file_path="docs/shared-name.md", # Same file_path
content_type="text/markdown",
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc),
)
# These should succeed without "multiple rows" errors
result1 = await repo1.upsert_entity(entity1)
result2 = await repo2.upsert_entity(entity2)
# Verify both entities were created successfully
assert result1.id is not None
assert result2.id is not None
@@ -319,7 +318,7 @@ async def test_upsert_entity_project_scoping_isolation(session_maker):
assert result2.project_id == project2.id
assert result1.permalink == "docs/shared-name"
assert result2.permalink == "docs/shared-name"
# Test updating entities in different projects (should also work without conflicts)
entity1_update = Entity(
project_id=project1.id,
@@ -331,7 +330,7 @@ async def test_upsert_entity_project_scoping_isolation(session_maker):
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc),
)
entity2_update = Entity(
project_id=project2.id,
title="Also Updated Shared Entity",
@@ -342,21 +341,21 @@ async def test_upsert_entity_project_scoping_isolation(session_maker):
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc),
)
# Updates should work without conflicts
updated1 = await repo1.upsert_entity(entity1_update)
updated2 = await repo2.upsert_entity(entity2_update)
# Should update existing entities (same IDs)
assert updated1.id == result1.id
assert updated2.id == result2.id
assert updated1.title == "Updated Shared Entity"
assert updated2.title == "Also Updated Shared Entity"
# Verify cross-project queries don't interfere
found_in_project1 = await repo1.get_by_permalink("docs/shared-name")
found_in_project2 = await repo2.get_by_permalink("docs/shared-name")
assert found_in_project1 is not None
assert found_in_project2 is not None
assert found_in_project1.id == updated1.id
@@ -368,14 +367,14 @@ async def test_upsert_entity_project_scoping_isolation(session_maker):
@pytest.mark.asyncio
async def test_upsert_entity_permalink_conflict_within_project_only(session_maker):
"""Test that permalink conflicts only occur within the same project.
This ensures that the project scoping fix allows entities with identical
permalinks to exist across different projects without triggering
permalinks to exist across different projects without triggering
permalink conflict resolution.
"""
# Create two separate projects
project_repository = ProjectRepository(session_maker)
project1_data = {
"name": "conflict-project-1",
"description": "First conflict test project",
@@ -384,20 +383,20 @@ async def test_upsert_entity_permalink_conflict_within_project_only(session_make
"is_default": False,
}
project1 = await project_repository.create(project1_data)
project2_data = {
"name": "conflict-project-2",
"description": "Second conflict test project",
"description": "Second conflict test project",
"path": "/tmp/conflict2",
"is_active": True,
"is_default": False,
}
project2 = await project_repository.create(project2_data)
# Create entity repositories for each project
repo1 = EntityRepository(session_maker, project_id=project1.id)
repo2 = EntityRepository(session_maker, project_id=project2.id)
# Create first entity in project1
entity1 = Entity(
project_id=project1.id,
@@ -409,10 +408,10 @@ async def test_upsert_entity_permalink_conflict_within_project_only(session_make
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc),
)
result1 = await repo1.upsert_entity(entity1)
assert result1.permalink == "test/conflict-permalink"
# Create entity with same permalink in project2 (should NOT get suffix)
entity2 = Entity(
project_id=project2.id,
@@ -424,11 +423,11 @@ async def test_upsert_entity_permalink_conflict_within_project_only(session_make
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc),
)
result2 = await repo2.upsert_entity(entity2)
# Should keep original permalink (no suffix) since it's in a different project
assert result2.permalink == "test/conflict-permalink"
# Now create entity with same permalink in project1 (should get suffix)
entity3 = Entity(
project_id=project1.id,
@@ -440,11 +439,11 @@ async def test_upsert_entity_permalink_conflict_within_project_only(session_make
created_at=datetime.now(timezone.utc),
updated_at=datetime.now(timezone.utc),
)
result3 = await repo1.upsert_entity(entity3)
# Should get suffix since it conflicts within the same project
assert result3.permalink == "test/conflict-permalink-1"
# Verify all entities exist correctly
assert result1.id != result2.id != result3.id
assert result1.project_id == project1.id
+13 -13
View File
@@ -1737,16 +1737,15 @@ async def test_move_entity_with_null_permalink_generates_permalink(
entity_repository: EntityRepository,
):
"""Test that moving entity with null permalink generates a new permalink automatically.
This tests the fix for issue #155 where entities with null permalinks from the database
migration would fail validation when being moved. The fix ensures that entities with
null permalinks get a generated permalink during move operations, regardless of the
This tests the fix for issue #155 where entities with null permalinks from the database
migration would fail validation when being moved. The fix ensures that entities with
null permalinks get a generated permalink during move operations, regardless of the
update_permalinks_on_move setting.
"""
# Create entity through direct database insertion to simulate migrated entity with null permalink
from basic_memory.models.knowledge import Entity as EntityModel
from datetime import datetime, timezone
# Create an entity with null permalink directly in database (simulating migrated data)
entity_data = {
"title": "Test Entity",
@@ -1757,19 +1756,19 @@ async def test_move_entity_with_null_permalink_generates_permalink(
"created_at": datetime.now(timezone.utc),
"updated_at": datetime.now(timezone.utc),
}
# Create the entity directly in database
created_entity = await entity_repository.create(entity_data)
assert created_entity.permalink is None
# Create the physical file
file_path = project_config.home / created_entity.file_path
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text("# Test Entity\n\nContent here.")
# Configure move without permalink updates (the default setting that previously triggered the bug)
app_config = BasicMemoryConfig(update_permalinks_on_move=False)
# Move entity - this should now succeed and generate a permalink
moved_entity = await entity_service.move_entity(
identifier=created_entity.title, # Use title since permalink is None
@@ -1777,18 +1776,19 @@ async def test_move_entity_with_null_permalink_generates_permalink(
project_config=project_config,
app_config=app_config,
)
# Verify the move succeeded and a permalink was generated
assert moved_entity is not None
assert moved_entity.file_path == "moved/test-entity.md"
assert moved_entity.permalink is not None
assert moved_entity.permalink != ""
# Verify the moved entity can be used to create an EntityResponse without validation errors
from basic_memory.schemas.response import EntityResponse
response = EntityResponse.model_validate(moved_entity)
assert response.permalink == moved_entity.permalink
# Verify the physical file was moved
old_path = project_config.home / "test/null-permalink-entity.md"
new_path = project_config.home / "moved/test-entity.md"
-5
View File
@@ -1,10 +1,5 @@
"""Test configuration management."""
import os
from pathlib import Path
import pytest
from basic_memory.config import BasicMemoryConfig