mirror of
https://github.com/basicmachines-co/basic-memory
synced 2026-06-21 13:47:35 +00:00
fix formatting on files
Signed-off-by: phernandez <paul@basicmachines.co>
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
"""Test configuration management."""
|
||||
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from basic_memory.config import BasicMemoryConfig
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user