fix tests for context and permalinks

This commit is contained in:
phernandez
2025-01-18 23:54:28 -06:00
parent 9af2f481b2
commit 32606d771d
41 changed files with 262 additions and 250 deletions
@@ -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.
@@ -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"]]
+2 -1
View File
@@ -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",
+30
View File
@@ -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")
+1 -1
View File
@@ -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
@@ -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())
+3 -5
View File
@@ -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())
-41
View File
@@ -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):
+4 -3
View File
@@ -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):
+5 -5
View File
@@ -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",
-1
View File
@@ -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):
+4 -7
View File
@@ -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,
+1 -1
View File
@@ -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
+2 -6
View File
@@ -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.
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+49 -1
View File
@@ -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)
-1
View File
@@ -1 +0,0 @@
"""Utility functions and helpers."""
+4 -4
View File
@@ -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"),
+22 -22
View File
@@ -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):
+1 -1
View File
@@ -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
+15 -10
View File
@@ -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."""
+4 -4
View File
@@ -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
+14 -16
View File
@@ -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():
+2 -2
View File
@@ -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
+7 -7
View File
@@ -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"
+4 -4
View File
@@ -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"
@@ -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
+4 -4
View File
@@ -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"),
]
@@ -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",
)
+3 -3
View File
@@ -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",
+4 -4
View File
@@ -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:
+3 -3
View File
@@ -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)
+2 -2
View File
@@ -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)
+2 -2
View File
@@ -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",
+1 -1
View File
@@ -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)
+8 -8
View File
@@ -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",
)
+10 -35
View File
@@ -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
assert not changes.deleted
+24 -24
View File
@@ -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
+9 -6
View File
@@ -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"
assert remove_frontmatter(content).strip() == "content"