From 32606d771daed2825a8f16cfe8ee89de6e1bd37b Mon Sep 17 00:00:00 2001 From: phernandez Date: Sat, 18 Jan 2025 23:54:28 -0600 Subject: [PATCH] fix tests for context and permalinks --- .../api/routers/knowledge_router.py | 2 +- src/basic_memory/api/routers/memory_router.py | 3 +- src/basic_memory/{utils => }/file_utils.py | 0 src/basic_memory/markdown/__init__.py | 3 +- src/basic_memory/mcp/server.py | 30 +++++++++++ src/basic_memory/mcp/tools/__init__.py | 2 +- .../mcp/tools/{discussion.py => memory.py} | 6 ++- src/basic_memory/mcp/tools/search.py | 8 ++- src/basic_memory/models/knowledge.py | 41 --------------- src/basic_memory/schemas/base.py | 7 +-- src/basic_memory/schemas/response.py | 10 ++-- src/basic_memory/schemas/search.py | 1 - src/basic_memory/services/context_service.py | 11 ++-- src/basic_memory/services/file_service.py | 2 +- src/basic_memory/services/link_resolver.py | 8 +-- src/basic_memory/sync/entity_sync_service.py | 2 +- src/basic_memory/sync/file_change_scanner.py | 4 +- src/basic_memory/utils.py | 50 ++++++++++++++++++- src/basic_memory/utils/__init__.py | 1 - tests/api/test_discovery_router.py | 8 +-- tests/api/test_knowledge_router.py | 44 ++++++++-------- tests/cli/test_status.py | 2 +- tests/conftest.py | 25 ++++++---- tests/markdown/test_knowledge_writer.py | 8 +-- tests/markdown/test_parser_edge_cases.py | 30 ++++++----- tests/mcp/test_tool_create_entities.py | 4 +- tests/mcp/test_tool_create_relations.py | 14 +++--- tests/mcp/test_tool_get_entity.py | 8 +-- ...tool_discussion.py => test_tool_memory.py} | 4 +- tests/repository/test_entity_repository.py | 8 +-- .../repository/test_observation_repository.py | 12 ++--- tests/repository/test_relation_repository.py | 6 +-- tests/schemas/test_schemas.py | 8 +-- tests/services/test_context_service.py | 6 +-- tests/services/test_file_service.py | 4 +- tests/services/test_relation_service.py | 4 +- tests/services/test_search_service.py | 2 +- tests/sync/test_entity_sync_service.py | 16 +++--- tests/sync/test_file_change_scanner.py | 45 ++++------------- tests/sync/test_sync_service.py | 48 +++++++++--------- tests/utils/test_file_utils.py | 15 +++--- 41 files changed, 262 insertions(+), 250 deletions(-) rename src/basic_memory/{utils => }/file_utils.py (100%) rename src/basic_memory/mcp/tools/{discussion.py => memory.py} (88%) delete mode 100644 src/basic_memory/utils/__init__.py rename tests/mcp/{test_tool_discussion.py => test_tool_memory.py} (94%) diff --git a/src/basic_memory/api/routers/knowledge_router.py b/src/basic_memory/api/routers/knowledge_router.py index b5c89006..0c14d9fc 100644 --- a/src/basic_memory/api/routers/knowledge_router.py +++ b/src/basic_memory/api/routers/knowledge_router.py @@ -119,7 +119,7 @@ async def add_observations( @router.get("/entities/{permalink:path}", response_model=EntityResponse) async def get_entity( entity_service: EntityServiceDep, - permalink: PathId, + permalink: str, ) -> EntityResponse: """Get a specific entity by ID. diff --git a/src/basic_memory/api/routers/memory_router.py b/src/basic_memory/api/routers/memory_router.py index 6b718f9c..6748d932 100644 --- a/src/basic_memory/api/routers/memory_router.py +++ b/src/basic_memory/api/routers/memory_router.py @@ -36,6 +36,7 @@ async def get_memory_context( uri: str, depth: int = 1, timeframe: str = "7d", + max_results: int = 10 ) -> GraphContext: """Get rich context from memory:// URI.""" # add the project name from the config to the url as the "host @@ -46,7 +47,7 @@ async def get_memory_context( since = parse_timeframe(timeframe) # Build context - context = await context_service.build_context(memory_url, depth=depth, since=since) + context = await context_service.build_context(memory_url, depth=depth, since=since, max_results=max_results) primary_entities = [SearchResult(**asdict(r)) for r in context["primary_entities"]] related_entities = [RelatedResult(**asdict(r)) for r in context["related_entities"]] diff --git a/src/basic_memory/utils/file_utils.py b/src/basic_memory/file_utils.py similarity index 100% rename from src/basic_memory/utils/file_utils.py rename to src/basic_memory/file_utils.py diff --git a/src/basic_memory/markdown/__init__.py b/src/basic_memory/markdown/__init__.py index 5c14d43b..06bffc88 100644 --- a/src/basic_memory/markdown/__init__.py +++ b/src/basic_memory/markdown/__init__.py @@ -1,4 +1,6 @@ """Base package for markdown parsing.""" + +from basic_memory.file_utils import ParseError from basic_memory.markdown.entity_parser import EntityParser from basic_memory.markdown.schemas import ( EntityMarkdown, @@ -7,7 +9,6 @@ from basic_memory.markdown.schemas import ( Observation, Relation, ) -from basic_memory.utils.file_utils import ParseError __all__ = [ "EntityMarkdown", diff --git a/src/basic_memory/mcp/server.py b/src/basic_memory/mcp/server.py index c9b818bc..ac0e939d 100644 --- a/src/basic_memory/mcp/server.py +++ b/src/basic_memory/mcp/server.py @@ -1,9 +1,39 @@ """Enhanced FastMCP server instance for Basic Memory.""" +import sys +from loguru import logger from mcp.server.fastmcp import FastMCP from mcp.server.fastmcp.utilities.logging import configure_logging +from basic_memory.config import config + +# mcp console logging configure_logging(level="INFO") + +def setup_logging(home_dir: str = config.home, log_file: str = ".basic-memory/basic-memory.log"): + """Configure file logging to the basic-memory home directory.""" + log = f"{home_dir}/{log_file}" + + # Add file handler with rotation + logger.add( + log, + rotation="100 MB", + retention="10 days", + backtrace=True, + diagnose=True, + enqueue=True, + colorize=False, + ) + + # Add stderr handler + logger.add( + sys.stderr, + colorize=True, + ) + +# start our out file logging +setup_logging() + # Create the shared server instance mcp = FastMCP("Basic Memory") diff --git a/src/basic_memory/mcp/tools/__init__.py b/src/basic_memory/mcp/tools/__init__.py index a809c80e..36364733 100644 --- a/src/basic_memory/mcp/tools/__init__.py +++ b/src/basic_memory/mcp/tools/__init__.py @@ -13,7 +13,7 @@ from basic_memory.mcp.tools import search # noqa: F401 from basic_memory.mcp.tools.activity import ( get_recent_activity, ) -from basic_memory.mcp.tools.discussion import build_context +from basic_memory.mcp.tools.memory import build_context from basic_memory.mcp.tools.ai_edit import ai_edit # Export the tools diff --git a/src/basic_memory/mcp/tools/discussion.py b/src/basic_memory/mcp/tools/memory.py similarity index 88% rename from src/basic_memory/mcp/tools/discussion.py rename to src/basic_memory/mcp/tools/memory.py index f6c74ce6..ece53827 100644 --- a/src/basic_memory/mcp/tools/discussion.py +++ b/src/basic_memory/mcp/tools/memory.py @@ -14,8 +14,9 @@ from basic_memory.schemas.memory import GraphContext, MemoryUrl ) async def build_context( url: MemoryUrl, - depth: Optional[int] = 2, + depth: Optional[int] = 1, timeframe: Optional[str] = "7d", + max_results: int = 10 ) -> GraphContext: """Get context needed to continue a discussion. @@ -28,6 +29,7 @@ async def build_context( url: memory:// URI pointing to discussion content (e.g. memory://specs/search) depth: How many relation hops to traverse (default: 2) timeframe: How far back to look, e.g. "7d", "24h" (default: "7d") + max_results: The maximum number of results to return (default: 10) Returns: GraphContext containing: @@ -39,6 +41,6 @@ async def build_context( # Map directly to the memory endpoint memory_url = MemoryUrl.validate(url) response = await client.get( - f"/memory/{memory_url.relative_path()}", params={"depth": depth, "timeframe": timeframe} + f"/memory/{memory_url.relative_path()}", params={"depth": depth, "timeframe": timeframe, "max_results": max_results} ) return GraphContext.model_validate(response.json()) \ No newline at end of file diff --git a/src/basic_memory/mcp/tools/search.py b/src/basic_memory/mcp/tools/search.py index 683adb7f..b2b85d5d 100644 --- a/src/basic_memory/mcp/tools/search.py +++ b/src/basic_memory/mcp/tools/search.py @@ -1,17 +1,15 @@ """Search tools for Basic Memory MCP server.""" -from mcp.server.fastmcp import Context +from loguru import logger from basic_memory.mcp.server import mcp from basic_memory.schemas.search import SearchQuery, SearchResponse -from basic_memory.schemas.request import GetEntitiesRequest -from basic_memory.schemas.response import EntityListResponse from basic_memory.mcp.async_client import client @mcp.tool( description="Search across all content in basic-memory, including documents and entities", ) -async def search(ctx: Context, query: SearchQuery) -> SearchResponse: +async def search(query: SearchQuery) -> SearchResponse: """Search across all content in basic-memory. Args: @@ -24,6 +22,6 @@ async def search(ctx: Context, query: SearchQuery) -> SearchResponse: Returns: SearchResponse with search results and metadata """ - ctx.info(f"Searching for {query.text}") + logger.info(f"Searching for {query.text}") response = await client.post("/search/", json=query.model_dump()) return SearchResponse.model_validate(response.json()) diff --git a/src/basic_memory/models/knowledge.py b/src/basic_memory/models/knowledge.py index 91ba6c08..6dbdae5d 100644 --- a/src/basic_memory/models/knowledge.py +++ b/src/basic_memory/models/knowledge.py @@ -23,47 +23,6 @@ from basic_memory.models.base import Base from enum import Enum -def generate_permalink(file_path: str) -> str: - """Generate a stable permalink from a file path. - - Args: - file_path: Original file path - - Returns: - Normalized permalink that matches validation rules. Converts spaces and underscores - to hyphens for consistency. - - Examples: - >>> generate_permalink("docs/My Feature.md") - 'docs/my-feature' - >>> generate_permalink("specs/API (v2).md") - 'specs/api-v2' - >>> generate_permalink("design/unified_model_refactor.md") - 'design/unified-model-refactor' - """ - # Remove extension - base = os.path.splitext(file_path)[0] - - # Transliterate unicode to ascii - ascii_text = unidecode(base) - - # Convert to lowercase - lower_text = ascii_text.lower() - - # First replace underscores with hyphens - text_with_hyphens = lower_text.replace('_', '-') - - # Replace remaining invalid chars with hyphens - clean_text = re.sub(r'[^a-z0-9/\-]', '-', text_with_hyphens) - - # Collapse multiple hyphens - clean_text = re.sub(r'-+', '-', clean_text) - - # Clean each path segment - segments = clean_text.split('/') - clean_segments = [s.strip('-') for s in segments] - - return '/'.join(clean_segments) class Entity(Base): diff --git a/src/basic_memory/schemas/base.py b/src/basic_memory/schemas/base.py index 57e25c15..a872f55c 100644 --- a/src/basic_memory/schemas/base.py +++ b/src/basic_memory/schemas/base.py @@ -19,6 +19,8 @@ from typing import List, Optional, Annotated, Dict from annotated_types import MinLen, MaxLen from pydantic import BaseModel, BeforeValidator, Field, model_validator, ValidationError +from basic_memory.utils import generate_permalink + def to_snake_case(name: str) -> str: """Convert a string to snake_case. @@ -79,7 +81,7 @@ class ObservationCategory(str, Enum): return None -PathId = Annotated[str, BeforeValidator(to_snake_case), BeforeValidator(validate_path_format)] +PathId = Annotated[str, BeforeValidator(validate_path_format)] """Unique identifier in format '{path}/{normalized_name}'.""" Observation = Annotated[ @@ -154,8 +156,7 @@ class Entity(BaseModel): @property def permalink(self) -> PathId: """Get the path ID in format {snake_case_title}.""" - normalized_name = to_snake_case(self.title) - return normalized_name + return generate_permalink(self.title) @property def file_path(self): diff --git a/src/basic_memory/schemas/response.py b/src/basic_memory/schemas/response.py index 74b03895..9fee9b21 100644 --- a/src/basic_memory/schemas/response.py +++ b/src/basic_memory/schemas/response.py @@ -56,7 +56,7 @@ class RelationResponse(Relation, SQLAlchemyModel): Example Response: { "from_id": "test/memory_test", - "to_id": "component/memory_service", + "to_id": "component/memory-service", "relation_type": "validates", "context": "Comprehensive test suite" } @@ -91,7 +91,7 @@ class EntityResponse(SQLAlchemyModel): Example Response: { - "permalink": "component/memory_service", + "permalink": "component/memory-service", "title": "MemoryService", "entity_type": "component", "description": "Core persistence service", @@ -109,8 +109,8 @@ class EntityResponse(SQLAlchemyModel): ], "relations": [ { - "from_id": "test/memory_test", - "to_id": "component/memory_service", + "from_id": "test/memory-test", + "to_id": "component/memory-service", "relation_type": "validates", "context": "Main test suite" } @@ -181,7 +181,7 @@ class SearchNodesResponse(SQLAlchemyModel): { "matches": [ { - "permalink": "component/memory_service", + "permalink": "component/memory-service", "title": "MemoryService", "entity_type": "component", "description": "Core service", diff --git a/src/basic_memory/schemas/search.py b/src/basic_memory/schemas/search.py index ebf203cb..f617e7be 100644 --- a/src/basic_memory/schemas/search.py +++ b/src/basic_memory/schemas/search.py @@ -98,7 +98,6 @@ class RelatedResult(BaseModel): relation_type: Optional[str] = None category: Optional[str] = None entity_id: Optional[int] = None - content: Optional[str] = None class SearchResponse(BaseModel): diff --git a/src/basic_memory/services/context_service.py b/src/basic_memory/services/context_service.py index 4b627c03..7344c3ea 100644 --- a/src/basic_memory/services/context_service.py +++ b/src/basic_memory/services/context_service.py @@ -27,7 +27,7 @@ class ContextResultRow: relation_type: Optional[str] = None category: Optional[str] = None entity_id: Optional[int] = None - content: Optional[str] = None + class ContextService: @@ -50,8 +50,9 @@ class ContextService: async def build_context( self, memory_url: MemoryUrl, - depth: int = 2, + depth: int = 1, since: Optional[datetime] = None, + max_results: int = 10 ): """Build rich context from a memory:// URI.""" logger.debug(f"Building context for URI {memory_url}") @@ -136,7 +137,6 @@ class ContextService: relation_type, category, entity_id, - content, 0 as depth, id as root_id, created_at @@ -157,7 +157,6 @@ class ContextService: related.relation_type, related.category, related.entity_id, - related.content, cg.depth + 1, cg.root_id, related.created_at @@ -196,14 +195,13 @@ class ContextService: relation_type, category, entity_id, - content, MIN(depth) as depth, root_id, created_at FROM context_graph GROUP BY type, id, title, permalink, from_id, to_id, - relation_type, category, entity_id, content, + relation_type, category, entity_id, root_id, created_at ORDER BY depth, type, id """) @@ -222,7 +220,6 @@ class ContextService: relation_type=row.relation_type, category=row.category, entity_id=row.entity_id, - content=row.content, depth=row.depth, root_id=row.root_id, created_at=row.created_at, diff --git a/src/basic_memory/services/file_service.py b/src/basic_memory/services/file_service.py index d3622ae2..9b0c41b1 100644 --- a/src/basic_memory/services/file_service.py +++ b/src/basic_memory/services/file_service.py @@ -5,9 +5,9 @@ from typing import Optional, Dict, Any, Tuple from loguru import logger +from basic_memory import file_utils from basic_memory.markdown.knowledge_writer import KnowledgeWriter from basic_memory.services.exceptions import FileOperationError -from basic_memory.utils import file_utils from basic_memory.models import Entity as EntityModel diff --git a/src/basic_memory/services/link_resolver.py b/src/basic_memory/services/link_resolver.py index 5e71a248..b5035a70 100644 --- a/src/basic_memory/services/link_resolver.py +++ b/src/basic_memory/services/link_resolver.py @@ -4,15 +4,13 @@ from typing import Optional, Tuple, List from loguru import logger -from basic_memory.services.service import BaseService from basic_memory.repository.entity_repository import EntityRepository from basic_memory.services.search_service import SearchService from basic_memory.models import Entity -from basic_memory.models.knowledge import generate_permalink from basic_memory.schemas.search import SearchQuery, SearchResult, SearchItemType -class LinkResolver(): +class LinkResolver: """Service for resolving markdown links to permalinks. Uses a combination of exact matching and search-based resolution: @@ -31,8 +29,7 @@ class LinkResolver(): self, link_text: str, ) -> Entity: - """Resolve a markdown link to a permalink. - """ + """Resolve a markdown link to a permalink.""" logger.debug(f"Resolving link: {link_text}") # Clean link text and extract any alias @@ -60,7 +57,6 @@ class LinkResolver(): best_match = self._select_best_match(clean_text, results) logger.debug(f"Selected best match from {len(results)} results: {best_match.permalink}") return await self.entity_repository.get_by_permalink(best_match.permalink) - def _normalize_link_text(self, link_text: str) -> Tuple[str, Optional[str]]: """Normalize link text and extract alias if present. diff --git a/src/basic_memory/sync/entity_sync_service.py b/src/basic_memory/sync/entity_sync_service.py index 8956de7c..c30f99ef 100644 --- a/src/basic_memory/sync/entity_sync_service.py +++ b/src/basic_memory/sync/entity_sync_service.py @@ -5,7 +5,7 @@ from sqlalchemy.exc import IntegrityError from basic_memory.models import Entity as EntityModel, Observation, Relation, ObservationCategory from basic_memory.markdown.schemas import EntityMarkdown -from basic_memory.models.knowledge import generate_permalink +from basic_memory.utils import generate_permalink from basic_memory.repository import EntityRepository, ObservationRepository, RelationRepository from basic_memory.services.exceptions import EntityNotFoundError from basic_memory.services.link_resolver import LinkResolver diff --git a/src/basic_memory/sync/file_change_scanner.py b/src/basic_memory/sync/file_change_scanner.py index 51198816..7125897f 100644 --- a/src/basic_memory/sync/file_change_scanner.py +++ b/src/basic_memory/sync/file_change_scanner.py @@ -6,10 +6,10 @@ from typing import Dict, Sequence from loguru import logger +from basic_memory.file_utils import compute_checksum from basic_memory.models import Entity from basic_memory.repository.entity_repository import EntityRepository from basic_memory.sync.utils import SyncReport -from basic_memory.utils.file_utils import compute_checksum @dataclass @@ -87,7 +87,7 @@ class FileChangeScanner: return result async def find_changes( - self, directory: Path, db_file_state: Dict[str, FileState] + self, directory: Path, db_file_state: Dict[str, FileState] ) -> SyncReport: """Find changes between filesystem and database.""" # Get current files and checksums diff --git a/src/basic_memory/utils.py b/src/basic_memory/utils.py index 1988aefe..210703c5 100644 --- a/src/basic_memory/utils.py +++ b/src/basic_memory/utils.py @@ -1,8 +1,10 @@ """Utility functions for basic-memory.""" - +import os import re import unicodedata +from unidecode import unidecode + def sanitize_name(name: str) -> str: """ @@ -27,3 +29,49 @@ def sanitize_name(name: str) -> str: name = re.sub(r"_+", "_", name).strip("_") return name + + +def generate_permalink(file_path: str) -> str: + """Generate a stable permalink from a file path. + + Args: + file_path: Original file path + + Returns: + Normalized permalink that matches validation rules. Converts spaces and underscores + to hyphens for consistency. + + Examples: + >>> generate_permalink("docs/My Feature.md") + 'docs/my-feature' + >>> generate_permalink("specs/API (v2).md") + 'specs/api-v2' + >>> generate_permalink("design/unified_model_refactor.md") + 'design/unified-model-refactor' + """ + # Remove extension + base = os.path.splitext(file_path)[0] + + # Transliterate unicode to ascii + ascii_text = unidecode(base) + + # Insert dash between camelCase + ascii_text = re.sub(r"([a-z0-9])([A-Z])", r"\1-\2", ascii_text) + + # Convert to lowercase + lower_text = ascii_text.lower() + + # replace underscores with hyphens + text_with_hyphens = lower_text.replace('_', '-') + + # Replace remaining invalid chars with hyphens + clean_text = re.sub(r'[^a-z0-9/\-]', '-', text_with_hyphens) + + # Collapse multiple hyphens + clean_text = re.sub(r'-+', '-', clean_text) + + # Clean each path segment + segments = clean_text.split('/') + clean_segments = [s.strip('-') for s in segments] + + return '/'.join(clean_segments) diff --git a/src/basic_memory/utils/__init__.py b/src/basic_memory/utils/__init__.py deleted file mode 100644 index b8795e3c..00000000 --- a/src/basic_memory/utils/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Utility functions and helpers.""" \ No newline at end of file diff --git a/tests/api/test_discovery_router.py b/tests/api/test_discovery_router.py index 367ff27a..2314a2b2 100644 --- a/tests/api/test_discovery_router.py +++ b/tests/api/test_discovery_router.py @@ -20,7 +20,7 @@ async def test_entities(entity_repository: EntityRepository) -> list[Entity]: entity_type="test", content_type="text/markdown", summary="Core memory service", - permalink="component/memory_service", + permalink="component/memory-service", file_path="component/memory_service.md", observations=[ Observation(category="tech", content="Using SQLite for storage"), @@ -32,7 +32,7 @@ async def test_entities(entity_repository: EntityRepository) -> list[Entity]: entity_type="test", content_type="text/markdown", summary="File format spec", - permalink="spec/file_format", + permalink="spec/file-format", file_path="spec/file_format.md", observations=[ Observation(category="feature", content="Support for frontmatter"), @@ -44,7 +44,7 @@ async def test_entities(entity_repository: EntityRepository) -> list[Entity]: entity_type="test", content_type="text/markdown", summary="Architecture decision", - permalink="decision/tech_choice", + permalink="decision/tech-choice", file_path="decision/tech_choice.md", observations=[ Observation(category="note", content="Team discussed options"), @@ -57,7 +57,7 @@ async def test_entities(entity_repository: EntityRepository) -> list[Entity]: entity_type="test", content_type="text/markdown", summary="API layer", - permalink="component/api_service", + permalink="component/api-service", file_path="component/api_service.md", observations=[ Observation(category="tech", content="FastAPI based"), diff --git a/tests/api/test_knowledge_router.py b/tests/api/test_knowledge_router.py index 0244827f..aaf78937 100644 --- a/tests/api/test_knowledge_router.py +++ b/tests/api/test_knowledge_router.py @@ -69,8 +69,8 @@ async def create_related_entities(client) -> List[RelationResponse]: # pyright: create_response = await client.post("/knowledge/entities", json={"entities": entities}) assert create_response.status_code == 200 created = create_response.json()["entities"] - source_permalink = "source_entity" - target_permalink = "target_entity" + source_permalink = "source-entity" + target_permalink = "target-entity" # Create relation between them response = await client.post( @@ -133,7 +133,7 @@ async def test_get_entity(client: AsyncClient): entity = response.json() assert entity["title"] == "TestEntity" assert entity["entity_type"] == "test" - assert entity["permalink"] == "test_entity" + assert entity["permalink"] == "test-entity" @pytest.mark.asyncio @@ -149,7 +149,7 @@ async def test_add_observations(client: AsyncClient): data = {"title": "TestEntity", "entity_type": "test"} response = await client.post("/knowledge/entities", json={"entities": [data]}) - permalink = "test_entity" + permalink = "test-entity" # Add observations await add_observations(client, permalink) @@ -171,7 +171,7 @@ async def test_get_entities(client: AsyncClient): # Open nodes by path IDs response = await client.get( - "/knowledge/entities?permalink=alpha_test&permalink=beta_test", + "/knowledge/entities?permalink=alpha-test&permalink=beta-test", ) # Verify results @@ -182,12 +182,12 @@ async def test_get_entities(client: AsyncClient): entity_0 = data["entities"][0] assert entity_0["title"] == "AlphaTest" assert entity_0["entity_type"] == "test" - assert entity_0["permalink"] == "alpha_test" + assert entity_0["permalink"] == "alpha-test" entity_1 = data["entities"][1] assert entity_1["title"] == "BetaTest" assert entity_1["entity_type"] == "test" - assert entity_1["permalink"] == "beta_test" + assert entity_1["permalink"] == "beta-test" @pytest.mark.asyncio @@ -240,10 +240,10 @@ async def test_delete_entity_with_observations(client, observation_repository): # Create test entity and add observations entity_data = {"title": "TestEntity", "entity_type": "test"} await client.post("/knowledge/entities", json={"entities": [entity_data]}) - await add_observations(client, "TestEntity") + await add_observations(client, "test-entity") # Delete entity - response = await client.post("/knowledge/entities/delete", json={"permalinks": ["TestEntity"]}) + response = await client.post("/knowledge/entities/delete", json={"permalinks": ["test-entity"]}) assert response.status_code == 200 assert response.json() == {"deleted": True} @@ -258,10 +258,10 @@ async def test_delete_observations(client, observation_repository): # Create entity and add observations entity_data = {"title": "TestEntity", "entity_type": "test"} await client.post("/knowledge/entities", json={"entities": [entity_data]}) - observations = await add_observations(client, "TestEntity") # adds 2 + observations = await add_observations(client, "test-entity") # adds 2 # Delete specific observations - request_data = {"permalink": "TestEntity", "observations": [observations[0].content]} + request_data = {"permalink": "test-entity", "observations": [observations[0].content]} response = await client.post("/knowledge/observations/delete", json=request_data) assert response.status_code == 200 data = response.json() @@ -314,7 +314,7 @@ async def test_delete_nonexistent_observations(client: AsyncClient): entity_data = {"title": "TestEntity", "entity_type": "test"} await client.post("/knowledge/entities", json={"entities": [entity_data]}) - request_data = {"permalink": "TestEntity", "observations": ["Nonexistent observation"]} + request_data = {"permalink": "test-entity", "observations": ["Nonexistent observation"]} response = await client.post("/knowledge/observations/delete", json=request_data) assert response.status_code == 200 @@ -366,13 +366,13 @@ async def test_full_knowledge_flow(client: AsyncClient): json={ "relations": [ { - "from_id": "main_entity", - "to_id": "related_one", + "from_id": "main-entity", + "to_id": "related-one", "relation_type": "connects_to", }, { - "from_id": "main_entity", - "to_id": "related_two", + "from_id": "main-entity", + "to_id": "related-two", "relation_type": "connects_to", }, ] @@ -386,7 +386,7 @@ async def test_full_knowledge_flow(client: AsyncClient): await client.post( "/knowledge/observations", json={ - "permalink": "main_entity", + "permalink": "main-entity", "observations": [ {"content": "Connected to first related entity", "category": "tech"}, {"content": "Connected to second related entity", "category": "note"}, @@ -396,7 +396,7 @@ async def test_full_knowledge_flow(client: AsyncClient): ) # 5. Verify full graph structure - permalink = "MainEntity" + permalink = "main-entity" main_get = await client.get(f"/knowledge/entities/{permalink}") main_entity = main_get.json() @@ -412,13 +412,13 @@ async def test_full_knowledge_flow(client: AsyncClient): # 7. Delete main entity response = await client.post( - "/knowledge/entities/delete", json={"permalinks": ["MainEntity", "NonEntity"]} + "/knowledge/entities/delete", json={"permalinks": ["main-entity", "non-entity"]} ) assert response.status_code == 200 assert response.json() == {"deleted": True} # Verify deletion - permalink = "MainEntity" + permalink = "main-entity" response = await client.get(f"/knowledge/entities/{permalink}") assert response.status_code == 404 @@ -443,7 +443,7 @@ async def test_entity_indexing(client: AsyncClient): assert search_response.status_code == 200 search_result = SearchResponse.model_validate(search_response.json()) assert len(search_result.results) == 1 - assert search_result.results[0].permalink == "search_test" + assert search_result.results[0].permalink == "search-test" assert search_result.results[0].type == SearchItemType.ENTITY.value @@ -513,6 +513,7 @@ async def test_entity_delete_indexing(client: AsyncClient): search_result = SearchResponse.model_validate(search_response.json()) assert len(search_result.results) == 0 + @pytest.mark.skip("relation info is not indexed yet") @pytest.mark.asyncio async def test_relation_indexing(client: AsyncClient): @@ -579,7 +580,6 @@ async def test_update_entity_basic(client: AsyncClient): assert updated["entity_metadata"]["status"] == "draft" # Preserved - @pytest.mark.skip("Skip until we can request content") @pytest.mark.asyncio async def test_update_entity_content(client: AsyncClient): diff --git a/tests/cli/test_status.py b/tests/cli/test_status.py index 847b86af..db9f3b58 100644 --- a/tests/cli/test_status.py +++ b/tests/cli/test_status.py @@ -8,7 +8,7 @@ # from basic_memory.cli.commands.status import display_changes, run_status # from basic_memory.sync.file_change_scanner import FileState # from basic_memory.sync.utils import SyncReport -# from basic_memory.utils.file_utils import compute_checksum +# from basic_memory.file_utils import compute_checksum # # # @pytest.fixture diff --git a/tests/conftest.py b/tests/conftest.py index fb6ab7f8..837e1535 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -171,7 +171,9 @@ async def entity_sync_service( link_resolver: LinkResolver, ) -> EntitySyncService: """Create EntitySyncService with repository.""" - return EntitySyncService(entity_repository, observation_repository, relation_repository, link_resolver) + return EntitySyncService( + entity_repository, observation_repository, relation_repository, link_resolver + ) @pytest_asyncio.fixture @@ -221,7 +223,7 @@ async def sample_entity(entity_repository: EntityRepository) -> Entity: "title": "Test Entity", "entity_type": "test", "summary": "A test entity", - "permalink": "test/test_entity", + "permalink": "test/test-entity", "file_path": "test/test_entity.md", "content_type": "text/markdown", } @@ -232,14 +234,16 @@ async def sample_entity(entity_repository: EntityRepository) -> Entity: async def full_entity(sample_entity, entity_repository): """Create a search test entity.""" - search_entity = await entity_repository.create({ - "title": "Search Entity", - "entity_type": "test", - "summary": "A searchable entity", - "permalink": "test/search_entity", - "file_path": "test/search_entity.md", - "content_type": "text/markdown", - }) + search_entity = await entity_repository.create( + { + "title": "Search Entity", + "entity_type": "test", + "summary": "A searchable entity", + "permalink": "test/search-entity", + "file_path": "test/search_entity.md", + "content_type": "text/markdown", + } + ) observations = [ Observation(content="Tech note", category=ObservationCategory.TECH), @@ -253,6 +257,7 @@ async def full_entity(sample_entity, entity_repository): search_entity.outgoing_relations = relations return await entity_repository.add(search_entity) + @pytest_asyncio.fixture async def test_graph(entity_repository, search_service): """Create a test knowledge graph with entities, relations and observations.""" diff --git a/tests/markdown/test_knowledge_writer.py b/tests/markdown/test_knowledge_writer.py index baa4103c..bf816fbe 100644 --- a/tests/markdown/test_knowledge_writer.py +++ b/tests/markdown/test_knowledge_writer.py @@ -20,7 +20,7 @@ def sample_entity() -> Entity: id=1, title="test_entity", entity_type="test", - permalink="knowledge/test_entity", + permalink="knowledge/test-entity", file_path="knowledge/test_entity.md", summary="Test description", created_at=datetime(2025, 1, 1, tzinfo=UTC), @@ -44,7 +44,7 @@ def entity_with_observations(sample_entity: Entity) -> Entity: def entity_with_relations(sample_entity: Entity) -> Entity: """Create an entity with relations.""" target = Entity( - id=2, title="target_entity", entity_type="test", permalink="knowledge/target_entity" + id=2, title="target_entity", entity_type="test", permalink="knowledge/target-entity" ) sample_entity.outgoing_relations = [ Relation(from_id=1, to_id=2, relation_type="connects_to", to_entity=target) @@ -57,7 +57,7 @@ async def test_format_frontmatter_basic(knowledge_writer: KnowledgeWriter, sampl """Test basic frontmatter formatting.""" frontmatter = await knowledge_writer.format_frontmatter(sample_entity) - assert frontmatter["id"] == "knowledge/test_entity" + assert frontmatter["id"] == "knowledge/test-entity" assert frontmatter["type"] == "test" assert frontmatter["created"] == "2025-01-01T00:00:00+00:00" assert frontmatter["modified"] == "2025-01-02T00:00:00+00:00" @@ -74,7 +74,7 @@ async def test_format_frontmatter_with_metadata( assert frontmatter["status"] == "active" assert frontmatter["priority"] == "high" - assert frontmatter["id"] == "knowledge/test_entity" + assert frontmatter["id"] == "knowledge/test-entity" @pytest.mark.asyncio diff --git a/tests/markdown/test_parser_edge_cases.py b/tests/markdown/test_parser_edge_cases.py index 761b3647..0ca78f22 100644 --- a/tests/markdown/test_parser_edge_cases.py +++ b/tests/markdown/test_parser_edge_cases.py @@ -4,10 +4,8 @@ from pathlib import Path from textwrap import dedent import pytest -from markdown_it import MarkdownIt from basic_memory.markdown.entity_parser import EntityParser -from basic_memory.utils.file_utils import FileError @pytest.mark.asyncio @@ -34,22 +32,22 @@ async def test_unicode_content(tmp_path): - tested_by [[测试组件]] (Unicode test) - depends_on [[компонент]] (Another test) """) - + test_file = tmp_path / "unicode.md" test_file.write_text(content, encoding="utf-8") - + parser = EntityParser(tmp_path) entity = await parser.parse_file(test_file) - + assert "测试" in entity.frontmatter.tags assert "chinese" not in entity.frontmatter.tags assert "🧪" in entity.content.content - + # Verify Unicode in observations assert any(o.content == "Emoji test 👍" for o in entity.content.observations) assert any(o.category == "中文" for o in entity.content.observations) assert any(o.category == "русский" for o in entity.content.observations) - + # Verify Unicode in relations assert any(r.target == "测试组件" for r in entity.content.relations) assert any(r.target == "компонент" for r in entity.content.relations) @@ -60,7 +58,7 @@ async def test_empty_file(tmp_path): """Test handling of empty files.""" empty_file = tmp_path / "empty.md" empty_file.write_text("") - + parser = EntityParser(tmp_path) entity = await parser.parse_file(empty_file) assert entity.content.observations == [] @@ -82,10 +80,10 @@ async def test_missing_sections(tmp_path): Just some content with [[links]] but no sections """) - + test_file = tmp_path / "missing.md" test_file.write_text(content) - + parser = EntityParser(tmp_path) entity = await parser.parse_file(test_file) assert len(entity.content.relations) == 1 @@ -145,13 +143,13 @@ async def test_nested_content(tmp_path): - [test] Level 3 #test (Third level) - needs [[Three]] """) - + test_file = tmp_path / "nested.md" test_file.write_text(content) - + parser = EntityParser(tmp_path) entity = await parser.parse_file(test_file) - + # Should find all observations and relations regardless of nesting assert len(entity.content.observations) == 3 assert len(entity.content.relations) == 3 @@ -169,14 +167,14 @@ async def test_malformed_frontmatter(tmp_path): # Test """) - + test_file = tmp_path / "malformed.md" test_file.write_text(content) - + parser = EntityParser(tmp_path) entity = await parser.parse_file(test_file) assert entity.frontmatter.permalink is None - + @pytest.mark.asyncio async def test_file_not_found(): diff --git a/tests/mcp/test_tool_create_entities.py b/tests/mcp/test_tool_create_entities.py index 10c5473f..760de0de 100644 --- a/tests/mcp/test_tool_create_entities.py +++ b/tests/mcp/test_tool_create_entities.py @@ -30,7 +30,7 @@ async def test_create_basic_entity(client): entity = result.entities[0] assert entity.title == "TestEntity" assert entity.entity_type == "test" - assert entity.permalink == "test_entity" + assert entity.permalink == "test-entity" assert entity.summary == "A test entity" # Check observations @@ -122,7 +122,7 @@ async def test_create_minimal_entity(client): entity = result.entities[0] assert entity.title == "MinimalEntity" assert entity.entity_type == "test" - assert entity.permalink == "minimal_entity" + assert entity.permalink == "minimal-entity" assert entity.summary is None assert len(entity.observations) == 0 assert len(entity.relations) == 0 diff --git a/tests/mcp/test_tool_create_relations.py b/tests/mcp/test_tool_create_relations.py index 5cb31146..8ad62c12 100644 --- a/tests/mcp/test_tool_create_relations.py +++ b/tests/mcp/test_tool_create_relations.py @@ -22,7 +22,7 @@ async def test_create_basic_relation(client): # Create relation between them relation_request = CreateRelationsRequest( relations=[ - Relation(from_id="source_entity", to_id="target_entity", relation_type="depends_on") + Relation(from_id="source-entity", to_id="target-entity", relation_type="depends_on") ] ) result = await create_relations(relation_request) @@ -30,8 +30,8 @@ async def test_create_basic_relation(client): assert len(result.entities) == 2 # Find source and target entities - source = next(e for e in result.entities if e.permalink == "source_entity") - target = next(e for e in result.entities if e.permalink == "target_entity") + source = next(e for e in result.entities if e.permalink == "source-entity") + target = next(e for e in result.entities if e.permalink == "target-entity") # Both entities should have the relation for bi-directional navigation assert len(source.relations) == 1 @@ -39,14 +39,14 @@ async def test_create_basic_relation(client): # Source's relation shows it depends_on target source_relation = source.relations[0] - assert source_relation.from_id == "source_entity" - assert source_relation.to_id == "target_entity" + assert source_relation.from_id == "source-entity" + assert source_relation.to_id == "target-entity" assert source_relation.relation_type == "depends_on" # Target's relation is the same, allowing backwards traversal target_relation = target.relations[0] - assert target_relation.from_id == "source_entity" - assert target_relation.to_id == "target_entity" + assert target_relation.from_id == "source-entity" + assert target_relation.to_id == "target-entity" assert target_relation.relation_type == "depends_on" diff --git a/tests/mcp/test_tool_get_entity.py b/tests/mcp/test_tool_get_entity.py index 20a28b5b..c52fd6ca 100644 --- a/tests/mcp/test_tool_get_entity.py +++ b/tests/mcp/test_tool_get_entity.py @@ -31,7 +31,7 @@ async def test_get_basic_entity(client): # Verify entity details assert entity.title == "TestEntity" assert entity.entity_type == "test" - assert entity.permalink == "test_entity" + assert entity.permalink == "test-entity" assert entity.summary == "A test entity" # Check observations @@ -61,16 +61,16 @@ async def test_get_entity_with_relations(client): relation_request = CreateRelationsRequest( relations=[ - Relation(from_id="source_entity", to_id="target_entity", relation_type="depends_on") + Relation(from_id="source-entity", to_id="target-entity", relation_type="depends_on") ] ) await create_relations(relation_request) # Get and verify source entity without content - source = await get_entity("source_entity") + source = await get_entity("source-entity") assert len(source.relations) == 1 relation = source.relations[0] - assert relation.to_id == "target_entity" + assert relation.to_id == "target-entity" assert relation.relation_type == "depends_on" diff --git a/tests/mcp/test_tool_discussion.py b/tests/mcp/test_tool_memory.py similarity index 94% rename from tests/mcp/test_tool_discussion.py rename to tests/mcp/test_tool_memory.py index 8099c2d6..567ad340 100644 --- a/tests/mcp/test_tool_discussion.py +++ b/tests/mcp/test_tool_memory.py @@ -1,7 +1,7 @@ """Tests for discussion context MCP tool.""" import pytest -from basic_memory.mcp.tools.discussion import build_context +from basic_memory.mcp.tools.memory import build_context from basic_memory.schemas.memory import GraphContext @pytest.mark.asyncio @@ -18,7 +18,7 @@ async def test_get_basic_discussion_context(client, test_graph): # Verify metadata assert context.metadata["uri"] == "test/root" - assert context.metadata["depth"] == 2 # default depth + assert context.metadata["depth"] == 1 # default depth assert context.metadata["timeframe"] is not None assert isinstance(context.metadata["generated_at"], str) assert context.metadata["matched_entities"] == 1 diff --git a/tests/repository/test_entity_repository.py b/tests/repository/test_entity_repository.py index 1b5477b8..c50f2207 100644 --- a/tests/repository/test_entity_repository.py +++ b/tests/repository/test_entity_repository.py @@ -8,7 +8,7 @@ from sqlalchemy import select from basic_memory import db from basic_memory.models import Entity, Observation, Relation -from basic_memory.models.knowledge import generate_permalink +from basic_memory.utils import generate_permalink from basic_memory.repository.entity_repository import EntityRepository @@ -94,7 +94,7 @@ async def test_create_all(entity_repository: EntityRepository): { "title": "Test_1", "entity_type": "test", - "permalink": "test/test_1", + "permalink": "test/test-1", "file_path": "test/test_1.md", "summary": "Test description", "content_type": "text/markdown", @@ -102,7 +102,7 @@ async def test_create_all(entity_repository: EntityRepository): { "title": "Test-2", "entity_type": "test", - "permalink": "test/test_2", + "permalink": "test/test-2", "file_path": "test/test_2.md", "summary": "Test description", "content_type": "text/markdown", @@ -471,7 +471,7 @@ async def test_generate_permalink_from_file_path(): ("specs/API (v2).md", "specs/api-v2"), ("notes/2024/Q1 Planning!!!.md", "notes/2024/q1-planning"), ("test/Über File.md", "test/uber-file"), - ("docs/my_feature_name.md", "docs/my_feature_name"), + ("docs/my_feature_name.md", "docs/my-feature-name"), ("specs/multiple--dashes.md", "specs/multiple-dashes"), ("notes/trailing/space/ file.md", "notes/trailing/space/file"), ] diff --git a/tests/repository/test_observation_repository.py b/tests/repository/test_observation_repository.py index 60e1c2d3..16a7ff84 100644 --- a/tests/repository/test_observation_repository.py +++ b/tests/repository/test_observation_repository.py @@ -91,7 +91,7 @@ async def test_delete_observations(session_maker: async_sessionmaker, repo): title="test_entity", entity_type="test", summary="Test entity", - permalink="test/test_entity", + permalink="test/test-entity", file_path="test/test_entity.md", content_type="text/markdown", ) @@ -121,7 +121,7 @@ async def test_delete_observation_by_id(session_maker: async_sessionmaker, repo) title="test_entity", entity_type="test", summary="Test entity", - permalink="test/test_entity", + permalink="test/test-entity", file_path="test/test_entity.md", content_type="text/markdown", ) @@ -150,7 +150,7 @@ async def test_delete_observation_by_content(session_maker: async_sessionmaker, title="test_entity", entity_type="test", summary="Test entity", - permalink="test/test_entity", + permalink="test/test-entity", file_path="test/test_entity.md", content_type="text/markdown", ) @@ -181,7 +181,7 @@ async def test_find_by_category(session_maker: async_sessionmaker, repo): title="test_entity", entity_type="test", summary="Test entity", - permalink="test/test_entity", + permalink="test/test-entity", file_path="test/test_entity.md", content_type="text/markdown", ) @@ -223,7 +223,7 @@ async def test_observation_categories(session_maker: async_sessionmaker, repo): title="test_entity", entity_type="test", summary="Test entity", - permalink="test/test_entity", + permalink="test/test-entity", file_path="test/test_entity.md", content_type="text/markdown", ) @@ -271,7 +271,7 @@ async def test_find_by_category_case_sensitivity(session_maker: async_sessionmak title="test_entity", entity_type="test", summary="Test entity", - permalink="test/test_entity", + permalink="test/test-entity", file_path="test/test_entity.md", content_type="text/markdown", ) diff --git a/tests/repository/test_relation_repository.py b/tests/repository/test_relation_repository.py index ec70d19d..51d6dfa1 100644 --- a/tests/repository/test_relation_repository.py +++ b/tests/repository/test_relation_repository.py @@ -15,7 +15,7 @@ async def source_entity(session_maker): entity = Entity( title="test_source", entity_type="test", - permalink="source/test_source", + permalink="source/test-source", file_path="source/test_source.md", summary="Source entity", content_type="text/markdown", @@ -32,7 +32,7 @@ async def target_entity(session_maker): entity = Entity( title="test_target", entity_type="test", - permalink="target/test_target", + permalink="target/test-target", file_path="target/test_target.md", summary="Target entity", content_type="text/markdown", @@ -62,7 +62,7 @@ async def related_entity(entity_repository): entity_data = { "title": "Related Entity", "entity_type": "test", - "permalink": "test/related_entity", + "permalink": "test/related-entity", "file_path": "test/related_entity.md", "summary": "A related test entity", "content_type": "text/markdown", diff --git a/tests/schemas/test_schemas.py b/tests/schemas/test_schemas.py index 4d1b859a..52b1bb91 100644 --- a/tests/schemas/test_schemas.py +++ b/tests/schemas/test_schemas.py @@ -201,10 +201,10 @@ def test_path_sanitization(): def test_permalink_generation(): """Test permalink property generates correct paths.""" test_cases = [ - ({"title": "BasicMemory", "entity_type": "knowledge"}, "basic_memory"), - ({"title": "Memory Service", "entity_type": "knowledge"}, "memory_service"), - ({"title": "API Gateway", "entity_type": "knowledge"}, "api_gateway"), - ({"title": "TestCase1", "entity_type": "knowledge"}, "test_case1"), + ({"title": "BasicMemory", "entity_type": "knowledge"}, "basic-memory"), + ({"title": "Memory Service", "entity_type": "knowledge"}, "memory-service"), + ({"title": "API Gateway", "entity_type": "knowledge"}, "api-gateway"), + ({"title": "TestCase1", "entity_type": "knowledge"}, "test-case1"), ] for input_data, expected_path in test_cases: diff --git a/tests/services/test_context_service.py b/tests/services/test_context_service.py index 552c5a95..c8c8f41f 100644 --- a/tests/services/test_context_service.py +++ b/tests/services/test_context_service.py @@ -40,7 +40,7 @@ async def test_find_connected_basic(context_service, test_graph, search_service) assert test_graph["connected2"].id in entity_ids # Verify we found observations - assert any(r.type == "observation" and "Root note 1" in r.content for r in results) + assert any(r.type == "observation" and "Root" in r.title for r in results) @pytest.mark.asyncio @@ -149,10 +149,10 @@ async def test_build_context(context_service, test_graph): total_entities = results["metadata"]["total_entities"] assert results["metadata"]["uri"] == url.relative_path() - assert results["metadata"]["depth"] == 2 + assert results["metadata"]["depth"] == 1 assert matched_entities == 1 assert len(primary_entities) == 1 - assert len(related_entities) == 10 + assert len(related_entities) == 8 assert total_entities == len(primary_entities) + len(related_entities) diff --git a/tests/services/test_file_service.py b/tests/services/test_file_service.py index 9cf4fd81..26ff8ea2 100644 --- a/tests/services/test_file_service.py +++ b/tests/services/test_file_service.py @@ -78,7 +78,7 @@ async def test_write_atomic(tmp_path: Path, file_service: FileService): temp_path = test_path.with_suffix(".tmp") # Mock write_file_atomic to raise an error - with patch("basic_memory.utils.file_utils.write_file_atomic") as mock_write: + with patch("basic_memory.file_utils.write_file_atomic") as mock_write: mock_write.side_effect = Exception("Write failed") # Attempt write that will fail @@ -195,7 +195,7 @@ async def test_frontmatter_invalid_metadata(file_service: FileService): bad_metadata = {"bad": NonSerializable()} # Attempting to add frontmatter with non-serializable content - with patch("basic_memory.utils.file_utils.add_frontmatter") as mock_add: + with patch("basic_memory.file_utils.add_frontmatter") as mock_add: mock_add.side_effect = FileOperationError("Failed to serialize metadata") with pytest.raises(FileOperationError): await file_service.add_frontmatter(frontmatter=frontmatter, content="content", metadata=bad_metadata) diff --git a/tests/services/test_relation_service.py b/tests/services/test_relation_service.py index fbf6288f..c4e71961 100644 --- a/tests/services/test_relation_service.py +++ b/tests/services/test_relation_service.py @@ -19,7 +19,7 @@ async def test_entities( entity1 = EntityModel( title="test_entity_1", entity_type="test", - permalink="test/test_entity_1", + permalink="test/test-entity-1", file_path="test/test_entity_1.md", summary="Test entity 1", content_type="text/markdown", @@ -27,7 +27,7 @@ async def test_entities( entity2 = EntityModel( title="test_entity_2", entity_type="test", - permalink="test/test_entity_2", + permalink="test/test-entity-2", file_path="test/test_entity_2.md", summary="Test entity 2", content_type="text/markdown", diff --git a/tests/services/test_search_service.py b/tests/services/test_search_service.py index 1080c047..31332122 100644 --- a/tests/services/test_search_service.py +++ b/tests/services/test_search_service.py @@ -61,7 +61,7 @@ async def test_text_search_features(search_service, test_graph): assert any(r.file_path == "test/connected2.md" for r in results) # Multiple terms - results = await search_service.search(SearchQuery(text="root connected")) + results = await search_service.search(SearchQuery(text="root note")) assert any("test/root" in r.permalink for r in results) diff --git a/tests/sync/test_entity_sync_service.py b/tests/sync/test_entity_sync_service.py index e5e87800..2bc16529 100644 --- a/tests/sync/test_entity_sync_service.py +++ b/tests/sync/test_entity_sync_service.py @@ -22,7 +22,7 @@ def test_frontmatter() -> EntityFrontmatter: return EntityFrontmatter( title="Test Entity", type="knowledge", - permalink="concept/test_entity", + permalink="concept/test-entity", created=datetime.now(), modified=datetime.now(), tags=["test", "sync"], @@ -39,8 +39,8 @@ def test_content() -> EntityContent: MarkdownObservation(content="Second observation"), ], relations=[ - MarkdownRelation(type="depends_on", target="concept/other_entity"), - MarkdownRelation(type="related_to", target="concept/another_entity"), + MarkdownRelation(type="depends_on", target="concept/other-entity"), + MarkdownRelation(type="related_to", target="concept/another-entity"), ], ) @@ -62,7 +62,7 @@ async def test_create_entity_without_relations( # Check basic fields assert entity.title == "Test Entity" assert entity.entity_type == "knowledge" - assert entity.permalink == "concept/test_entity" + assert entity.permalink == "concept/test-entity" assert entity.summary == "A test entity description" # Check observations @@ -117,14 +117,14 @@ async def test_update_entity_relations( other_entity = EntityModel( title="Other Entity", entity_type="test", - permalink="concept/other_entity", + permalink="concept/other-entity", file_path="concept/other_entity.md", content_type="text/markdown", ) another_entity = EntityModel( title="Another Entity", entity_type="test", - permalink="concept/another_entity", + permalink="concept/another-entity", file_path="concept/another_entity.md", content_type="text/markdown", ) @@ -159,14 +159,14 @@ async def test_two_pass_sync_flow( other_entity = EntityModel( title="Other Entity", entity_type="test", - permalink="concept/other_entity", + permalink="concept/other-entity", file_path="concept/other_entity.md", content_type="text/markdown", ) another_entity = EntityModel( title="Another Entity", entity_type="test", - permalink="concept/another_entity", + permalink="concept/another-entity", file_path="concept/another_entity.md", content_type="text/markdown", ) diff --git a/tests/sync/test_file_change_scanner.py b/tests/sync/test_file_change_scanner.py index ddaf7574..60fd3cfc 100644 --- a/tests/sync/test_file_change_scanner.py +++ b/tests/sync/test_file_change_scanner.py @@ -4,10 +4,10 @@ from pathlib import Path import pytest +from basic_memory.file_utils import compute_checksum from basic_memory.models import Entity from basic_memory.sync import FileChangeScanner from basic_memory.sync.file_change_scanner import FileState -from basic_memory.utils.file_utils import compute_checksum @pytest.fixture @@ -158,11 +158,7 @@ async def test_detect_moved_file(file_change_scanner: FileChangeScanner, temp_di # Set up DB state with original location db_records = { - old_path: FileState( - file_path=old_path, - permalink="test", - checksum=original_checksum - ) + old_path: FileState(file_path=old_path, permalink="test", checksum=original_checksum) } # Move file to new location @@ -172,10 +168,7 @@ async def test_detect_moved_file(file_change_scanner: FileChangeScanner, temp_di old_file.rename(new_file) # Check changes - changes = await file_change_scanner.find_changes( - directory=temp_dir, - db_file_state=db_records - ) + changes = await file_change_scanner.find_changes(directory=temp_dir, db_file_state=db_records) # Should detect as move assert len(changes.moves) == 1 @@ -199,11 +192,7 @@ async def test_move_with_content_change(file_change_scanner: FileChangeScanner, # Set up DB state with original location db_records = { - old_path: FileState( - file_path=old_path, - permalink="test", - checksum=original_checksum - ) + old_path: FileState(file_path=old_path, permalink="test", checksum=original_checksum) } # Move file and change content @@ -214,10 +203,7 @@ async def test_move_with_content_change(file_change_scanner: FileChangeScanner, old_file.unlink() # Check changes - changes = await file_change_scanner.find_changes( - directory=temp_dir, - db_file_state=db_records - ) + changes = await file_change_scanner.find_changes(directory=temp_dir, db_file_state=db_records) # Should be treated as delete + new, not move assert old_path in changes.deleted @@ -229,14 +215,8 @@ async def test_move_with_content_change(file_change_scanner: FileChangeScanner, async def test_multiple_moves(file_change_scanner: FileChangeScanner, temp_dir: Path): """Test detecting multiple file moves at once.""" # Create original files - files = { - "a/test1.md": "content1", - "b/test2.md": "content2" - } - new_locations = { - "a/test1.md": "new/test1.md", - "b/test2.md": "new/nested/test2.md" - } + files = {"a/test1.md": "content1", "b/test2.md": "content2"} + new_locations = {"a/test1.md": "new/test1.md", "b/test2.md": "new/nested/test2.md"} db_records = {} # Create files and DB state @@ -244,9 +224,7 @@ async def test_multiple_moves(file_change_scanner: FileChangeScanner, temp_dir: await create_test_file(temp_dir / old_path, content) checksum = await compute_checksum(content) db_records[old_path] = FileState( - file_path=old_path, - permalink=old_path.replace(".md", ""), - checksum=checksum + file_path=old_path, permalink=old_path.replace(".md", ""), checksum=checksum ) # Move all files @@ -257,14 +235,11 @@ async def test_multiple_moves(file_change_scanner: FileChangeScanner, temp_dir: old_file.rename(new_file) # Check changes - changes = await file_change_scanner.find_changes( - directory=temp_dir, - db_file_state=db_records - ) + changes = await file_change_scanner.find_changes(directory=temp_dir, db_file_state=db_records) # Should detect both moves assert len(changes.moves) == 2 assert changes.moves["a/test1.md"] == "new/test1.md" assert changes.moves["b/test2.md"] == "new/nested/test2.md" assert not changes.new - assert not changes.deleted \ No newline at end of file + assert not changes.deleted diff --git a/tests/sync/test_sync_service.py b/tests/sync/test_sync_service.py index 3288b986..c192c8d4 100644 --- a/tests/sync/test_sync_service.py +++ b/tests/sync/test_sync_service.py @@ -29,7 +29,7 @@ async def test_sync_knowledge( new_content = """ --- type: knowledge -permalink: concept/test_concept +permalink: concept/test-concept created: 2023-01-01 modified: 2023-01-01 --- @@ -65,7 +65,7 @@ A test concept. assert len(entities) == 1 # Find new entity - test_concept = next(e for e in entities if e.permalink == "concept/test_concept") + test_concept = next(e for e in entities if e.permalink == "concept/test-concept") assert test_concept.entity_type == "knowledge" # Verify relation was not created @@ -86,7 +86,7 @@ async def test_sync_entity_with_nonexistent_relations( content = """ --- type: knowledge -permalink: concept/depends_on_future +permalink: concept/depends-on-future created: 2024-01-01 modified: 2024-01-01 --- @@ -106,7 +106,7 @@ modified: 2024-01-01 # Verify entity created but no relations entity = await sync_service.entity_sync_service.entity_repository.get_by_permalink( - "concept/depends_on_future" + "concept/depends-on-future" ) assert entity is not None assert len(entity.relations) == 0 # Relations to nonexistent entities should be skipped @@ -123,7 +123,7 @@ async def test_sync_entity_circular_relations( content_a = """ --- type: knowledge -permalink: concept/entity_a +permalink: concept/entity-a created: 2024-01-01 modified: 2024-01-01 --- @@ -133,7 +133,7 @@ modified: 2024-01-01 - First entity in circular reference ## Relations -- depends_on [[concept/entity_b]] +- depends_on [[concept/entity-b]] """ await create_test_file(project_dir / "concept/entity_a.md", content_a) @@ -141,7 +141,7 @@ modified: 2024-01-01 content_b = """ --- type: knowledge -permalink: concept/entity_b +permalink: concept/entity-b created: 2024-01-01 modified: 2024-01-01 --- @@ -151,7 +151,7 @@ modified: 2024-01-01 - Second entity in circular reference ## Relations -- depends_on [[concept/entity_a]] +- depends_on [[concept/entity-a]] """ await create_test_file(project_dir / "concept/entity_b.md", content_b) @@ -160,10 +160,10 @@ modified: 2024-01-01 # Verify both entities and their relations entity_a = await sync_service.entity_sync_service.entity_repository.get_by_permalink( - "concept/entity_a" + "concept/entity-a" ) entity_b = await sync_service.entity_sync_service.entity_repository.get_by_permalink( - "concept/entity_b" + "concept/entity-b" ) # outgoing relations @@ -213,7 +213,7 @@ modified: 2024-01-01 content = """ --- type: knowledge -permalink: concept/duplicate_relations +permalink: concept/duplicate-relations created: 2024-01-01 modified: 2024-01-01 --- @@ -235,7 +235,7 @@ modified: 2024-01-01 # Verify duplicates are handled entity = await sync_service.entity_sync_service.entity_repository.get_by_permalink( - "concept/duplicate_relations" + "concept/duplicate-relations" ) # Count relations by type @@ -258,7 +258,7 @@ async def test_sync_entity_with_invalid_category( content = """ --- type: knowledge -permalink: concept/invalid_category +permalink: concept/invalid-category created: 2024-01-01 modified: 2024-01-01 --- @@ -277,7 +277,7 @@ modified: 2024-01-01 # Verify observations entity = await sync_service.entity_sync_service.entity_repository.get_by_permalink( - "concept/invalid_category" + "concept/invalid-category" ) assert len(entity.observations) == 3 @@ -301,7 +301,7 @@ async def test_sync_entity_with_order_dependent_relations( "a": """ --- type: knowledge -permalink: concept/entity_a +permalink: concept/entity-a created: 2024-01-01 modified: 2024-01-01 --- @@ -312,13 +312,13 @@ modified: 2024-01-01 - depends on c ## Relations -- depends_on [[concept/entity_b]] -- depends_on [[concept/entity_c]] +- depends_on [[concept/entity-b]] +- depends_on [[concept/entity-c]] """, "b": """ --- type: knowledge -permalink: concept/entity_b +permalink: concept/entity-b created: 2024-01-01 modified: 2024-01-01 --- @@ -328,12 +328,12 @@ modified: 2024-01-01 - depends on c ## Relations -- depends_on [[concept/entity_c]] +- depends_on [[concept/entity-c]] """, "c": """ --- type: knowledge -permalink: concept/entity_c +permalink: concept/entity-c created: 2024-01-01 modified: 2024-01-01 --- @@ -343,7 +343,7 @@ modified: 2024-01-01 - depends on a ## Relations -- depends_on [[concept/entity_a]] +- depends_on [[concept/entity-a]] """, } @@ -356,13 +356,13 @@ modified: 2024-01-01 # Verify all relations are created correctly regardless of order entity_a = await sync_service.entity_sync_service.entity_repository.get_by_permalink( - "concept/entity_a" + "concept/entity-a" ) entity_b = await sync_service.entity_sync_service.entity_repository.get_by_permalink( - "concept/entity_b" + "concept/entity-b" ) entity_c = await sync_service.entity_sync_service.entity_repository.get_by_permalink( - "concept/entity_c" + "concept/entity-c" ) assert len(entity_a.outgoing_relations) == 2 # Should depend on B and C diff --git a/tests/utils/test_file_utils.py b/tests/utils/test_file_utils.py index d67d639e..4722a983 100644 --- a/tests/utils/test_file_utils.py +++ b/tests/utils/test_file_utils.py @@ -4,7 +4,7 @@ from pathlib import Path import pytest -from basic_memory.utils.file_utils import ( +from basic_memory.file_utils import ( compute_checksum, ensure_directory, write_file_atomic, @@ -86,7 +86,7 @@ async def test_add_frontmatter(): assert "- a\n- b" in result or "['a', 'b']" in result # Should preserve content - assert result.endswith(f"test content") + assert result.endswith("test content") def test_has_frontmatter(): @@ -127,7 +127,7 @@ tags: - b --- content""" - + result = parse_frontmatter(content) assert result == {"title": "Test", "tags": ["a", "b"]} @@ -177,9 +177,12 @@ title: Test assert remove_frontmatter(content) == "" # frontmatter missing some fields - assert remove_frontmatter("""--- + assert ( + remove_frontmatter("""--- title: Test -content""") == "---\ntitle: Test\ncontent" +content""") + == "---\ntitle: Test\ncontent" + ) @pytest.mark.asyncio @@ -248,4 +251,4 @@ content""" title: Test --- content """ - assert remove_frontmatter(content).strip() == "content" \ No newline at end of file + assert remove_frontmatter(content).strip() == "content"