fix tests

This commit is contained in:
phernandez
2025-01-07 20:29:59 -06:00
parent 6686c331a5
commit 1c6836d83f
37 changed files with 596 additions and 780 deletions
@@ -11,8 +11,6 @@ from basic_memory.deps import (
from basic_memory.schemas import (
CreateEntityRequest,
EntityListResponse,
SearchNodesRequest,
SearchNodesResponse,
CreateRelationsRequest,
EntityResponse,
AddObservationsRequest,
@@ -23,7 +21,7 @@ from basic_memory.schemas import (
DeleteEntitiesRequest,
UpdateEntityRequest,
)
from basic_memory.schemas.base import PathId, EntityType
from basic_memory.schemas.base import PathId
from basic_memory.services.exceptions import EntityNotFoundError
router = APIRouter(prefix="/knowledge", tags=["knowledge"])
@@ -36,15 +34,15 @@ async def create_entities(
data: CreateEntityRequest,
background_tasks: BackgroundTasks,
knowledge_service: KnowledgeServiceDep,
search_service = Depends(get_search_service)
search_service=Depends(get_search_service),
) -> EntityListResponse:
"""Create new entities in the knowledge graph and index them."""
entities = await knowledge_service.create_entities(data.entities)
# Index each entity
for entity in entities:
await search_service.index_entity(entity, background_tasks=background_tasks)
return EntityListResponse(
entities=[EntityResponse.model_validate(entity) for entity in entities]
)
@@ -56,21 +54,21 @@ async def update_entity(
data: UpdateEntityRequest,
background_tasks: BackgroundTasks,
knowledge_service: KnowledgeServiceDep,
search_service = Depends(get_search_service)
search_service=Depends(get_search_service),
) -> EntityResponse:
"""Update an existing entity and reindex it."""
try:
# Convert request to dict, excluding None values
update_data = data.model_dump(exclude_none=True)
# Update the entity
updated_entity = await knowledge_service.update_entity(path_id, **update_data)
# Reindex since content changed
await search_service.index_entity(updated_entity, background_tasks=background_tasks)
return EntityResponse.model_validate(updated_entity)
except EntityNotFoundError:
raise HTTPException(status_code=404, detail=f"Entity with {path_id} not found")
@@ -80,15 +78,15 @@ async def create_relations(
data: CreateRelationsRequest,
background_tasks: BackgroundTasks,
knowledge_service: KnowledgeServiceDep,
search_service = Depends(get_search_service),
search_service=Depends(get_search_service),
) -> EntityListResponse:
"""Create relations between entities and update search index."""
updated_entities = await knowledge_service.create_relations(data.relations)
# Reindex updated entities since relations have changed
for entity in updated_entities:
await search_service.index_entity(entity, background_tasks=background_tasks)
return EntityListResponse(
entities=[EntityResponse.model_validate(entity) for entity in updated_entities]
)
@@ -99,17 +97,17 @@ async def add_observations(
data: AddObservationsRequest,
background_tasks: BackgroundTasks,
knowledge_service: KnowledgeServiceDep,
search_service = Depends(get_search_service)
search_service=Depends(get_search_service),
) -> EntityResponse:
"""Add observations to an entity and update search index."""
logger.debug(f"Adding observations to entity: {data.path_id}")
updated_entity = await knowledge_service.add_observations(
data.path_id, data.observations, data.context
)
# Reindex the entity with new observations
await search_service.index_entity(updated_entity, background_tasks=background_tasks)
return EntityResponse.model_validate(updated_entity)
@@ -122,9 +120,9 @@ async def get_entity(path_id: PathId, knowledge_service: KnowledgeServiceDep) ->
try:
entity = await knowledge_service.get_entity_by_path_id(path_id)
entity_response = EntityResponse.model_validate(entity)
# if the entity is a note, we add the content via reading from the file
if entity_response.entity_type == EntityType.NOTE:
if entity_response.entity_type == "note":
content = await knowledge_service.read_entity_content(entity)
entity_response.content = content
@@ -132,8 +130,11 @@ async def get_entity(path_id: PathId, knowledge_service: KnowledgeServiceDep) ->
except EntityNotFoundError:
raise HTTPException(status_code=404, detail=f"Entity with {path_id} not found")
@router.post("/nodes", response_model=EntityListResponse)
async def open_nodes(data: OpenNodesRequest, entity_service: EntityServiceDep) -> EntityListResponse:
async def open_nodes(
data: OpenNodesRequest, entity_service: EntityServiceDep
) -> EntityListResponse:
"""Open specific nodes by their names."""
entities = await entity_service.open_nodes(data.path_ids)
return EntityListResponse(
@@ -149,15 +150,15 @@ async def delete_entities(
data: DeleteEntitiesRequest,
background_tasks: BackgroundTasks,
knowledge_service: KnowledgeServiceDep,
search_service = Depends(get_search_service)
search_service=Depends(get_search_service),
) -> DeleteEntitiesResponse:
"""Delete entities and remove from search index."""
deleted = await knowledge_service.delete_entities(data.path_ids)
# Remove each deleted entity from search index
for path_id in data.path_ids:
background_tasks.add_task(search_service.delete_by_path_id, path_id)
return DeleteEntitiesResponse(deleted=deleted)
@@ -166,15 +167,15 @@ async def delete_observations(
data: DeleteObservationsRequest,
background_tasks: BackgroundTasks,
knowledge_service: KnowledgeServiceDep,
search_service = Depends(get_search_service)
search_service=Depends(get_search_service),
) -> EntityResponse:
"""Delete observations and update search index."""
path_id = data.path_id
updated_entity = await knowledge_service.delete_observations(path_id, data.observations)
# Reindex the entity since observations changed
await search_service.index_entity(updated_entity, background_tasks=background_tasks)
return EntityResponse.model_validate(updated_entity)
@@ -183,15 +184,15 @@ async def delete_relations(
data: DeleteRelationsRequest,
background_tasks: BackgroundTasks,
knowledge_service: KnowledgeServiceDep,
search_service = Depends(get_search_service)
search_service=Depends(get_search_service),
) -> EntityListResponse:
"""Delete relations and update search index."""
updated_entities = await knowledge_service.delete_relations(data.relations)
# Reindex entities since relations changed
for entity in updated_entities:
await search_service.index_entity(entity, background_tasks=background_tasks)
return EntityListResponse(
entities=[EntityResponse.model_validate(entity) for entity in updated_entities]
)
)
@@ -152,7 +152,7 @@ class KnowledgeParser(MarkdownParser[EntityMarkdown]):
relations.append(relation)
return EntityContent(
title=title, description=description, observations=observations, relations=relations
title=title, summary=description, observations=observations, relations=relations
)
except ParseError:
+1 -1
View File
@@ -37,7 +37,7 @@ class EntityContent(BaseModel):
"""Content sections of an entity markdown file."""
title: str
description: Optional[str] = None
summary: Optional[str] = None
observations: List[Observation] = []
relations: List[Relation] = []
context: Optional[str] = None
+1 -1
View File
@@ -85,7 +85,7 @@ class Entity(Base):
return self.incoming_relations + self.outgoing_relations
def __repr__(self) -> str:
return f"Entity(id={self.id}, name='{self.name}', type='{self.entity_type}')"
return f"Entity(id={self.id}, name='{self.name}', type='{self.entity_type}', summary='{self.summary}')"
class ObservationCategory(str, Enum):
+46 -9
View File
@@ -31,13 +31,13 @@ Common Relation Types:
- 'extends': Inheritance/extension
- 'tested_by': Test coverage
"""
import mimetypes
import re
from enum import Enum
from typing import List, Optional, Annotated, Dict
from annotated_types import MinLen, MaxLen
from pydantic import BaseModel, BeforeValidator, Field
from pydantic import BaseModel, BeforeValidator, Field, model_validator, ValidationError
def to_snake_case(name: str) -> str:
@@ -122,15 +122,33 @@ Examples:
- "Depends on SQLAlchemy for database operations"
"""
EntityType = Annotated[str, BeforeValidator(to_snake_case), MinLen(1), MaxLen(200)]
"""Classification of entity (e.g., 'person', 'project', 'concept').
class EntityType(str, Enum):
"""Type of entity.
The type serves multiple purposes:
1. Enables filtering and querying
3. Provides context for relations
- knowledge: Contain information used in the semantic graph
- note: Free form information
Common types are listed in the module docstring.
"""
KNOWLEDGE = "knowledge"
NOTE = "note"
ALLOWED_CONTENT_TYPES = {
'text/markdown',
'text/plain',
'application/pdf',
'image/jpeg',
'image/png',
'image/svg+xml',
}
ContentType = Annotated[
str,
BeforeValidator(str.lower),
Field(pattern=r'^[\w\-\+\.]+/[\w\-\+\.]+$'),
Field(json_schema_extra={"examples": list(ALLOWED_CONTENT_TYPES)})
]
RelationType = Annotated[str, BeforeValidator(to_snake_case), MinLen(1), MaxLen(200)]
"""Type of relationship between entities. Always use active voice present tense.
@@ -252,7 +270,11 @@ class Entity(BaseModel):
entity_type: EntityType
entity_metadata: Optional[Dict] = Field(default=None, description="Optional metadata")
content: Optional[str] = None
description: Optional[str] = None
summary: Optional[str] = None
content_type: ContentType = Field(
description="MIME type of the content (e.g. text/markdown, image/jpeg)",
examples=["text/markdown", "image/jpeg"]
)
observations: List[Observation] = []
@property
@@ -265,3 +287,18 @@ class Entity(BaseModel):
def file_path(self):
"""Get the file path for this entity based on its path_id."""
return f"{self.path_id}.md"
@model_validator(mode='before')
@classmethod
def infer_content_type(cls, data: Dict) -> Dict:
"""Infer content_type from file_path if not provided."""
if 'content_type' not in data:
# Get path from either file_path or construct from path_id
file_path = data.get('file_path') or f"{data.get('name')}.md"
if not file_path:
raise ValidationError("Either file_path or name must be provided")
mime_type, _ = mimetypes.guess_type(file_path)
data['content_type'] = mime_type or 'text/plain'
return data
+1 -1
View File
@@ -94,7 +94,7 @@ class UpdateEntityRequest(BaseModel):
"""Request to update an existing entity."""
name: Optional[str] = None
entity_type: Optional[EntityType] = None
description: Optional[str] = None
summary: Optional[str] = None
content: Optional[str] = None
entity_metadata: Optional[Dict[str, Any]] = None
+3 -2
View File
@@ -16,7 +16,7 @@ from typing import List, Optional, Dict, Any
from pydantic import BaseModel, ConfigDict, Field, AliasPath, AliasChoices
from basic_memory.schemas.base import Observation, Relation, PathId, Entity, EntityType
from basic_memory.schemas.base import Observation, Relation, PathId, Entity, EntityType, ContentType
from basic_memory.schemas.request import ObservationCreate
@@ -122,7 +122,8 @@ class EntityResponse(SQLAlchemyModel):
name: str
entity_type: EntityType
entity_metadata: Optional[Dict] = None
description: Optional[str] = None
content_type: ContentType
summary: Optional[str] = None
content: Optional[str] = None
observations: List[ObservationResponse] = []
relations: List[RelationResponse] = []
+2 -1
View File
@@ -18,7 +18,8 @@ def entity_model(entity: EntitySchema):
entity_metadata=entity.entity_metadata,
path_id=entity.path_id,
file_path=entity.file_path,
description=entity.description,
summary=entity.summary,
content_type=entity.content_type,
observations=[Observation(content=observation) for observation in entity.observations],
)
return model
@@ -10,7 +10,6 @@ from basic_memory.schemas import Entity as EntitySchema
from basic_memory.services.entity_service import EntityService
from basic_memory.services.exceptions import EntityNotFoundError
from .file_operations import FileOperations
from ...models.knowledge import EntityType
class EntityOperations:
@@ -41,9 +40,6 @@ class EntityOperations:
"""
logger.debug(f"Reading entity with path_id: {entity.path_id}")
if entity.entity_type != EntityType.NOTE:
raise ValueError(f"Entity type {entity.entity_type} not supported")
# For notes, read the actual file content
file_path = self.file_operations.get_entity_path(entity)
@@ -8,7 +8,6 @@ from loguru import logger
from basic_memory.markdown.knowledge_writer import KnowledgeWriter
from basic_memory.markdown.note_writer import NoteWriter
from basic_memory.models import Entity as EntityModel
from basic_memory.models.knowledge import EntityType
from basic_memory.services.entity_service import EntityService
from basic_memory.services.exceptions import FileOperationError
from basic_memory.services.file_service import FileService
@@ -54,7 +53,7 @@ class FileOperations:
entity = await self.entity_service.get_by_path_id(entity.path_id)
# Select writer based on entity type
writer = self.note_writer if entity.entity_type == EntityType.NOTE else self.knowledge_writer
writer = self.note_writer if entity.entity_type == "note" else self.knowledge_writer
# Get frontmatter and content
frontmatter = await writer.format_frontmatter(entity)
@@ -29,7 +29,8 @@ def entity_model_from_markdown(file_path: str, markdown: EntityMarkdown) -> Enti
entity_type=markdown.frontmatter.type,
path_id=markdown.frontmatter.id,
file_path=file_path,
description=markdown.content.description,
content_type="text/markdown",
summary=markdown.content.summary,
observations=[
Observation(
content=obs.content,
@@ -83,7 +84,7 @@ class KnowledgeSyncService:
# Update fields from markdown
db_entity.name = markdown.content.title
db_entity.entity_type = markdown.frontmatter.type
db_entity.summary = markdown.content.description
db_entity.summary = markdown.content.summary
# Clear and update observations
await self.observation_service.delete_by_entity(db_entity.id)
@@ -102,7 +103,7 @@ class KnowledgeSyncService:
{
"name": db_entity.name,
"entity_type": db_entity.entity_type,
"description": db_entity.summary,
"summary": db_entity.summary,
# Mark as incomplete
"checksum": None,
},
+33 -30
View File
@@ -4,11 +4,10 @@ import pytest
import pytest_asyncio
from httpx import AsyncClient
from basic_memory.models.knowledge import Entity, Observation, EntityType
from basic_memory.models.knowledge import Entity, Observation
from basic_memory.repository.entity_repository import EntityRepository
from basic_memory.schemas import EntityTypeList, ObservationCategoryList, TypedEntityList
pytestmark = pytest.mark.asyncio
@@ -18,50 +17,54 @@ async def test_entities(entity_repository: EntityRepository) -> list[Entity]:
entities = [
Entity(
name="Memory Service",
entity_type=EntityType.KNOWLEDGE,
description="Core memory service",
entity_type="test",
content_type="text/markdown",
summary="Core memory service",
path_id="component/memory_service",
file_path="component/memory_service.md",
observations=[
Observation(category="tech", content="Using SQLite for storage"),
Observation(category="design", content="Local-first architecture"),
]
],
),
Entity(
name="File Format",
entity_type=EntityType.KNOWLEDGE,
description="File format spec",
entity_type="test",
content_type="text/markdown",
summary="File format spec",
path_id="spec/file_format",
file_path="spec/file_format.md",
observations=[
Observation(category="feature", content="Support for frontmatter"),
Observation(category="tech", content="UTF-8 encoding"),
]
],
),
Entity(
name="Technical Decision",
entity_type=EntityType.KNOWLEDGE,
description="Architecture decision",
entity_type="test",
content_type="text/markdown",
summary="Architecture decision",
path_id="decision/tech_choice",
file_path="decision/tech_choice.md",
observations=[
Observation(category="note", content="Team discussed options"),
Observation(category="design", content="Selected for scalability"),
]
],
),
# Add another technical component for sorting tests
Entity(
name="API Service",
entity_type=EntityType.KNOWLEDGE,
description="API layer",
entity_type="test",
content_type="text/markdown",
summary="API layer",
path_id="component/api_service",
file_path="component/api_service.md",
observations=[
Observation(category="tech", content="FastAPI based"),
]
],
),
]
created = await entity_repository.add_all(entities)
return created
@@ -71,18 +74,18 @@ async def test_get_entity_types(client: AsyncClient, test_entities):
# Get types
response = await client.get("/discovery/entity-types")
assert response.status_code == 200
# Parse response
data = EntityTypeList.model_validate(response.json())
# Should have types from test data
assert len(data.types) > 0
assert "knowledge" in data.types
assert "test" in data.types
# Types should all be strings
assert isinstance(data.types, list)
assert all(isinstance(t, str) for t in data.types)
# Types should be unique
assert len(data.types) == len(set(data.types))
@@ -92,21 +95,21 @@ async def test_get_observation_categories(client: AsyncClient, test_entities):
# Get categories
response = await client.get("/discovery/observation-categories")
assert response.status_code == 200
# Parse response
data = ObservationCategoryList.model_validate(response.json())
# Should have categories from test data
assert len(data.categories) > 0
assert "tech" in data.categories
assert "design" in data.categories
assert "feature" in data.categories
assert "note" in data.categories
# Categories should all be strings
assert isinstance(data.categories, list)
assert all(isinstance(c, str) for c in data.categories)
# Categories should be unique
assert len(data.categories) == len(set(data.categories))
@@ -114,17 +117,17 @@ async def test_get_observation_categories(client: AsyncClient, test_entities):
async def test_list_entities_by_type(client: AsyncClient, test_entities):
"""Test listing entities by type."""
# List technical components
response = await client.get("/discovery/entities/knowledge")
response = await client.get("/discovery/entities/test")
assert response.status_code == 200
# Parse response
data = TypedEntityList.model_validate(response.json())
# Check response structure
assert data.entity_type == "knowledge"
assert data.entity_type == "test"
assert len(data.entities) == 4
assert data.total == 4
# Verify content
names = {e.name for e in data.entities}
assert "Memory Service" in names
@@ -152,7 +155,7 @@ async def test_list_entities_empty_type(client: AsyncClient, test_entities):
"""Test listing entities for a type that doesn't exist."""
response = await client.get("/discovery/entities/nonexistent_type")
assert response.status_code == 200
data = TypedEntityList.model_validate(response.json())
assert data.entity_type == "nonexistent_type"
assert len(data.entities) == 0
+79 -102
View File
@@ -10,7 +10,7 @@ from basic_memory.schemas import (
EntityResponse,
EntityListResponse,
ObservationResponse,
RelationResponse, EntityType,
RelationResponse,
)
from basic_memory.schemas.search import SearchItemType, SearchResponse
@@ -18,7 +18,7 @@ from basic_memory.schemas.search import SearchItemType, SearchResponse
async def create_entity(client) -> EntityResponse:
data = {
"name": "TestEntity",
"entity_type": EntityType.KNOWLEDGE,
"entity_type": "test",
"observations": ["First observation", "Second observation"],
}
# Create an entity
@@ -63,8 +63,8 @@ async def add_observations(client, path_id: str) -> List[ObservationResponse]:
async def create_related_entities(client) -> List[RelationResponse]: # pyright: ignore [reportReturnType]
# Create two entities to relate
entities = [
{"name": "SourceEntity", "entity_type": EntityType.KNOWLEDGE},
{"name": "TargetEntity", "entity_type": EntityType.KNOWLEDGE},
{"name": "SourceEntity", "entity_type": "test"},
{"name": "TargetEntity", "entity_type": "test"},
]
create_response = await client.post("/knowledge/entities", json={"entities": entities})
created = create_response.json()["entities"]
@@ -114,7 +114,7 @@ async def test_create_entities(client: AsyncClient):
async def test_get_entity(client: AsyncClient):
"""Should retrieve an entity by path ID."""
# First create an entity
data = {"name": "TestEntity", "entity_type": EntityType.KNOWLEDGE}
data = {"name": "TestEntity", "entity_type": "test"}
response = await client.post("/knowledge/entities", json={"entities": [data]})
assert response.status_code == 200
data = response.json()
@@ -127,7 +127,7 @@ async def test_get_entity(client: AsyncClient):
assert response.status_code == 200
entity = response.json()
assert entity["name"] == "TestEntity"
assert entity["entity_type"] == EntityType.KNOWLEDGE
assert entity["entity_type"] == "test"
assert entity["path_id"] == "test_entity"
@@ -141,7 +141,7 @@ async def test_create_relations(client: AsyncClient):
async def test_add_observations(client: AsyncClient):
"""Should add observations to an entity."""
# Create an entity first
data = {"name": "TestEntity", "entity_type": EntityType.KNOWLEDGE}
data = {"name": "TestEntity", "entity_type": "test"}
response = await client.post("/knowledge/entities", json={"entities": [data]})
path_id = "test_entity"
@@ -154,14 +154,13 @@ async def test_add_observations(client: AsyncClient):
assert len(entity["observations"]) == 2
@pytest.mark.asyncio
async def test_open_nodes(client: AsyncClient):
"""Should open multiple nodes by path IDs."""
# Create a few entities with different names
entities = [
{"name": "AlphaTest", "entity_type": EntityType.KNOWLEDGE},
{"name": "BetaTest", "entity_type": EntityType.KNOWLEDGE},
{"name": "AlphaTest", "entity_type": "test"},
{"name": "BetaTest", "entity_type": "test"},
]
await client.post("/knowledge/entities", json={"entities": entities})
@@ -177,7 +176,7 @@ async def test_open_nodes(client: AsyncClient):
assert len(data["entities"]) == 1
entity = data["entities"][0]
assert entity["name"] == "AlphaTest"
assert entity["entity_type"] == EntityType.KNOWLEDGE
assert entity["entity_type"] == "test"
assert entity["path_id"] == "alpha_test"
@@ -185,7 +184,7 @@ async def test_open_nodes(client: AsyncClient):
async def test_delete_entity(client: AsyncClient):
"""Test DELETE /knowledge/entities with path ID."""
# Create test entity
entity_data = {"name": "TestEntity", "entity_type": EntityType.KNOWLEDGE}
entity_data = {"name": "TestEntity", "entity_type": "test"}
await client.post("/knowledge/entities", json={"entities": [entity_data]})
# Test deletion
@@ -206,8 +205,8 @@ async def test_delete_entity_bulk(client: AsyncClient):
"""Test bulk entity deletion using path IDs."""
# Create test entities
entities = [
{"name": "Entity1", "entity_type": EntityType.KNOWLEDGE},
{"name": "Entity2", "entity_type": EntityType.KNOWLEDGE},
{"name": "Entity1", "entity_type": "test"},
{"name": "Entity2", "entity_type": "test"},
]
await client.post("/knowledge/entities", json={"entities": entities})
@@ -229,14 +228,12 @@ async def test_delete_entity_bulk(client: AsyncClient):
async def test_delete_entity_with_observations(client, observation_repository):
"""Test cascading delete with observations."""
# Create test entity and add observations
entity_data = {"name": "TestEntity", "entity_type": EntityType.KNOWLEDGE}
entity_data = {"name": "TestEntity", "entity_type": "test"}
await client.post("/knowledge/entities", json={"entities": [entity_data]})
await add_observations(client, "TestEntity")
# Delete entity
response = await client.post(
"/knowledge/entities/delete", json={"path_ids": ["TestEntity"]}
)
response = await client.post("/knowledge/entities/delete", json={"path_ids": ["TestEntity"]})
assert response.status_code == 200
assert response.json() == {"deleted": True}
@@ -249,7 +246,7 @@ async def test_delete_entity_with_observations(client, observation_repository):
async def test_delete_observations(client, observation_repository):
"""Test deleting specific observations."""
# Create entity and add observations
entity_data = {"name": "TestEntity", "entity_type": EntityType.KNOWLEDGE}
entity_data = {"name": "TestEntity", "entity_type": "test"}
await client.post("/knowledge/entities", json={"entities": [entity_data]})
observations = await add_observations(client, "TestEntity") # adds 2
@@ -293,9 +290,7 @@ async def test_delete_relations(client, relation_repository):
@pytest.mark.asyncio
async def test_delete_nonexistent_entity(client: AsyncClient):
"""Test deleting a nonexistent entity by path ID."""
response = await client.post(
"/knowledge/entities/delete", json={"path_ids": ["non_existent"]}
)
response = await client.post("/knowledge/entities/delete", json={"path_ids": ["non_existent"]})
assert response.status_code == 200
assert response.json() == {"deleted": True}
@@ -304,7 +299,7 @@ async def test_delete_nonexistent_entity(client: AsyncClient):
async def test_delete_nonexistent_observations(client: AsyncClient):
"""Test deleting nonexistent observations."""
# Create test entity
entity_data = {"name": "TestEntity", "entity_type": EntityType.KNOWLEDGE}
entity_data = {"name": "TestEntity", "entity_type": "test"}
await client.post("/knowledge/entities", json={"entities": [entity_data]})
request_data = {"path_id": "TestEntity", "observations": ["Nonexistent observation"]}
@@ -336,21 +331,20 @@ async def test_delete_nonexistent_relations(client: AsyncClient):
assert del_response.entities == []
@pytest.mark.asyncio
async def test_full_knowledge_flow(client: AsyncClient):
"""Test complete knowledge graph flow with path IDs."""
# 1. Create main entities
main_entities = [
{"name": "MainEntity", "entity_type": EntityType.KNOWLEDGE},
{"name": "NonEntity", "entity_type": EntityType.KNOWLEDGE},
{"name": "MainEntity", "entity_type": "test"},
{"name": "NonEntity", "entity_type": "test"},
]
await client.post("/knowledge/entities", json={"entities": main_entities})
# 2. Create related entities
related_entities = [
{"name": "RelatedOne", "entity_type": EntityType.KNOWLEDGE},
{"name": "RelatedTwo", "entity_type": EntityType.KNOWLEDGE},
{"name": "RelatedOne", "entity_type": "test"},
{"name": "RelatedTwo", "entity_type": "test"},
]
await client.post("/knowledge/entities", json={"entities": related_entities})
@@ -374,7 +368,7 @@ async def test_full_knowledge_flow(client: AsyncClient):
)
assert relations_response.status_code == 200
relations_entities = relations_response.json()
assert len(relations_entities["entities"]) == 3
assert len(relations_entities["entities"]) == 3
# 4. Add observations to main entity
await client.post(
@@ -402,9 +396,7 @@ async def test_full_knowledge_flow(client: AsyncClient):
# 6. Search should find all related entities
search = await client.post("/search/", json={"text": "Related"})
matches = search.json()["results"]
assert (
len(matches) == 1
)
assert len(matches) == 1
# 7. Delete main entity
response = await client.post(
@@ -424,7 +416,7 @@ async def test_entity_indexing(client: AsyncClient):
"""Test entity creation includes search indexing."""
data = {
"name": "SearchTest",
"entity_type": EntityType.KNOWLEDGE,
"entity_type": "test",
"observations": ["Unique searchable observation"],
}
@@ -449,7 +441,7 @@ async def test_observation_update_indexing(client: AsyncClient):
# Create entity
data = {
"name": "TestEntity",
"entity_type": EntityType.KNOWLEDGE,
"entity_type": "test",
"observations": ["Initial observation"],
}
response = await client.post("/knowledge/entities", json={"entities": [data]})
@@ -479,7 +471,7 @@ async def test_entity_delete_indexing(client: AsyncClient):
"""Test deleted entities are removed from search index."""
data = {
"name": "DeleteTest",
"entity_type": EntityType.KNOWLEDGE,
"entity_type": "test",
"observations": ["Searchable observation that should be removed"],
}
@@ -514,8 +506,8 @@ async def test_relation_indexing(client: AsyncClient):
"""Test relations are included in search index."""
# Create entities
entities = [
{"name": "SourceTest", "entity_type": EntityType.KNOWLEDGE},
{"name": "TargetTest", "entity_type": EntityType.KNOWLEDGE},
{"name": "SourceTest", "entity_type": "test"},
{"name": "TargetTest", "entity_type": "test"},
]
create_response = await client.post("/knowledge/entities", json={"entities": entities})
assert create_response.status_code == 200
@@ -541,7 +533,7 @@ async def test_relation_indexing(client: AsyncClient):
"/search/", json={"text": "sphinx relation", "types": [SearchItemType.ENTITY.value]}
)
search_result = SearchResponse.model_validate(search_response.json())
assert len(search_result.results) == 2 # Both source and target entities
assert len(search_result.results) == 2 # Both source and target entities
path_ids = {r.path_id for r in search_result.results}
assert path_ids == {"source_test", "target_test"}
@@ -552,9 +544,9 @@ async def test_update_entity_basic(client: AsyncClient):
# Create initial entity
data = {
"name": "test",
"entity_type": EntityType.KNOWLEDGE,
"description": "Initial description",
"entity_metadata": {"status": "draft"}
"entity_type": "test",
"summary": "Initial description",
"entity_metadata": {"status": "draft"},
}
response = await client.post("/knowledge/entities", json={"entities": [data]})
entity = response.json()["entities"][0]
@@ -562,7 +554,7 @@ async def test_update_entity_basic(client: AsyncClient):
# Update basic fields
update_data = {
"name": "updated-test",
"description": "Updated description",
"summary": "Updated description",
}
response = await client.put(f"/knowledge/entities/{entity['path_id']}", json=update_data)
assert response.status_code == 200
@@ -570,7 +562,7 @@ async def test_update_entity_basic(client: AsyncClient):
# Verify updates
assert updated["name"] == "updated-test"
assert updated["description"] == "Updated description"
assert updated["summary"] == "Updated description"
assert updated["entity_metadata"]["status"] == "draft" # Preserved
@@ -578,19 +570,14 @@ async def test_update_entity_basic(client: AsyncClient):
async def test_update_entity_content(client: AsyncClient):
"""Test updating content for different entity types."""
# Create a note entity
note_data = {
"name": "test-note",
"entity_type": EntityType.NOTE,
"description": "Test note"
}
note_data = {"name": "test-note", "entity_type": "note", "summary": "Test note"}
response = await client.post("/knowledge/entities", json={"entities": [note_data]})
note = response.json()["entities"][0]
# Update note content
new_content = "# Updated Note\n\nNew content."
response = await client.put(
f"/knowledge/entities/{note['path_id']}",
json={"content": new_content}
f"/knowledge/entities/{note['path_id']}", json={"content": new_content}
)
assert response.status_code == 200
updated = response.json()
@@ -608,49 +595,39 @@ async def test_update_entity_type_conversion(client: AsyncClient):
# Create a note
note_data = {
"name": "test-note",
"entity_type": EntityType.NOTE,
"description": "Test note",
"content": "# Test Note\n\nInitial content."
"entity_type": "note",
"summary": "Test note",
"content": "# Test Note\n\nInitial content.",
}
response = await client.post("/knowledge/entities", json={"entities": [note_data]})
note = response.json()["entities"][0]
# Convert to knowledge type
response = await client.put(
f"/knowledge/entities/{note['path_id']}",
json={"entity_type": EntityType.KNOWLEDGE}
f"/knowledge/entities/{note['path_id']}", json={"entity_type": "test"}
)
assert response.status_code == 200
updated = response.json()
# Verify conversion
assert updated["entity_type"] == EntityType.KNOWLEDGE
assert updated["entity_type"] == "test"
# Get latest to verify file format
response = await client.get(f"/knowledge/entities/{updated['path_id']}")
knowledge = response.json()
assert knowledge.get("content") is None
assert knowledge.get("content") is None
@pytest.mark.asyncio
async def test_update_entity_metadata(client: AsyncClient):
"""Test updating entity metadata."""
# Create entity
data = {
"name": "test",
"entity_type": EntityType.KNOWLEDGE,
"entity_metadata": {"status": "draft"}
}
data = {"name": "test", "entity_type": "test", "entity_metadata": {"status": "draft"}}
response = await client.post("/knowledge/entities", json={"entities": [data]})
entity = response.json()["entities"][0]
# Update metadata
update_data = {
"entity_metadata": {
"status": "final",
"reviewed": True
}
}
update_data = {"entity_metadata": {"status": "final", "reviewed": True}}
response = await client.put(f"/knowledge/entities/{entity['path_id']}", json=update_data)
assert response.status_code == 200
updated = response.json()
@@ -663,10 +640,7 @@ async def test_update_entity_metadata(client: AsyncClient):
@pytest.mark.asyncio
async def test_update_entity_not_found(client: AsyncClient):
"""Test updating non-existent entity."""
response = await client.put(
"/knowledge/entities/nonexistent",
json={"name": "new-name"}
)
response = await client.put("/knowledge/entities/nonexistent", json={"name": "new-name"})
assert response.status_code == 404
@@ -674,59 +648,62 @@ async def test_update_entity_not_found(client: AsyncClient):
async def test_update_entity_search_index(client: AsyncClient):
"""Test search index is updated after entity changes."""
# Create entity
data = {
"name": "test",
"entity_type": EntityType.KNOWLEDGE,
"description": "Initial searchable content"
}
data = {"name": "test", "entity_type": "test", "summary": "Initial searchable content"}
response = await client.post("/knowledge/entities", json={"entities": [data]})
entity = response.json()["entities"][0]
# Update with new searchable content
update_data = {
"description": "Updated with unique sphinx marker"
}
update_data = {"summary": "Updated with unique sphinx marker"}
response = await client.put(f"/knowledge/entities/{entity['path_id']}", json=update_data)
assert response.status_code == 200
# Search should find new content
search_response = await client.post(
"/search/",
json={"text": "sphinx marker", "types": [SearchItemType.ENTITY.value]}
"/search/", json={"text": "sphinx marker", "types": [SearchItemType.ENTITY.value]}
)
results = search_response.json()["results"]
assert len(results) == 1
assert results[0]["path_id"] == entity["path_id"]
@pytest.mark.asyncio
async def test_get_entity_with_relations(client: AsyncClient):
"""Test get response includes relations for both types."""
# Create a note and knowledge entity
note = await client.post("/knowledge/entities", json={"entities": [{
"name": "test-note",
"entity_type": EntityType.NOTE,
"description": "Test note"
}]})
knowledge = await client.post("/knowledge/entities", json={"entities": [{
"name": "test-knowledge",
"entity_type": EntityType.KNOWLEDGE,
"description": "Test knowledge"
}]})
note = await client.post(
"/knowledge/entities",
json={
"entities": [{"name": "test-note", "entity_type": "note", "summary": "Test note"}]
},
)
knowledge = await client.post(
"/knowledge/entities",
json={
"entities": [
{"name": "test-knowledge", "entity_type": "test", "summary": "Test knowledge"}
]
},
)
# Add some relations between them
await client.post("/knowledge/relations", json={
"relations": [{
"from_id": note.json()["entities"][0]["path_id"],
"to_id": knowledge.json()["entities"][0]["path_id"],
"relation_type": "references"
}]
})
await client.post(
"/knowledge/relations",
json={
"relations": [
{
"from_id": note.json()["entities"][0]["path_id"],
"to_id": knowledge.json()["entities"][0]["path_id"],
"relation_type": "references",
}
]
},
)
# Verify GET returns relations for both types
note_response = await client.get(f"/knowledge/entities/{note.json()['entities'][0]['path_id']}")
knowledge_response = await client.get(f"/knowledge/entities/{knowledge.json()['entities'][0]['path_id']}")
knowledge_response = await client.get(
f"/knowledge/entities/{knowledge.json()['entities'][0]['path_id']}"
)
assert len(note_response.json()["relations"]) == 1
assert len(knowledge_response.json()["relations"]) == 1
+33 -86
View File
@@ -5,28 +5,30 @@ from datetime import datetime, timezone
import pytest
import pytest_asyncio
from sqlalchemy import text
from basic_memory import db
from basic_memory.schemas import EntityType
from basic_memory.schemas.search import SearchQuery, SearchItemType, SearchResponse
from basic_memory.schemas.search import SearchItemType, SearchResponse
@pytest.fixture
def test_entity():
"""Create a test entity."""
class Entity:
id = 1
name = "TestComponent"
entity_type = EntityType.KNOWLEDGE
entity_metadata = { "test": "test"}
entity_type = "test"
entity_metadata = {"test": "test"}
path_id = "component/test_component"
file_path = "entities/component/test_component.md"
description = "A test component for search testing"
summary = "A test component for search testing"
content_type = "text/markdown"
created_at = datetime.now(timezone.utc)
updated_at = datetime.now(timezone.utc)
observations = []
relations = []
return Entity()
return Entity()
@pytest_asyncio.fixture
@@ -36,16 +38,10 @@ async def indexed_entity(init_search_index, test_entity, search_service):
return test_entity
@pytest.mark.asyncio
async def test_search_basic(client, indexed_entity):
"""Test basic text search."""
response = await client.post(
"/search/",
json={
"text": "test component"
}
)
response = await client.post("/search/", json={"text": "test component"})
assert response.status_code == 200
search_results = SearchResponse.model_validate(response.json())
assert len(search_results.results) == 1
@@ -57,23 +53,15 @@ async def test_search_with_type_filter(client, indexed_entity):
"""Test search with type filter."""
# Should find with correct type
response = await client.post(
"/search/",
json={
"text": "test",
"types": [SearchItemType.ENTITY.value]
}
"/search/", json={"text": "test", "types": [SearchItemType.ENTITY.value]}
)
assert response.status_code == 200
search_results = SearchResponse.model_validate(response.json())
assert len(search_results.results) == 1
# Should not find with wrong type
response = await client.post(
"/search/",
json={
"text": "test",
"types": [SearchItemType.DOCUMENT.value]
}
"/search/", json={"text": "test", "types": [SearchItemType.DOCUMENT.value]}
)
assert response.status_code == 200
search_results = SearchResponse.model_validate(response.json())
@@ -84,25 +72,13 @@ async def test_search_with_type_filter(client, indexed_entity):
async def test_search_with_entity_type_filter(client, indexed_entity):
"""Test search with entity type filter."""
# Should find with correct entity type
response = await client.post(
"/search/",
json={
"text": "test",
"entity_types": [EntityType.KNOWLEDGE]
}
)
response = await client.post("/search/", json={"text": "test", "entity_types": ["test"]})
assert response.status_code == 200
search_results = SearchResponse.model_validate(response.json())
assert len(search_results.results) == 1
# Should not find with wrong entity type
response = await client.post(
"/search/",
json={
"text": "test",
"entity_types": [EntityType.NOTE]
}
)
response = await client.post("/search/", json={"text": "test", "entity_types": ["note"]})
assert response.status_code == 200
search_results = SearchResponse.model_validate(response.json())
assert len(search_results.results) == 0
@@ -114,24 +90,16 @@ async def test_search_with_date_filter(client, indexed_entity):
# Should find with past date
past_date = datetime(2020, 1, 1, tzinfo=timezone.utc)
response = await client.post(
"/search/",
json={
"text": "test",
"after_date": past_date.isoformat()
}
"/search/", json={"text": "test", "after_date": past_date.isoformat()}
)
assert response.status_code == 200
search_results = SearchResponse.model_validate(response.json())
assert len(search_results.results) == 1
# Should not find with future date
future_date = datetime(2030, 1, 1, tzinfo=timezone.utc)
response = await client.post(
"/search/",
json={
"text": "test",
"after_date": future_date.isoformat()
}
"/search/", json={"text": "test", "after_date": future_date.isoformat()}
)
assert response.status_code == 200
search_results = SearchResponse.model_validate(response.json())
@@ -142,49 +110,34 @@ async def test_search_with_date_filter(client, indexed_entity):
async def test_search_scoring(client, indexed_entity):
"""Test search result scoring."""
# Exact match should score higher
exact_response = await client.post(
"/search/",
json={"text": "TestComponent"}
)
exact_response = await client.post("/search/", json={"text": "TestComponent"})
# Partial match should score lower
partial_response = await client.post(
"/search/",
json={"text": "test"}
)
partial_response = await client.post("/search/", json={"text": "test"})
assert exact_response.status_code == 200
assert partial_response.status_code == 200
exact_result = SearchResponse.model_validate(exact_response.json())
partial_result = SearchResponse.model_validate(partial_response.json())
exact_score = exact_result.results[0].score
partial_score = partial_result.results[0].score
assert exact_score > partial_score
@pytest.mark.asyncio
async def test_search_empty(search_service, client):
"""Test search with no matches."""
response = await client.post(
"/search/",
json={"text": "nonexistent"}
)
response = await client.post("/search/", json={"text": "nonexistent"})
assert response.status_code == 200
search_result = SearchResponse.model_validate(response.json())
assert len(search_result.results) == 0
@pytest.mark.asyncio
async def test_reindex(
client,
search_service,
entity_service,
test_entity,
session_maker
):
async def test_reindex(client, search_service, entity_service, test_entity, session_maker):
"""Test reindex endpoint."""
# Create test entity and document
await entity_service.create_entity(test_entity)
@@ -195,10 +148,7 @@ async def test_reindex(
await session.commit()
# Verify nothing is searchable
response = await client.post(
"/search/",
json={"text": "test"}
)
response = await client.post("/search/", json={"text": "test"})
search_results = SearchResponse.model_validate(response.json())
assert len(search_results.results) == 0
@@ -208,12 +158,9 @@ async def test_reindex(
assert reindex_response.json()["status"] == "ok"
# Verify content is searchable again
search_response = await client.post(
"/search/",
json={"text": "test"}
)
search_response = await client.post("/search/", json={"text": "test"})
search_results = SearchResponse.model_validate(search_response.json())
assert len(search_results.results) == 1
assert len(search_results.results) == 1
@pytest.mark.asyncio
@@ -224,9 +171,9 @@ async def test_multiple_filters(client, indexed_entity):
json={
"text": "test",
"types": [SearchItemType.ENTITY.value],
"entity_types": [EntityType.KNOWLEDGE],
"after_date": datetime(2020, 1, 1, tzinfo=timezone.utc).isoformat()
}
"entity_types": ["test"],
"after_date": datetime(2020, 1, 1, tzinfo=timezone.utc).isoformat(),
},
)
assert response.status_code == 200
search_result = SearchResponse.model_validate(response.json())
@@ -234,4 +181,4 @@ async def test_multiple_filters(client, indexed_entity):
result = search_result.results[0]
assert result.path_id == indexed_entity.path_id
assert result.type == SearchItemType.ENTITY.value
assert result.metadata["entity_type"] == EntityType.KNOWLEDGE
assert result.metadata["entity_type"] == "test"
+7 -6
View File
@@ -14,7 +14,7 @@ from basic_memory.markdown.knowledge_parser import KnowledgeParser
from basic_memory.markdown.knowledge_writer import KnowledgeWriter
from basic_memory.markdown.note_writer import NoteWriter
from basic_memory.models import Base
from basic_memory.models.knowledge import Entity, EntityType
from basic_memory.models.knowledge import Entity
from basic_memory.repository.entity_repository import EntityRepository
from basic_memory.repository.observation_repository import ObservationRepository
from basic_memory.repository.relation_repository import RelationRepository
@@ -74,7 +74,6 @@ async def session_maker(engine_factory) -> async_sessionmaker[AsyncSession]:
return session_maker
@pytest_asyncio.fixture(scope="function")
async def entity_repository(session_maker: async_sessionmaker[AsyncSession]) -> EntityRepository:
"""Create an EntityRepository instance."""
@@ -129,6 +128,7 @@ def knowledge_writer():
"""Create writer instance."""
return KnowledgeWriter()
@pytest.fixture
def note_writer():
"""Create writer instance."""
@@ -190,7 +190,7 @@ async def sync_service(
knowledge_sync_service: KnowledgeSyncService,
file_change_scanner: FileChangeScanner,
knowledge_parser: KnowledgeParser,
search_service: SearchService
search_service: SearchService,
) -> SyncService:
"""Create sync service for testing."""
return SyncService(
@@ -206,12 +206,12 @@ async def search_repository(session_maker):
"""Create SearchRepository instance"""
return SearchRepository(session_maker)
@pytest_asyncio.fixture(autouse=True)
async def init_search_index(search_service):
await search_service.init_search_index()
@pytest_asyncio.fixture
async def search_service(
search_repository: SearchRepository,
@@ -228,9 +228,10 @@ async def sample_entity(entity_repository: EntityRepository) -> Entity:
"""Create a sample entity for testing."""
entity_data = {
"name": "Test Entity",
"entity_type": EntityType.KNOWLEDGE,
"description": "A test entity",
"entity_type": "test",
"summary": "A test entity",
"path_id": "test/test_entity",
"file_path": "test/test_entity.md",
"content_type": "text/markdown",
}
return await entity_repository.create(entity_data)
+1 -1
View File
@@ -68,7 +68,7 @@ async def test_parse_complete_file(tmp_path, valid_entity_content):
# Check content
assert entity.content.title == "Auth Service"
assert (
entity.content.description
entity.content.summary
== "Core authentication service that handles user authentication."
)
+26 -35
View File
@@ -3,9 +3,9 @@
from datetime import datetime, UTC
import pytest
from basic_memory.models import Entity, Observation, Relation
from basic_memory.markdown.knowledge_writer import KnowledgeWriter
from basic_memory.models.knowledge import EntityType
from basic_memory.models import Entity, Observation, Relation
@pytest.fixture
@@ -19,12 +19,12 @@ def sample_entity() -> Entity:
return Entity(
id=1,
name="test_entity",
entity_type=EntityType.KNOWLEDGE,
entity_type="test",
path_id="knowledge/test_entity",
file_path="knowledge/test_entity.md",
description="Test description",
summary="Test description",
created_at=datetime(2025, 1, 1, tzinfo=UTC),
updated_at=datetime(2025, 1, 2, tzinfo=UTC)
updated_at=datetime(2025, 1, 2, tzinfo=UTC),
)
@@ -33,7 +33,9 @@ def entity_with_observations(sample_entity: Entity) -> Entity:
"""Create an entity with observations."""
sample_entity.observations = [
Observation(entity_id=1, category="tech", content="First observation"),
Observation(entity_id=1, category="design", content="Second observation", context="Some context")
Observation(
entity_id=1, category="design", content="Second observation", context="Some context"
),
]
return sample_entity
@@ -42,18 +44,10 @@ 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,
name="target_entity",
entity_type=EntityType.KNOWLEDGE,
path_id="knowledge/target_entity"
id=2, name="target_entity", entity_type="test", path_id="knowledge/target_entity"
)
sample_entity.outgoing_relations = [
Relation(
from_id=1,
to_id=2,
relation_type="connects_to",
to_entity=target
)
Relation(from_id=1, to_id=2, relation_type="connects_to", to_entity=target)
]
return sample_entity
@@ -62,23 +56,22 @@ def entity_with_relations(sample_entity: Entity) -> Entity:
async def test_format_frontmatter_basic(knowledge_writer: KnowledgeWriter, sample_entity: Entity):
"""Test basic frontmatter formatting."""
frontmatter = await knowledge_writer.format_frontmatter(sample_entity)
assert frontmatter["id"] == "knowledge/test_entity"
assert frontmatter["type"] == EntityType.KNOWLEDGE
assert frontmatter["type"] == "test"
assert frontmatter["created"] == "2025-01-01T00:00:00+00:00"
assert frontmatter["modified"] == "2025-01-02T00:00:00+00:00"
@pytest.mark.asyncio
async def test_format_frontmatter_with_metadata(knowledge_writer: KnowledgeWriter, sample_entity: Entity):
async def test_format_frontmatter_with_metadata(
knowledge_writer: KnowledgeWriter, sample_entity: Entity
):
"""Test frontmatter includes entity metadata."""
sample_entity.entity_metadata = {
"status": "active",
"priority": "high"
}
sample_entity.entity_metadata = {"status": "active", "priority": "high"}
frontmatter = await knowledge_writer.format_frontmatter(sample_entity)
assert frontmatter["status"] == "active"
assert frontmatter["priority"] == "high"
assert frontmatter["id"] == "knowledge/test_entity"
@@ -89,20 +82,19 @@ async def test_format_content_basic(knowledge_writer: KnowledgeWriter, sample_en
"""Test basic content formatting."""
content = ""
result = await knowledge_writer.format_content(sample_entity, content)
assert "# test_entity" in result
assert "Test description" in result
@pytest.mark.asyncio
async def test_format_content_with_observations(
knowledge_writer: KnowledgeWriter,
entity_with_observations: Entity
knowledge_writer: KnowledgeWriter, entity_with_observations: Entity
):
"""Test content formatting with observations."""
content = ""
result = await knowledge_writer.format_content(entity_with_observations, content)
assert "## Observations" in result
assert "- [tech] First observation" in result
assert "- [design] Second observation (Some context)" in result
@@ -110,13 +102,12 @@ async def test_format_content_with_observations(
@pytest.mark.asyncio
async def test_format_content_with_relations(
knowledge_writer: KnowledgeWriter,
entity_with_relations: Entity
knowledge_writer: KnowledgeWriter, entity_with_relations: Entity
):
"""Test content formatting with relations."""
content = ""
result = await knowledge_writer.format_content(entity_with_relations, content)
assert "## Relations" in result
assert "- connects_to [[target_entity]]" in result
@@ -125,18 +116,18 @@ async def test_format_content_with_relations(
async def test_format_content_full_entity(
knowledge_writer: KnowledgeWriter,
entity_with_relations: Entity,
entity_with_observations: Entity
entity_with_observations: Entity,
):
"""Test content formatting with all entity features."""
# Combine observations and relations
entity_with_relations.observations = entity_with_observations.observations
content = ""
result = await knowledge_writer.format_content(entity_with_relations, content)
# Verify all sections present
assert "# test_entity" in result
assert "Test description" in result
assert "## Observations" in result
assert "- [tech] First observation" in result
assert "## Relations" in result
assert "- connects_to [[target_entity]]" in result
assert "- connects_to [[target_entity]]" in result
+13 -16
View File
@@ -3,9 +3,9 @@
from datetime import datetime, UTC
import pytest
from basic_memory.models import Entity
from basic_memory.markdown.note_writer import NoteWriter
from basic_memory.models.knowledge import EntityType
from basic_memory.models import Entity
@pytest.fixture
@@ -19,11 +19,11 @@ def sample_note() -> Entity:
return Entity(
id=1,
name="test_note",
entity_type=EntityType.NOTE,
entity_type="note",
path_id="notes/test_note",
file_path="notes/test_note.md",
created_at=datetime(2025, 1, 1, tzinfo=UTC),
updated_at=datetime(2025, 1, 2, tzinfo=UTC)
updated_at=datetime(2025, 1, 2, tzinfo=UTC),
)
@@ -31,9 +31,9 @@ def sample_note() -> Entity:
async def test_format_frontmatter_basic(note_writer: NoteWriter, sample_note: Entity):
"""Test basic frontmatter formatting."""
frontmatter = await note_writer.format_frontmatter(sample_note)
assert frontmatter["id"] == "notes/test_note"
assert frontmatter["type"] == EntityType.NOTE
assert frontmatter["type"] == "note"
assert frontmatter["created"] == "2025-01-01T00:00:00+00:00"
assert frontmatter["modified"] == "2025-01-02T00:00:00+00:00"
@@ -41,13 +41,10 @@ async def test_format_frontmatter_basic(note_writer: NoteWriter, sample_note: En
@pytest.mark.asyncio
async def test_format_frontmatter_with_metadata(note_writer: NoteWriter, sample_note: Entity):
"""Test frontmatter includes entity metadata."""
sample_note.entity_metadata = {
"category": "research",
"tags": ["python", "testing"]
}
sample_note.entity_metadata = {"category": "research", "tags": ["python", "testing"]}
frontmatter = await note_writer.format_frontmatter(sample_note)
assert frontmatter["category"] == "research"
assert frontmatter["tags"] == ["python", "testing"]
assert frontmatter["id"] == "notes/test_note"
@@ -58,7 +55,7 @@ async def test_format_content_basic(note_writer: NoteWriter, sample_note: Entity
"""Test basic content formatting."""
content = "# Test Note\n\nThis is a test note."
result = await note_writer.format_content(sample_note, content)
assert result == content
@@ -67,7 +64,7 @@ async def test_format_content_strips_whitespace(note_writer: NoteWriter, sample_
"""Test content formatting strips extra whitespace."""
content = "\n\n# Test Note\n\nThis is a test note.\n\n"
result = await note_writer.format_content(sample_note, content)
assert result == "# Test Note\n\nThis is a test note."
@@ -86,6 +83,6 @@ This note has:
def test():
pass
```"""
result = await note_writer.format_content(sample_note, content)
assert result == content
assert result == content
+2 -2
View File
@@ -33,7 +33,7 @@ def test_entity_data():
{
"name": "Test Entity",
"entity_type": "test",
"description": "", # Empty string instead of None
"summary": "", # Empty string instead of None
"observations": ["This is a test observation"],
}
]
@@ -48,7 +48,7 @@ def test_directory_entity_data():
{
"name": "Directory Organization",
"entity_type": "memory",
"description": "Implemented filesystem organization by entity type",
"summary": "Implemented filesystem organization by entity type",
"observations": [
"Files are now organized by type using directories like entities/project/basic_memory",
"Entity IDs match filesystem paths for better mental model",
+31 -62
View File
@@ -3,26 +3,25 @@
import pytest
from basic_memory.mcp.tools.knowledge import create_entities, add_observations
from basic_memory.schemas.base import ObservationCategory, Entity, EntityType
from basic_memory.schemas.request import CreateEntityRequest, AddObservationsRequest, ObservationCreate
from basic_memory.schemas.base import ObservationCategory, Entity
from basic_memory.schemas.request import (
CreateEntityRequest,
AddObservationsRequest,
ObservationCreate,
)
@pytest.mark.asyncio
async def test_add_basic_observation(client):
"""Test adding a single observation with default category."""
# First create an entity to add observations to
entity_request = CreateEntityRequest(
entities=[Entity(name="TestEntity", entity_type=EntityType.KNOWLEDGE)]
)
entity_request = CreateEntityRequest(entities=[Entity(name="TestEntity", entity_type="test")])
result = await create_entities(entity_request)
entity_id = result.entities[0].path_id
# Add an observation
request = AddObservationsRequest(
path_id=entity_id,
observations=[
ObservationCreate(content="Test observation")
]
path_id=entity_id, observations=[ObservationCreate(content="Test observation")]
)
updated = await add_observations(request)
@@ -37,9 +36,7 @@ async def test_add_basic_observation(client):
async def test_add_categorized_observations(client):
"""Test adding observations with different categories."""
# Create test entity
entity_request = CreateEntityRequest(
entities=[Entity(name="TestEntity", entity_type=EntityType.KNOWLEDGE)]
)
entity_request = CreateEntityRequest(entities=[Entity(name="TestEntity", entity_type="test")])
result = await create_entities(entity_request)
entity_id = result.entities[0].path_id
@@ -48,23 +45,20 @@ async def test_add_categorized_observations(client):
path_id=entity_id,
observations=[
ObservationCreate(
content="Implementation uses SQLite",
category=ObservationCategory.TECH
content="Implementation uses SQLite", category=ObservationCategory.TECH
),
ObservationCreate(
content="Chose SQLite for simplicity",
category=ObservationCategory.DESIGN
content="Chose SQLite for simplicity", category=ObservationCategory.DESIGN
),
ObservationCreate(
content="Supports atomic operations",
category=ObservationCategory.FEATURE
)
]
content="Supports atomic operations", category=ObservationCategory.FEATURE
),
],
)
updated = await add_observations(request)
assert len(updated.observations) == 3
# Find and verify each observation by category
tech_obs = next(o for o in updated.observations if o.category == ObservationCategory.TECH)
design_obs = next(o for o in updated.observations if o.category == ObservationCategory.DESIGN)
@@ -79,9 +73,7 @@ async def test_add_categorized_observations(client):
async def test_add_observations_with_context(client):
"""Test adding observations with shared context."""
# Create test entity
entity_request = CreateEntityRequest(
entities=[Entity(name="TestEntity", entity_type=EntityType.KNOWLEDGE)]
)
entity_request = CreateEntityRequest(entities=[Entity(name="TestEntity", entity_type="test")])
result = await create_entities(entity_request)
entity_id = result.entities[0].path_id
@@ -92,21 +84,17 @@ async def test_add_observations_with_context(client):
context=shared_context,
observations=[
ObservationCreate(
content="Decided on file format",
category=ObservationCategory.DESIGN
content="Decided on file format", category=ObservationCategory.DESIGN
),
ObservationCreate(
content="Will use markdown",
category=ObservationCategory.TECH
)
]
ObservationCreate(content="Will use markdown", category=ObservationCategory.TECH),
],
)
updated = await add_observations(request)
assert len(updated.observations) == 2
for obs in updated.observations:
# Note: context handling depends on our schema - might need adjustment
if hasattr(obs, 'context'):
if hasattr(obs, "context"):
assert obs.context == shared_context
@@ -116,11 +104,7 @@ async def test_add_observations_preserves_existing(client):
# Create entity with initial observation
entity_request = CreateEntityRequest(
entities=[
Entity(
name="TestEntity",
entity_type=EntityType.KNOWLEDGE,
observations=["Initial observation"]
)
Entity(name="TestEntity", entity_type="test", observations=["Initial observation"])
]
)
result = await create_entities(entity_request)
@@ -130,11 +114,8 @@ async def test_add_observations_preserves_existing(client):
request = AddObservationsRequest(
path_id=entity_id,
observations=[
ObservationCreate(
content="New observation",
category=ObservationCategory.TECH
)
]
ObservationCreate(content="New observation", category=ObservationCategory.TECH)
],
)
updated = await add_observations(request)
@@ -149,28 +130,19 @@ async def test_add_observations_preserves_existing(client):
async def test_add_multiple_observations_same_category(client):
"""Test adding multiple observations in the same category."""
# Create test entity
entity_request = CreateEntityRequest(
entities=[Entity(name="TestEntity", entity_type=EntityType.KNOWLEDGE)]
)
entity_request = CreateEntityRequest(entities=[Entity(name="TestEntity", entity_type="test")])
result = await create_entities(entity_request)
entity_id = result.entities[0].path_id
# Add multiple tech observations
tech_observations = [
"Uses async/await",
"Implements SQLite backend",
"Handles UTF-8 encoding"
]
tech_observations = ["Uses async/await", "Implements SQLite backend", "Handles UTF-8 encoding"]
request = AddObservationsRequest(
path_id=entity_id,
observations=[
ObservationCreate(
content=obs,
category=ObservationCategory.TECH
)
ObservationCreate(content=obs, category=ObservationCategory.TECH)
for obs in tech_observations
]
],
)
updated = await add_observations(request)
@@ -188,13 +160,10 @@ async def test_add_observation_to_nonexistent_entity(client):
request = AddObservationsRequest(
path_id="test/nonexistent",
observations=[
ObservationCreate(
content="This should fail",
category=ObservationCategory.NOTE
)
]
ObservationCreate(content="This should fail", category=ObservationCategory.NOTE)
],
)
# Should fail because entity doesn't exist
with pytest.raises(Exception): # Adjust exception type based on your error handling
await add_observations(request)
await add_observations(request)
+22 -45
View File
@@ -3,7 +3,7 @@
import pytest
from basic_memory.mcp.tools.knowledge import create_entities
from basic_memory.schemas.base import ObservationCategory, Entity, EntityType
from basic_memory.schemas.base import ObservationCategory, Entity
from basic_memory.schemas.request import CreateEntityRequest
@@ -14,9 +14,9 @@ async def test_create_basic_entity(client):
entities=[
Entity(
name="TestEntity",
entity_type=EntityType.KNOWLEDGE,
description="A test entity",
observations=["First observation"]
entity_type="test",
summary="A test entity",
observations=["First observation"],
)
]
)
@@ -25,13 +25,13 @@ async def test_create_basic_entity(client):
# Result should be an EntityListResponse
assert len(result.entities) == 1
# Check the created entity
entity = result.entities[0]
assert entity.name == "TestEntity"
assert entity.entity_type == EntityType.KNOWLEDGE
assert entity.entity_type == "test"
assert entity.path_id == "test_entity"
assert entity.description == "A test entity"
assert entity.summary == "A test entity"
# Check observations
assert len(entity.observations) == 1
@@ -50,13 +50,9 @@ async def test_create_entity_with_multiple_observations(client):
entities=[
Entity(
name="TestEntity",
entity_type=EntityType.KNOWLEDGE,
description="A test entity",
observations=[
"First observation",
"Second observation",
"Third observation"
]
entity_type="test",
summary="A test entity",
observations=["First observation", "Second observation", "Third observation"],
)
]
)
@@ -65,16 +61,12 @@ async def test_create_entity_with_multiple_observations(client):
entity = result.entities[0]
assert len(entity.observations) == 3
# Each observation should have:
# - content (the observation text)
# - category (default NOTE)
for obs in entity.observations:
assert obs.content in [
"First observation",
"Second observation",
"Third observation"
]
assert obs.content in ["First observation", "Second observation", "Third observation"]
assert obs.category == ObservationCategory.NOTE
@@ -83,26 +75,18 @@ async def test_create_multiple_entities(client):
"""Test creating multiple entities in one request."""
request = CreateEntityRequest(
entities=[
Entity(
name="Entity1",
entity_type=EntityType.KNOWLEDGE,
observations=["Observation 1"]
),
Entity(
name="Entity2",
entity_type=EntityType.KNOWLEDGE,
observations=["Observation 2"]
)
Entity(name="Entity1", entity_type="test", observations=["Observation 1"]),
Entity(name="Entity2", entity_type="test", observations=["Observation 2"]),
]
)
result = await create_entities(request)
assert len(result.entities) == 2
# Entities should be in order
assert result.entities[0].name == "Entity1"
assert result.entities[1].name == "Entity2"
# Each should have its observation
assert result.entities[0].observations[0].content == "Observation 1"
assert result.entities[1].observations[0].content == "Observation 2"
@@ -115,8 +99,8 @@ async def test_create_entity_without_observations(client):
entities=[
Entity(
name="TestEntity",
entity_type=EntityType.KNOWLEDGE,
description="A test entity without observations"
entity_type="test",
summary="A test entity without observations",
)
]
)
@@ -131,21 +115,14 @@ async def test_create_entity_without_observations(client):
@pytest.mark.asyncio
async def test_create_minimal_entity(client):
"""Test creating an entity with just name and type."""
request = CreateEntityRequest(
entities=[
Entity(
name="MinimalEntity",
entity_type=EntityType.KNOWLEDGE
)
]
)
request = CreateEntityRequest(entities=[Entity(name="MinimalEntity", entity_type="test")])
result = await create_entities(request)
entity = result.entities[0]
assert entity.name == "MinimalEntity"
assert entity.entity_type == EntityType.KNOWLEDGE
assert entity.entity_type == "test"
assert entity.path_id == "minimal_entity"
assert entity.description is None
assert entity.summary is None
assert len(entity.observations) == 0
assert len(entity.relations) == 0
assert len(entity.relations) == 0
+28 -61
View File
@@ -3,9 +3,8 @@
import pytest
from basic_memory.mcp.tools.knowledge import create_entities, create_relations
from basic_memory.schemas.base import Relation, Entity, EntityType
from basic_memory.schemas.base import Relation, Entity
from basic_memory.schemas.request import CreateEntityRequest, CreateRelationsRequest
from basic_memory.services.exceptions import EntityNotFoundError
@pytest.mark.asyncio
@@ -14,8 +13,8 @@ async def test_create_basic_relation(client):
# First create test entities
entity_request = CreateEntityRequest(
entities=[
Entity(name="SourceEntity", entity_type=EntityType.KNOWLEDGE),
Entity(name="TargetEntity", entity_type=EntityType.KNOWLEDGE)
Entity(name="SourceEntity", entity_type="test"),
Entity(name="TargetEntity", entity_type="test"),
]
)
await create_entities(entity_request)
@@ -23,21 +22,17 @@ 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)
assert len(result.entities) == 2
# Find source and target entities
source = next(e for e in result.entities if e.path_id == "source_entity")
target = next(e for e in result.entities if e.path_id == "target_entity")
# Both entities should have the relation for bi-directional navigation
assert len(source.relations) == 1
assert len(target.relations) == 1
@@ -61,8 +56,8 @@ async def test_create_relation_with_context(client):
# Create test entities
entity_request = CreateEntityRequest(
entities=[
Entity(name="Source", entity_type=EntityType.KNOWLEDGE),
Entity(name="Target", entity_type=EntityType.KNOWLEDGE)
Entity(name="Source", entity_type="test"),
Entity(name="Target", entity_type="test"),
]
)
await create_entities(entity_request)
@@ -73,7 +68,7 @@ async def test_create_relation_with_context(client):
from_id="source",
to_id="target",
relation_type="implements",
context="Implementation details"
context="Implementation details",
)
]
)
@@ -95,37 +90,29 @@ async def test_create_multiple_relations(client):
# Create test entities
entity_request = CreateEntityRequest(
entities=[
Entity(name="Entity1", entity_type=EntityType.KNOWLEDGE),
Entity(name="Entity2", entity_type=EntityType.KNOWLEDGE),
Entity(name="Entity3", entity_type=EntityType.KNOWLEDGE)
Entity(name="Entity1", entity_type="test"),
Entity(name="Entity2", entity_type="test"),
Entity(name="Entity3", entity_type="test"),
]
)
await create_entities(entity_request)
relation_request = CreateRelationsRequest(
relations=[
Relation(
from_id="entity1",
to_id="entity2",
relation_type="connects_to"
),
Relation(
from_id="entity2",
to_id="entity3",
relation_type="depends_on"
)
Relation(from_id="entity1", to_id="entity2", relation_type="connects_to"),
Relation(from_id="entity2", to_id="entity3", relation_type="depends_on"),
]
)
result = await create_relations(relation_request)
# Should return all involved entities
assert len(result.entities) == 3
# Get entities
entity1 = next(e for e in result.entities if e.path_id == "entity1")
entity2 = next(e for e in result.entities if e.path_id == "entity2")
entity3 = next(e for e in result.entities if e.path_id == "entity3")
# Entity1 and Entity2 should share the connects_to relation
assert len(entity1.relations) == 1
assert len(entity2.relations) == 2 # Has both relations
@@ -144,8 +131,8 @@ async def test_create_bidirectional_relations(client):
# Create test entities
entity_request = CreateEntityRequest(
entities=[
Entity(name="Service", entity_type=EntityType.KNOWLEDGE),
Entity(name="Database", entity_type=EntityType.KNOWLEDGE)
Entity(name="Service", entity_type="test"),
Entity(name="Database", entity_type="test"),
]
)
await create_entities(entity_request)
@@ -153,16 +140,8 @@ async def test_create_bidirectional_relations(client):
# Create relations in both directions
relation_request = CreateRelationsRequest(
relations=[
Relation(
from_id="service",
to_id="database",
relation_type="depends_on"
),
Relation(
from_id="database",
to_id="service",
relation_type="supports"
)
Relation(from_id="service", to_id="database", relation_type="depends_on"),
Relation(from_id="database", to_id="service", relation_type="supports"),
]
)
result = await create_relations(relation_request)
@@ -187,20 +166,12 @@ async def test_create_bidirectional_relations(client):
async def test_create_relation_with_invalid_entity(client):
"""Test creating a relation with non-existent entity fails."""
# Create only one of the needed entities
entity_request = CreateEntityRequest(
entities=[
Entity(name="RealEntity", entity_type=EntityType.KNOWLEDGE)
]
)
entity_request = CreateEntityRequest(entities=[Entity(name="RealEntity", entity_type="test")])
await create_entities(entity_request)
relation_request = CreateRelationsRequest(
relations=[
Relation(
from_id="real_entity",
to_id="non_existent_entity",
relation_type="depends_on"
)
Relation(from_id="real_entity", to_id="non_existent_entity", relation_type="depends_on")
]
)
@@ -215,20 +186,16 @@ async def test_create_duplicate_relation(client):
# Create test entities
entity_request = CreateEntityRequest(
entities=[
Entity(name="Source", entity_type=EntityType.KNOWLEDGE),
Entity(name="Target", entity_type=EntityType.KNOWLEDGE)
Entity(name="Source", entity_type="test"),
Entity(name="Target", entity_type="test"),
]
)
await create_entities(entity_request)
# Create relation
relation = Relation(
from_id="source",
to_id="target",
relation_type="connects_to"
)
relation = Relation(from_id="source", to_id="target", relation_type="connects_to")
relation_request = CreateRelationsRequest(relations=[relation])
# Create first relation
first_result = await create_relations(relation_request)
assert len(first_result.entities) == 2
@@ -237,4 +204,4 @@ async def test_create_duplicate_relation(client):
# Attempt to create same relation again
second_result = await create_relations(relation_request)
# Current behavior: No entities returned when duplicate relation fails
assert len(second_result.entities) == 0
assert len(second_result.entities) == 0
+3 -3
View File
@@ -5,8 +5,8 @@ import pytest
from basic_memory.mcp.tools.discovery import (
get_observation_categories,
)
from basic_memory.schemas import Entity, CreateEntityRequest, ObservationCategoryList, EntityType
from basic_memory.mcp.tools.knowledge import create_entities, add_observations
from basic_memory.schemas import Entity, CreateEntityRequest, ObservationCategoryList
from basic_memory.schemas.request import ObservationCreate, AddObservationsRequest
@@ -18,8 +18,8 @@ async def test_get_observation_categories(client):
entities=[
Entity(
name="Test Entity",
entity_type=EntityType.KNOWLEDGE,
description="Test entity",
entity_type="test",
summary="Test entity",
observations=[],
)
]
+16 -33
View File
@@ -3,7 +3,7 @@
import pytest
from basic_memory.mcp.tools.knowledge import get_entity, create_entities
from basic_memory.schemas.base import Entity, ObservationCategory, EntityType
from basic_memory.schemas.base import Entity, ObservationCategory
from basic_memory.schemas.request import CreateEntityRequest
from basic_memory.services.exceptions import EntityNotFoundError
@@ -16,9 +16,9 @@ async def test_get_basic_entity(client):
entities=[
Entity(
name="TestEntity",
entity_type=EntityType.KNOWLEDGE,
description="A test entity",
observations=["First observation"]
entity_type="test",
summary="A test entity",
observations=["First observation"],
)
]
)
@@ -30,10 +30,10 @@ async def test_get_basic_entity(client):
# Verify entity details
assert entity.name == "TestEntity"
assert entity.entity_type == EntityType.KNOWLEDGE
assert entity.entity_type == "test"
assert entity.path_id == "test_entity"
assert entity.description == "A test entity"
assert entity.summary == "A test entity"
# Check observations
assert len(entity.observations) == 1
obs = entity.observations[0]
@@ -47,8 +47,8 @@ async def test_get_entity_with_relations(client):
# Create two entities that will have a relation
entity_request = CreateEntityRequest(
entities=[
Entity(name="SourceEntity", entity_type=EntityType.KNOWLEDGE),
Entity(name="TargetEntity", entity_type=EntityType.KNOWLEDGE)
Entity(name="SourceEntity", entity_type="test"),
Entity(name="TargetEntity", entity_type="test"),
]
)
await create_entities(entity_request)
@@ -57,14 +57,10 @@ async def test_get_entity_with_relations(client):
from basic_memory.mcp.tools.knowledge import create_relations
from basic_memory.schemas.request import CreateRelationsRequest
from basic_memory.schemas.base import Relation
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)
@@ -83,11 +79,7 @@ async def test_get_entity_with_categorized_observations(client):
# Create entity with categorized observations
entity_request = CreateEntityRequest(
entities=[
Entity(
name="TestEntity",
entity_type=EntityType.KNOWLEDGE,
description="Test entity with categories"
)
Entity(name="TestEntity", entity_type="test", summary="Test entity with categories")
]
)
result = await create_entities(entity_request)
@@ -100,19 +92,10 @@ async def test_get_entity_with_categorized_observations(client):
obs_request = AddObservationsRequest(
path_id=path_id,
observations=[
ObservationCreate(
content="Technical detail",
category=ObservationCategory.TECH
),
ObservationCreate(
content="Design decision",
category=ObservationCategory.DESIGN
),
ObservationCreate(
content="Feature note",
category=ObservationCategory.FEATURE
)
]
ObservationCreate(content="Technical detail", category=ObservationCategory.TECH),
ObservationCreate(content="Design decision", category=ObservationCategory.DESIGN),
ObservationCreate(content="Feature note", category=ObservationCategory.FEATURE),
],
)
await add_observations(obs_request)
+19 -57
View File
@@ -2,10 +2,10 @@
import pytest
from basic_memory.mcp.tools.search import open_nodes
from basic_memory.mcp.tools.knowledge import create_entities
from basic_memory.mcp.tools.search import open_nodes
from basic_memory.schemas import EntityListResponse
from basic_memory.schemas.base import Entity, EntityType
from basic_memory.schemas.base import Entity
from basic_memory.schemas.request import CreateEntityRequest, OpenNodesRequest
@@ -15,16 +15,8 @@ async def test_open_multiple_entities(client):
# Create some test entities
entity_request = CreateEntityRequest(
entities=[
Entity(
name="Entity1",
entity_type=EntityType.KNOWLEDGE,
description="First test entity"
),
Entity(
name="Entity2",
entity_type=EntityType.KNOWLEDGE,
description="Second test entity"
)
Entity(name="Entity1", entity_type="test", summary="First test entity"),
Entity(name="Entity2", entity_type="test", summary="Second test entity"),
]
)
create_result = await create_entities(entity_request)
@@ -35,14 +27,14 @@ async def test_open_multiple_entities(client):
result = await open_nodes(request)
assert isinstance(result, EntityListResponse)
response = EntityListResponse.model_validate(result)
# Verify we got a dictionary with both entities
assert len(response.entities) == 2
for response_entity in response.entities:
assert response_entity.path_id in path_ids
assert response_entity.name in ["Entity1", "Entity2"]
@pytest.mark.asyncio
async def test_open_nodes_with_details(client):
@@ -52,9 +44,9 @@ async def test_open_nodes_with_details(client):
entities=[
Entity(
name="DetailedEntity",
entity_type=EntityType.KNOWLEDGE,
description="Test entity with details",
observations=["First observation", "Second observation"]
entity_type="test",
summary="Test entity with details",
observations=["First observation", "Second observation"],
)
]
)
@@ -69,8 +61,8 @@ async def test_open_nodes_with_details(client):
# Verify all details are present
entity = response.entities[0]
assert entity.name == "DetailedEntity"
assert entity.entity_type == EntityType.KNOWLEDGE
assert entity.description == "Test entity with details"
assert entity.entity_type == "test"
assert entity.summary == "Test entity with details"
assert len(entity.observations) == 2
@@ -80,16 +72,8 @@ async def test_open_nodes_with_relations(client):
# Create related entities
entity_request = CreateEntityRequest(
entities=[
Entity(
name="Service",
entity_type=EntityType.KNOWLEDGE,
description="A service"
),
Entity(
name="Database",
entity_type=EntityType.KNOWLEDGE,
description="A database"
)
Entity(name="Service", entity_type="test", summary="A service"),
Entity(name="Database", entity_type="test", summary="A database"),
]
)
create_result = await create_entities(entity_request)
@@ -101,13 +85,7 @@ async def test_open_nodes_with_relations(client):
from basic_memory.schemas.base import Relation
relation_request = CreateRelationsRequest(
relations=[
Relation(
from_id=path_ids[0],
to_id=path_ids[1],
relation_type="depends_on"
)
]
relations=[Relation(from_id=path_ids[0], to_id=path_ids[1], relation_type="depends_on")]
)
await create_relations(relation_request)
@@ -125,21 +103,12 @@ async def test_open_nodes_with_relations(client):
async def test_open_nonexistent_nodes(client):
"""Test behavior when some requested nodes don't exist."""
# First create one real entity
entity_request = CreateEntityRequest(
entities=[
Entity(
name="RealEntity",
entity_type=EntityType.KNOWLEDGE
)
]
)
entity_request = CreateEntityRequest(entities=[Entity(name="RealEntity", entity_type="test")])
create_result = await create_entities(entity_request)
real_path_id = create_result.entities[0].path_id
# Try to open both real and non-existent
request = OpenNodesRequest(
path_ids=[real_path_id, "nonexistent"]
)
request = OpenNodesRequest(path_ids=[real_path_id, "nonexistent"])
result = await open_nodes(request)
response = EntityListResponse.model_validate(result)
@@ -152,14 +121,7 @@ async def test_open_nonexistent_nodes(client):
async def test_open_single_node(client):
"""Test behavior with single path_id."""
# Create an entity
entity_request = CreateEntityRequest(
entities=[
Entity(
name="SingleEntity",
entity_type=EntityType.KNOWLEDGE
)
]
)
entity_request = CreateEntityRequest(entities=[Entity(name="SingleEntity", entity_type="test")])
create_result = await create_entities(entity_request)
path_id = create_result.entities[0].path_id
@@ -170,4 +132,4 @@ async def test_open_single_node(client):
# Should get just that entity
assert len(response.entities) == 1
assert path_id in response.entities[0].path_id
assert path_id in response.entities[0].path_id
+45 -41
View File
@@ -1,15 +1,13 @@
"""Tests for the EntityRepository."""
from datetime import datetime, UTC
from datetime import datetime
import pytest
import pytest_asyncio
from sqlalchemy import select, text
from sqlalchemy.exc import IntegrityError
from sqlalchemy import select
from basic_memory import db
from basic_memory.models import Entity, Observation, Relation
from basic_memory.models.knowledge import EntityType
from basic_memory.repository.entity_repository import EntityRepository
@@ -31,17 +29,19 @@ async def related_entities(session_maker):
async with db.scoped_session(session_maker) as session:
source = Entity(
name="source",
entity_type=EntityType.KNOWLEDGE,
entity_type="test",
path_id="source/source",
file_path="source/source.md",
description="Source entity",
summary="Source entity",
content_type="text/markdown",
)
target = Entity(
name="target",
entity_type=EntityType.KNOWLEDGE,
entity_type="test",
path_id="target/target",
file_path="target/target.md",
description="Target entity",
summary="Target entity",
content_type="text/markdown",
)
session.add(source)
session.add(target)
@@ -58,10 +58,11 @@ async def test_create_entity(entity_repository: EntityRepository):
"""Test creating a new entity"""
entity_data = {
"name": "Test",
"entity_type": EntityType.KNOWLEDGE,
"entity_type": "test",
"path_id": "test/test",
"file_path": "test/test.md",
"description": "Test description",
"summary": "Test description",
"content_type": "text/markdown",
}
entity = await entity_repository.create(entity_data)
@@ -91,17 +92,19 @@ async def test_create_all(entity_repository: EntityRepository):
entity_data = [
{
"name": "Test_1",
"entity_type": EntityType.KNOWLEDGE,
"entity_type": "test",
"path_id": "test/test_1",
"file_path": "test/test_1.md",
"description": "Test description",
"summary": "Test description",
"content_type": "text/markdown",
},
{
"name": "Test-2",
"entity_type": EntityType.KNOWLEDGE,
"entity_type": "test",
"path_id": "test/test_2",
"file_path": "test/test_2.md",
"description": "Test description",
"summary": "Test description",
"content_type": "text/markdown",
},
]
entities = await entity_repository.create_all(entity_data)
@@ -127,10 +130,11 @@ async def test_create_entity_null_description(session_maker, entity_repository:
"""Test creating an entity with null description"""
entity_data = {
"name": "Test",
"entity_type": EntityType.KNOWLEDGE,
"entity_type": "test",
"path_id": "test/test",
"file_path": "test/test.md",
"description": None,
"content_type": "text/markdown",
"summary": None,
}
entity = await entity_repository.create(entity_data)
@@ -164,7 +168,7 @@ async def test_find_by_id(entity_repository: EntityRepository, sample_entity: En
async def test_update_entity(entity_repository: EntityRepository, sample_entity: Entity):
"""Test updating an entity"""
updated = await entity_repository.update(
sample_entity.id, {"description": "Updated description"}
sample_entity.id, {"summary": "Updated description"}
)
assert updated is not None
assert updated.summary == "Updated description"
@@ -182,7 +186,7 @@ async def test_update_entity(entity_repository: EntityRepository, sample_entity:
@pytest.mark.asyncio
async def test_update_entity_to_null(entity_repository: EntityRepository, sample_entity: Entity):
"""Test updating an entity's description to null"""
updated = await entity_repository.update(sample_entity.id, {"description": None})
updated = await entity_repository.update(sample_entity.id, {"summary": None})
assert updated is not None
assert updated.summary is None
@@ -269,8 +273,6 @@ async def test_delete_nonexistent_entity(entity_repository: EntityRepository):
assert result is False
@pytest_asyncio.fixture
async def test_entities(session_maker):
"""Create multiple test entities."""
@@ -278,24 +280,27 @@ async def test_entities(session_maker):
entities = [
Entity(
name="entity1",
entity_type=EntityType.KNOWLEDGE,
description="First test entity",
entity_type="test",
summary="First test entity",
path_id="type1/entity1",
file_path="type1/entity1.md",
content_type= "text/markdown",
),
Entity(
name="entity2",
entity_type=EntityType.KNOWLEDGE,
description="Second test entity",
entity_type="test",
summary="Second test entity",
path_id="type1/entity2",
file_path="type1/entity2.md",
content_type="text/markdown",
),
Entity(
name="entity3",
entity_type=EntityType.KNOWLEDGE,
description="Third test entity",
entity_type="test",
summary="Third test entity",
path_id="type2/entity3",
file_path="type2/entity3.md",
content_type="text/markdown",
),
]
session.add_all(entities)
@@ -384,31 +389,34 @@ async def test_delete_by_path_ids_with_observations(
@pytest.mark.asyncio
async def test_list_entities_with_related(entity_repository: EntityRepository, session_maker):
"""Test listing entities with related entities included."""
# Create test entities
async with db.scoped_session(session_maker) as session:
# Core entities
core = Entity(
name="core_service",
entity_type=EntityType.NOTE,
entity_type="note",
path_id="service/core",
file_path="service/core.md",
description="Core service"
summary="Core service",
content_type="text/markdown",
)
dbe = Entity(
name="db_service",
entity_type=EntityType.KNOWLEDGE,
entity_type="test",
path_id="service/db",
file_path="service/db.md",
description="Database service"
summary="Database service",
content_type="text/markdown",
)
# Related entity of different type
config = Entity(
name="service_config",
entity_type=EntityType.KNOWLEDGE,
entity_type="test",
path_id="config/service",
file_path="config/service.md",
description="Service configuration"
summary="Service configuration",
content_type="text/markdown",
)
session.add_all([core, dbe, config])
await session.flush()
@@ -418,23 +426,19 @@ async def test_list_entities_with_related(entity_repository: EntityRepository, s
# core -> db (depends_on)
Relation(from_id=core.id, to_id=dbe.id, relation_type="depends_on"),
# config -> core (configures)
Relation(from_id=config.id, to_id=core.id, relation_type="configures")
Relation(from_id=config.id, to_id=core.id, relation_type="configures"),
]
session.add_all(relations)
# Test 1: List without related entities
services = await entity_repository.list_entities(
entity_type=EntityType.KNOWLEDGE,
include_related=False
)
services = await entity_repository.list_entities(entity_type="test", include_related=False)
assert len(services) == 2
service_names = {s.name for s in services}
assert service_names == {"service_config", "db_service"}
# Test 2: List services with related entities
services_and_related = await entity_repository.list_entities(
entity_type=EntityType.KNOWLEDGE,
include_related=True
entity_type="test", include_related=True
)
assert len(services_and_related) == 3
# Should include both services and the config
@@ -444,4 +448,4 @@ async def test_list_entities_with_related(entity_repository: EntityRepository, s
# Test 3: Verify relations are loaded
core_service = next(e for e in services_and_related if e.name == "core_service")
assert len(core_service.outgoing_relations) > 0 # Has incoming relation from config
assert len(core_service.incoming_relations) > 0 # Has outgoing relation to db
assert len(core_service.incoming_relations) > 0 # Has outgoing relation to db
+18 -13
View File
@@ -7,7 +7,6 @@ from sqlalchemy.ext.asyncio import async_sessionmaker
from basic_memory import db
from basic_memory.models import Entity, Observation
from basic_memory.models.knowledge import EntityType
from basic_memory.repository.observation_repository import ObservationRepository
@@ -90,10 +89,11 @@ async def test_delete_observations(session_maker: async_sessionmaker, repo):
async with db.scoped_session(session_maker) as session:
entity = Entity(
name="test_entity",
entity_type=EntityType.KNOWLEDGE,
description="Test entity",
entity_type="test",
summary="Test entity",
path_id="test/test_entity",
file_path="test/test_entity.md",
content_type="text/markdown",
)
session.add(entity)
await session.flush()
@@ -119,10 +119,11 @@ async def test_delete_observation_by_id(session_maker: async_sessionmaker, repo)
async with db.scoped_session(session_maker) as session:
entity = Entity(
name="test_entity",
entity_type=EntityType.KNOWLEDGE,
description="Test entity",
entity_type="test",
summary="Test entity",
path_id="test/test_entity",
file_path="test/test_entity.md",
content_type="text/markdown",
)
session.add(entity)
await session.flush()
@@ -147,10 +148,11 @@ async def test_delete_observation_by_content(session_maker: async_sessionmaker,
async with db.scoped_session(session_maker) as session:
entity = Entity(
name="test_entity",
entity_type=EntityType.KNOWLEDGE,
description="Test entity",
entity_type="test",
summary="Test entity",
path_id="test/test_entity",
file_path="test/test_entity.md",
content_type="text/markdown",
)
session.add(entity)
await session.flush()
@@ -177,10 +179,11 @@ async def test_find_by_category(session_maker: async_sessionmaker, repo):
async with db.scoped_session(session_maker) as session:
entity = Entity(
name="test_entity",
entity_type=EntityType.KNOWLEDGE,
description="Test entity",
entity_type="test",
summary="Test entity",
path_id="test/test_entity",
file_path="test/test_entity.md",
content_type="text/markdown",
)
session.add(entity)
await session.flush()
@@ -218,10 +221,11 @@ async def test_observation_categories(session_maker: async_sessionmaker, repo):
async with db.scoped_session(session_maker) as session:
entity = Entity(
name="test_entity",
entity_type=EntityType.KNOWLEDGE,
description="Test entity",
entity_type="test",
summary="Test entity",
path_id="test/test_entity",
file_path="test/test_entity.md",
content_type="text/markdown",
)
session.add(entity)
await session.flush()
@@ -265,10 +269,11 @@ async def test_find_by_category_case_sensitivity(session_maker: async_sessionmak
async with db.scoped_session(session_maker) as session:
entity = Entity(
name="test_entity",
entity_type=EntityType.KNOWLEDGE,
description="Test entity",
entity_type="test",
summary="Test entity",
path_id="test/test_entity",
file_path="test/test_entity.md",
content_type="text/markdown",
)
session.add(entity)
await session.flush()
+15 -10
View File
@@ -6,7 +6,6 @@ import sqlalchemy
from basic_memory import db
from basic_memory.models import Entity, Relation
from basic_memory.models.knowledge import EntityType
from basic_memory.repository.relation_repository import RelationRepository
@@ -15,10 +14,11 @@ async def source_entity(session_maker):
"""Create a source entity for testing relations."""
entity = Entity(
name="test_source",
entity_type=EntityType.KNOWLEDGE,
entity_type="test",
path_id="source/test_source",
file_path="source/test_source.md",
description="Source entity",
summary="Source entity",
content_type="text/markdown",
)
async with db.scoped_session(session_maker) as session:
session.add(entity)
@@ -31,10 +31,11 @@ async def target_entity(session_maker):
"""Create a target entity for testing relations."""
entity = Entity(
name="test_target",
entity_type=EntityType.KNOWLEDGE,
entity_type="test",
path_id="target/test_target",
file_path="target/test_target.md",
description="Target entity",
summary="Target entity",
content_type="text/markdown",
)
async with db.scoped_session(session_maker) as session:
session.add(entity)
@@ -60,10 +61,11 @@ async def related_entity(entity_repository):
"""Create a second entity for testing relations"""
entity_data = {
"name": "Related Entity",
"entity_type": EntityType.KNOWLEDGE,
"entity_type": "test",
"path_id": "test/related_entity",
"file_path": "test/related_entity.md",
"description": "A related test entity",
"summary": "A related test entity",
"content_type": "text/markdown",
"references": "",
}
return await entity_repository.create(entity_data)
@@ -158,12 +160,15 @@ async def test_find_by_entities(
assert relations[0].id == sample_relation.id
assert relations[0].relation_type == sample_relation.relation_type
@pytest.mark.asyncio
async def test_find_relation(relation_repository: RelationRepository, sample_relation: Relation):
"""Test finding relations by type"""
relation = await relation_repository.find_relation(from_path_id=sample_relation.from_entity.path_id,
to_path_id=sample_relation.to_entity.path_id,
relation_type=sample_relation.relation_type)
relation = await relation_repository.find_relation(
from_path_id=sample_relation.from_entity.path_id,
to_path_id=sample_relation.to_entity.path_id,
relation_type=sample_relation.relation_type,
)
assert relation.id == sample_relation.id
+14 -13
View File
@@ -20,7 +20,7 @@ def test_entity_in_minimal():
entity = Entity.model_validate(data)
assert entity.name == "test_entity"
assert entity.entity_type == "knowledge"
assert entity.description is None
assert entity.summary is None
assert entity.observations == []
@@ -29,13 +29,13 @@ def test_entity_in_complete():
data = {
"name": "test_entity",
"entity_type": "knowledge",
"description": "A test entity",
"summary": "A test entity",
"observations": ["Test observation"],
}
entity = Entity.model_validate(data)
assert entity.name == "test_entity"
assert entity.entity_type == "knowledge"
assert entity.description == "A test entity"
assert entity.summary == "A test entity"
assert len(entity.observations) == 1
assert entity.observations[0] == "Test observation"
@@ -43,7 +43,7 @@ def test_entity_in_complete():
def test_entity_in_validation():
"""Test validation errors for EntityIn."""
with pytest.raises(ValidationError):
Entity.model_validate({}) # Missing required fields
Entity.model_validate({"file_path": "test"}) # Missing required fields
with pytest.raises(ValidationError):
Entity.model_validate({"name": "test"}) # Missing entityType
@@ -85,12 +85,12 @@ def test_create_entities_input():
data = {
"entities": [
{"name": "entity1", "entity_type": "knowledge"},
{"name": "entity2", "entity_type": "knowledge", "description": "test description"},
{"name": "entity2", "entity_type": "knowledge", "summary": "test description"},
]
}
create_input = CreateEntityRequest.model_validate(data)
assert len(create_input.entities) == 2
assert create_input.entities[1].description == "test description"
assert create_input.entities[1].summary == "test description"
# Empty entities list should fail
with pytest.raises(ValidationError):
@@ -104,7 +104,8 @@ def test_entity_out_from_attributes():
"path_id": "test/test",
"name": "test",
"entity_type": "knowledge",
"description": "test description",
"content_type": "text/markdown",
"summary": "test description",
"observations": [{"id": 1, "content": "test obs", "context": None}],
"relations": [
{"id": 1, "from_id": "test/test", "to_id": "test/test", "relation_type": "test", "context": None}
@@ -112,7 +113,7 @@ def test_entity_out_from_attributes():
}
entity = EntityResponse.model_validate(db_data)
assert entity.path_id == "test/test"
assert entity.description == "test description"
assert entity.summary == "test description"
assert len(entity.observations) == 1
assert len(entity.relations) == 1
@@ -121,7 +122,7 @@ def test_optional_fields():
"""Test handling of optional fields."""
# Create with no optional fields
entity = Entity.model_validate({"name": "test", "entity_type": "knowledge"})
assert entity.description is None
assert entity.summary is None
assert entity.observations == []
# Create with empty optional fields
@@ -129,18 +130,18 @@ def test_optional_fields():
{
"name": "test",
"entity_type": "knowledge",
"description": None,
"summary": None,
"observations": [],
}
)
assert entity.description is None
assert entity.summary is None
assert entity.observations == []
# Create with some optional fields
entity = Entity.model_validate(
{"name": "test", "entity_type": "knowledge", "description": "test", "observations": []}
{"name": "test", "entity_type": "knowledge", "summary": "test", "observations": []}
)
assert entity.description == "test"
assert entity.summary == "test"
assert entity.observations == []
+41 -41
View File
@@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from basic_memory.models import Entity as EntityModel
from basic_memory.repository.entity_repository import EntityRepository
from basic_memory.schemas import Entity as EntitySchema, EntityType
from basic_memory.schemas import Entity as EntitySchema
from basic_memory.services.entity_service import EntityService
from basic_memory.services.exceptions import EntityNotFoundError
@@ -29,8 +29,8 @@ async def test_create_entity(entity_service: EntityService):
"""Test successful entity creation."""
entity_data = EntitySchema(
name="TestEntity",
entity_type=EntityType.KNOWLEDGE,
description="A test entity description",
entity_type="test",
summary="A test entity description",
observations=["this is a test observation"],
)
@@ -42,7 +42,7 @@ async def test_create_entity(entity_service: EntityService):
assert entity.name == "TestEntity"
assert entity.path_id == entity_data.path_id
assert entity.file_path == entity_data.file_path
assert entity.entity_type == EntityType.KNOWLEDGE
assert entity.entity_type == "test"
assert entity.summary == "A test entity description"
assert entity.created_at is not None
assert entity.observations[0].content == "this is a test observation"
@@ -52,7 +52,7 @@ async def test_create_entity(entity_service: EntityService):
retrieved = await entity_service.get_by_path_id(entity_data.path_id)
assert retrieved.summary == "A test entity description"
assert retrieved.name == "TestEntity"
assert retrieved.entity_type == EntityType.KNOWLEDGE
assert retrieved.entity_type == "test"
assert retrieved.summary == "A test entity description"
assert retrieved.created_at is not None
assert retrieved.observations[0].content == "this is a test observation"
@@ -63,14 +63,14 @@ async def test_create_entities(entity_service: EntityService):
entity_data = [
EntitySchema(
name="TestEntity1",
entity_type=EntityType.KNOWLEDGE,
description="A test entity description",
entity_type="test",
summary="A test entity description",
observations=["this is a test observation"],
),
EntitySchema(
name="TestEntity2",
entity_type=EntityType.KNOWLEDGE,
description="A test entity description",
entity_type="test",
summary="A test entity description",
observations=["this is a test observation"],
),
]
@@ -83,7 +83,7 @@ async def test_create_entities(entity_service: EntityService):
entity1 = entities[0]
assert isinstance(entity1, EntityModel)
assert entity1.name == "TestEntity1"
assert entity1.entity_type == EntityType.KNOWLEDGE
assert entity1.entity_type == "test"
assert entity1.summary == "A test entity description"
assert entity1.created_at is not None
assert entity1.observations[0].content == "this is a test observation"
@@ -92,7 +92,7 @@ async def test_create_entities(entity_service: EntityService):
entity2 = entities[1]
assert isinstance(entity1, EntityModel)
assert entity2.name == "TestEntity2"
assert entity2.entity_type == EntityType.KNOWLEDGE
assert entity2.entity_type == "test"
assert entity2.summary == "A test entity description"
assert entity2.created_at is not None
assert entity2.observations[0].content == "this is a test observation"
@@ -109,16 +109,16 @@ async def test_get_by_path_id(entity_service: EntityService):
"""Test finding entity by type and name combination."""
entity1_data = EntitySchema(
name="TestEntity1",
entity_type=EntityType.KNOWLEDGE,
description="First test entity",
entity_type="test",
summary="First test entity",
observations=[],
)
entity1 = await entity_service.create_entity(entity1_data)
entity2_data = EntitySchema(
name="TestEntity2",
entity_type=EntityType.KNOWLEDGE,
description="Second test entity",
name="TestEntity2",
entity_type="test",
summary="Second test entity",
observations=[],
)
entity2 = await entity_service.create_entity(entity2_data)
@@ -144,7 +144,7 @@ async def test_get_by_path_id(entity_service: EntityService):
async def test_create_entity_no_description(entity_service: EntityService):
"""Test creating entity without description (should be None)."""
entity_data = EntitySchema(name="TestEntity", entity_type=EntityType.KNOWLEDGE, observations=[])
entity_data = EntitySchema(name="TestEntity", entity_type="test", observations=[])
entity = await entity_service.create_entity(entity_data)
assert entity.summary is None
@@ -158,8 +158,8 @@ async def test_get_entity_success(entity_service: EntityService):
"""Test successful entity retrieval."""
entity_data = EntitySchema(
name="TestEntity",
entity_type=EntityType.KNOWLEDGE,
description="Test description",
entity_type="test",
summary="Test description",
observations=[],
)
await entity_service.create_entity(entity_data)
@@ -169,7 +169,7 @@ async def test_get_entity_success(entity_service: EntityService):
assert isinstance(retrieved, EntityModel)
assert retrieved.name == "TestEntity"
assert retrieved.entity_type == EntityType.KNOWLEDGE
assert retrieved.entity_type == "test"
assert retrieved.summary == "Test description"
@@ -177,15 +177,15 @@ async def test_update_entity_description(entity_service: EntityService):
"""Test updating an entity's description."""
entity_data = EntitySchema(
name="TestEntity",
entity_type=EntityType.KNOWLEDGE,
description="Initial description",
entity_type="test",
summary="Initial description",
observations=[],
)
await entity_service.create_entity(entity_data)
# Update description using path_id
updated = await entity_service.update_entity(
entity_data.path_id, {"description": "Updated description"}
entity_data.path_id, {"summary": "Updated description"}
)
assert updated.summary == "Updated description"
@@ -198,14 +198,14 @@ async def test_update_entity_description_to_none(entity_service: EntityService):
"""Test updating an entity's description to None."""
entity_data = EntitySchema(
name="TestEntity",
entity_type=EntityType.KNOWLEDGE,
description="Initial description",
entity_type="test",
summary="Initial description",
observations=[],
)
await entity_service.create_entity(entity_data)
# Update description to None using path_id
updated = await entity_service.update_entity(entity_data.path_id, {"description": None})
updated = await entity_service.update_entity(entity_data.path_id, {"summary": None})
assert updated.summary is None
# Verify after retrieval
@@ -217,7 +217,7 @@ async def test_delete_entity_success(entity_service: EntityService):
"""Test successful entity deletion."""
entity_data = EntitySchema(
name="TestEntity",
entity_type=EntityType.KNOWLEDGE,
entity_type="test",
observations=[],
)
await entity_service.create_entity(entity_data)
@@ -249,8 +249,8 @@ async def test_create_entity_with_special_chars(entity_service: EntityService):
description = "Description with $pecial chars & symbols!"
entity_data = EntitySchema(
name=name,
entity_type=EntityType.KNOWLEDGE,
description=description,
entity_type="test",
summary=description,
)
entity = await entity_service.create_entity(entity_data)
@@ -267,8 +267,8 @@ async def test_create_entity_long_description(entity_service: EntityService):
long_description = "A" * 1000 # 1000 character description
entity_data = EntitySchema(
name="TestEntity",
entity_type=EntityType.KNOWLEDGE,
description=long_description,
entity_type="test",
summary=long_description,
observations=[],
)
@@ -285,14 +285,14 @@ async def test_open_nodes_by_path_ids(entity_service: EntityService):
# Create test entities
entity1_data = EntitySchema(
name="Entity1",
entity_type=EntityType.KNOWLEDGE,
description="First entity",
entity_type="test",
summary="First entity",
observations=[],
)
entity2_data = EntitySchema(
name="Entity2",
entity_type=EntityType.KNOWLEDGE,
description="Second entity",
entity_type="test",
summary="Second entity",
observations=[],
)
await entity_service.create_entity(entity1_data)
@@ -318,8 +318,8 @@ async def test_open_nodes_some_not_found(entity_service: EntityService):
# Create one test entity
entity_data = EntitySchema(
name="Entity1",
entity_type=EntityType.KNOWLEDGE,
description="Test entity",
entity_type="test",
summary="Test entity",
observations=[],
)
await entity_service.create_entity(entity_data)
@@ -337,14 +337,14 @@ async def test_delete_entities_by_path_ids(entity_service: EntityService):
# Create test entities
entity1_data = EntitySchema(
name="Entity1",
entity_type=EntityType.KNOWLEDGE,
description="First entity",
entity_type="test",
summary="First entity",
observations=[],
)
entity2_data = EntitySchema(
name="Entity2",
entity_type=EntityType.KNOWLEDGE,
description="Second entity",
entity_type="test",
summary="Second entity",
observations=[],
)
await entity_service.create_entity(entity1_data)
+29 -41
View File
@@ -6,7 +6,7 @@ import pytest
import yaml
from basic_memory.models import Entity as EntityModel
from basic_memory.models.knowledge import EntityType, ObservationCategory
from basic_memory.models.knowledge import ObservationCategory
from basic_memory.schemas import Entity as EntitySchema, Relation as RelationSchema
from basic_memory.schemas.request import ObservationCreate
from basic_memory.services import EntityService
@@ -20,8 +20,8 @@ async def test_get_entity_path(knowledge_service: KnowledgeService):
id=1,
path_id="test-entity",
name="test-entity",
entity_type=EntityType.KNOWLEDGE,
description="Test entity",
entity_type="test",
summary="Test entity",
)
path = knowledge_service.get_entity_path(entity)
assert path == Path(knowledge_service.base_path / "test-entity.md")
@@ -31,9 +31,7 @@ async def test_get_entity_path(knowledge_service: KnowledgeService):
async def test_create_entity(knowledge_service: KnowledgeService):
"""Should create entity in DB and write file correctly."""
# Setup
entity_schema = EntitySchema(
name="test-entity", entity_type=EntityType.KNOWLEDGE, description="Test entity"
)
entity_schema = EntitySchema(name="test-entity", entity_type="test", summary="Test entity")
# Execute
created = await knowledge_service.create_entity(entity_schema)
@@ -41,7 +39,7 @@ async def test_create_entity(knowledge_service: KnowledgeService):
# Verify DB entity
assert created.name == entity_schema.name
assert created.entity_type == entity_schema.entity_type
assert created.summary == entity_schema.description
assert created.summary == entity_schema.summary
assert created.checksum is not None
assert created.path_id == "test_entity"
assert created.file_path == "test_entity.md"
@@ -65,9 +63,7 @@ async def test_create_entity(knowledge_service: KnowledgeService):
async def test_create_multiple_entities(knowledge_service: KnowledgeService):
"""Should create multiple entities successfully."""
entities = [
EntitySchema(
name=f"entity-{i}", entity_type=EntityType.KNOWLEDGE, description=f"Test entity {i}"
)
EntitySchema(name=f"entity-{i}", entity_type="test", summary=f"Test entity {i}")
for i in range(3)
]
@@ -85,10 +81,10 @@ async def test_create_relations(knowledge_service: KnowledgeService, entity_serv
"""Should create relations and update related entity files."""
# Create test entities
entity1 = await knowledge_service.create_entity(
EntitySchema(name="entity1", entity_type=EntityType.KNOWLEDGE, description="Test entity 1")
EntitySchema(name="entity1", entity_type="test", summary="Test entity 1")
)
entity2 = await knowledge_service.create_entity(
EntitySchema(name="entity2", entity_type=EntityType.KNOWLEDGE, description="Test entity 2")
EntitySchema(name="entity2", entity_type="test", summary="Test entity 2")
)
# Create relation
@@ -124,15 +120,15 @@ async def test_update_knowledge_entity_description(knowledge_service: KnowledgeS
entity = await knowledge_service.create_entity(
EntitySchema(
name="test",
entity_type=EntityType.KNOWLEDGE,
description="Test entity",
entity_type="test",
summary="Test entity",
entity_metadata={"status": "draft"},
)
)
# Update description
updated = await knowledge_service.update_entity(
entity.path_id, description="Updated description"
entity.path_id, summary="Updated description"
)
# Verify file has new description but preserved metadata
@@ -154,8 +150,8 @@ async def test_update_note_entity_content(knowledge_service: KnowledgeService):
entity = await knowledge_service.create_entity(
EntitySchema(
name="test",
entity_type=EntityType.NOTE,
description="Test note",
entity_type="note",
summary="Test note",
entity_metadata={"status": "draft"},
)
)
@@ -184,8 +180,8 @@ async def test_update_entity_name(knowledge_service: KnowledgeService):
entity = await knowledge_service.create_entity(
EntitySchema(
name="test",
entity_type=EntityType.KNOWLEDGE,
description="Test entity",
entity_type="test",
summary="Test entity",
entity_metadata={"status": "draft"},
)
)
@@ -216,26 +212,20 @@ async def test_update_entity_type_note_to_knowledge(knowledge_service: Knowledge
entity = await knowledge_service.create_entity(
EntitySchema(
name="test",
entity_type=EntityType.NOTE,
description="Test note",
entity_metadata={"status": "draft"}
entity_type="note",
summary="Test note",
entity_metadata={"status": "draft"},
)
)
# First update with some content as a note
await knowledge_service.update_entity(
entity.path_id,
content=initial_content
)
await knowledge_service.update_entity(entity.path_id, content=initial_content)
# Then update to knowledge type
updated = await knowledge_service.update_entity(
entity.path_id,
entity_type=EntityType.KNOWLEDGE
)
updated = await knowledge_service.update_entity(entity.path_id, entity_type="test")
# Verify type was updated in DB
assert updated.entity_type == EntityType.KNOWLEDGE
assert updated.entity_type == "test"
# Verify frontmatter was updated
file_path = knowledge_service.get_entity_path(updated)
@@ -243,7 +233,7 @@ async def test_update_entity_type_note_to_knowledge(knowledge_service: Knowledge
_, frontmatter, _ = content.split("---", 2)
metadata = yaml.safe_load(frontmatter)
assert metadata["type"] == EntityType.KNOWLEDGE
assert metadata["type"] == "test"
# Verify content format changed to knowledge style (structured)
assert "# test" in content
@@ -257,9 +247,9 @@ async def test_update_entity_type_knowledge_to_note(knowledge_service: Knowledge
entity = await knowledge_service.create_entity(
EntitySchema(
name="test",
entity_type=EntityType.KNOWLEDGE,
description="Test knowledge entity",
entity_metadata={"status": "draft"}
entity_type="test",
summary="Test knowledge entity",
entity_metadata={"status": "draft"},
)
)
@@ -272,13 +262,11 @@ async def test_update_entity_type_knowledge_to_note(knowledge_service: Knowledge
# Update to note type with new content
new_content = "# Test Note\n\nConverted to note format."
updated = await knowledge_service.update_entity(
entity.path_id,
entity_type=EntityType.NOTE,
content=new_content
entity.path_id, entity_type="note", content=new_content
)
# Verify type was updated in DB
assert updated.entity_type == EntityType.NOTE
assert updated.entity_type == "note"
# Verify frontmatter was updated
file_path = knowledge_service.get_entity_path(updated)
@@ -286,7 +274,7 @@ async def test_update_entity_type_knowledge_to_note(knowledge_service: Knowledge
_, frontmatter, _ = content.split("---", 2)
metadata = yaml.safe_load(frontmatter)
assert metadata["type"] == EntityType.NOTE
assert metadata["type"] == "note"
# Verify content changed to note style (direct content)
assert "# Test Note" in content
@@ -294,4 +282,4 @@ async def test_update_entity_type_knowledge_to_note(knowledge_service: Knowledge
assert "Test observation" not in content # Observations not included in note format
# Verify metadata was preserved
assert metadata["status"] == "draft"
assert metadata["status"] == "draft"
+6 -7
View File
@@ -5,12 +5,9 @@ import pytest_asyncio
from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker
from basic_memory.models import Entity, Relation
from basic_memory.models.knowledge import EntityType
from basic_memory.repository.relation_repository import RelationRepository
from basic_memory.services.relation_service import RelationService
@pytest_asyncio.fixture
async def test_entities(
session_maker: async_sessionmaker[AsyncSession],
@@ -19,17 +16,19 @@ async def test_entities(
async with session_maker() as session:
entity1 = Entity(
name="test_entity_1",
entity_type=EntityType.KNOWLEDGE,
entity_type="test",
path_id="test/test_entity_1",
file_path="test/test_entity_1.md",
description="Test entity 1",
summary="Test entity 1",
content_type="text/markdown",
)
entity2 = Entity(
name="test_entity_2",
entity_type=EntityType.KNOWLEDGE,
entity_type="test",
path_id="test/test_entity_2",
file_path="test/test_entity_2.md",
description="Test entity 2",
summary="Test entity 2",
content_type="text/markdown",
)
session.add_all([entity1, entity2])
await session.commit()
+3 -2
View File
@@ -19,7 +19,8 @@ def test_entity():
entity_metadata = { "test": "test"}
path_id = "component/test_component"
file_path = "entities/component/test_component.md"
description = "A test component for search"
summary = "A test component for search"
content_type = "text/markdown"
created_at = datetime.now(timezone.utc)
updated_at = datetime.now(timezone.utc)
observations = []
@@ -97,7 +98,7 @@ async def test_update_index(search_service, test_entity):
await search_service.index_entity(test_entity)
# Update entity
test_entity.description = "Updated description with new terms"
test_entity.summary = "Updated description with new terms"
await search_service.index_entity(test_entity)
# Search for new terms
+11 -8
View File
@@ -14,7 +14,6 @@ from basic_memory.markdown.schemas import (
Relation as MarkdownRelation,
)
from basic_memory.models import Entity as EntityModel
from basic_memory.models.knowledge import EntityType
from basic_memory.sync.knowledge_sync_service import KnowledgeSyncService
@@ -35,7 +34,7 @@ def test_content() -> EntityContent:
"""Create test content with observations and relations."""
return EntityContent(
title="Test Entity",
description="A test entity description",
summary="A test entity description",
observations=[
MarkdownObservation(content="First observation"),
MarkdownObservation(content="Second observation"),
@@ -65,7 +64,7 @@ async def test_create_entity_without_relations(
# Check basic fields
assert entity.name == "Test Entity"
assert entity.entity_type == EntityType.KNOWLEDGE
assert entity.entity_type == "knowledge"
assert entity.path_id == "concept/test_entity"
assert entity.summary == "A test entity description"
@@ -91,7 +90,7 @@ async def test_update_entity_without_relations(
# Modify markdown content
test_markdown.content.title = "Updated Title"
test_markdown.content.description = "Updated description"
test_markdown.content.summary = "Updated description"
test_markdown.content.observations = [MarkdownObservation(content="Updated observation")]
# Update entity
@@ -120,15 +119,17 @@ async def test_update_entity_relations(
# Create target entities that relations point to
other_entity = EntityModel(
name="Other Entity",
entity_type=EntityType.KNOWLEDGE,
entity_type="test",
path_id="concept/other_entity",
file_path="concept/other_entity.md",
content_type="text/markdown",
)
another_entity = EntityModel(
name="Another Entity",
entity_type=EntityType.KNOWLEDGE,
entity_type="test",
path_id="concept/another_entity",
file_path="concept/another_entity.md",
content_type="text/markdown",
)
await knowledge_sync_service.entity_service.add(other_entity)
await knowledge_sync_service.entity_service.add(another_entity)
@@ -163,15 +164,17 @@ async def test_two_pass_sync_flow(
# Create target entities first
other_entity = EntityModel(
name="Other Entity",
entity_type=EntityType.KNOWLEDGE,
entity_type="test",
path_id="concept/other_entity",
file_path="concept/other_entity.md",
content_type="text/markdown",
)
another_entity = EntityModel(
name="Another Entity",
entity_type=EntityType.KNOWLEDGE,
entity_type="test",
path_id="concept/another_entity",
file_path="concept/another_entity.md",
content_type="text/markdown",
)
await knowledge_sync_service.entity_service.add(other_entity)
await knowledge_sync_service.entity_service.add(another_entity)
+3 -3
View File
@@ -6,7 +6,6 @@ import pytest
from basic_memory.config import ProjectConfig
from basic_memory.models import Entity
from basic_memory.models.knowledge import EntityType
from basic_memory.services import EntityService
from basic_memory.sync.sync_service import SyncService
@@ -49,9 +48,10 @@ A test concept.
other = Entity(
path_id="concept/other",
name="Other",
entity_type=EntityType.KNOWLEDGE,
entity_type="test",
file_path="concept/other.md",
checksum="12345678",
content_type="text/markdown",
)
await entity_service.repository.add(other)
@@ -64,7 +64,7 @@ A test concept.
# Find new entity
test_concept = next(e for e in entities if e.path_id == "concept/test_concept")
assert test_concept.entity_type == EntityType.KNOWLEDGE
assert test_concept.entity_type == "knowledge"
# Verify relation was not created
# because file for related entity was not found
+7 -7
View File
@@ -7,7 +7,6 @@ import pytest
from basic_memory.config import ProjectConfig
from basic_memory.models import Entity
from basic_memory.models.knowledge import EntityType
from basic_memory.services import EntityService
from basic_memory.sync.sync_service import SyncService
@@ -34,7 +33,9 @@ async def test_sync_file_modified_during_sync(
"""Test handling of files that change during sync process."""
# Create initial files
doc_path = test_config.knowledge_dir / "changing.md"
await create_test_file(doc_path, """
await create_test_file(
doc_path,
"""
---
type: knowledge
id: changing
@@ -45,7 +46,8 @@ modified: 2024-01-01
## Observations
- This is a test
""")
""",
)
# Setup async modification during sync
async def modify_file():
@@ -71,9 +73,10 @@ async def test_sync_null_checksum_cleanup(
entity = Entity(
path_id="concept/incomplete",
name="Incomplete",
entity_type=EntityType.KNOWLEDGE,
entity_type="test",
file_path="concept/incomplete.md",
checksum=None, # Null checksum
content_type="text/markdown",
)
await entity_service.repository.add(entity)
@@ -98,6 +101,3 @@ modified: 2024-01-01
# Verify entity was properly synced
updated = await entity_service.get_by_path_id("concept/incomplete")
assert updated.checksum is not None