get tests working

This commit is contained in:
phernandez
2025-01-06 21:08:19 -06:00
parent 55c40bd6d3
commit c320f64b53
22 changed files with 223 additions and 669 deletions
@@ -11,12 +11,6 @@ from basic_memory.schemas import EntityTypeList, ObservationCategoryList, TypedE
router = APIRouter(prefix="/discovery", tags=["discovery"])
@router.get("/entity-types", response_model=EntityTypeList)
async def get_entity_types(entity_service: EntityServiceDep) -> EntityTypeList:
"""Get list of all unique entity types in the system."""
logger.debug("Getting all entity types")
types = await entity_service.get_entity_types()
return EntityTypeList(types=types)
@router.get("/observation-categories", response_model=ObservationCategoryList)
@@ -26,25 +20,3 @@ async def get_observation_categories(observation_service: ObservationServiceDep)
categories = await observation_service.observation_categories()
return ObservationCategoryList(categories=categories)
@router.get("/entities/{entity_type}", response_model=TypedEntityList)
async def list_entities_by_type(
entity_service: EntityServiceDep,
entity_type: str,
include_related: bool = False,
sort_by: Optional[str] = "updated_at",
) -> TypedEntityList:
"""List all entities of a specific type."""
logger.debug(f"Listing entities of type: {entity_type}")
entities = await entity_service.list_entities(
entity_type=entity_type,
sort_by=sort_by,
include_related=include_related
)
return TypedEntityList(
entity_type=entity_type,
entities=[EntityResponse.model_validate(e) for e in entities],
total=len(entities),
sort_by=sort_by,
include_related=include_related
)
+2
View File
@@ -210,6 +210,7 @@ async def get_knowledge_service(
relation_service: RelationServiceDep,
file_service: FileServiceDep,
knowledge_writer: KnowledgeWriterDep,
note_writer: NoteWriterDep,
project_config: ProjectConfigDep,
) -> KnowledgeService:
"""Create KnowledgeService with dependencies."""
@@ -219,6 +220,7 @@ async def get_knowledge_service(
relation_service=relation_service,
file_service=file_service,
knowledge_writer=knowledge_writer,
note_writer=note_writer,
base_path=project_config.knowledge_dir,
)
-2
View File
@@ -37,7 +37,6 @@ from basic_memory.mcp.tools.documents import (
)
from basic_memory.mcp.tools.discovery import (
get_entity_types,
get_observation_categories,
)
@@ -68,7 +67,6 @@ __all__ = [
"delete_document",
# Discovery tools
"get_entity_types",
"get_observation_categories",
# Activity tools
-217
View File
@@ -9,95 +9,6 @@ from basic_memory.schemas import EntityTypeList, ObservationCategoryList, TypedE
from basic_memory.mcp.async_client import client
@mcp.tool(
category="discovery",
description="List all unique entity types in the knowledge graph",
examples=[
{
"name": "Type Analysis",
"description": "Analyze entity type distribution and patterns",
"code": """
# Get all entity types
types = await get_entity_types()
# Analyze distribution by type
type_stats = {}
for entity_type in types["types"]:
entities = await list_by_type(
entity_type=entity_type,
include_related=True
)
type_stats[entity_type] = {
"count": len(entities.entities),
"with_observations": sum(1 for e in entities.entities
if e.observations),
"with_relations": sum(1 for e in entities.entities
if e.relations)
}
# Show type analysis
print("Knowledge Graph Structure:")
for type_, stats in type_stats.items():
print(f"\\n{type_}:")
print(f"- Total: {stats['count']} entities")
print(f"- With observations: {stats['with_observations']}")
print(f"- With relations: {stats['with_relations']}")"""
},
{
"name": "Custom Type Detection",
"description": "Identify and analyze custom entity types",
"code": """
# Get all types
types = await get_entity_types()
# Separate system and custom types
system_types = {
"component", "document", "feature",
"test", "concept"
}
custom_types = [t for t in types["types"]
if t not in system_types]
if custom_types:
print("Custom entity types discovered:")
for type_ in custom_types:
# Get entities of this type
entities = await list_by_type(type_)
print(f"\\n{type_} ({len(entities.entities)} entities):")
# Analyze type characteristics
observations = [o for e in entities.entities
for o in e.observations]
categories = {o.category for o in observations}
relations = [r for e in entities.entities
for r in e.relations]
relation_types = {r.relation_type for r in relations}
if categories:
print("Used categories:")
for cat in sorted(categories):
print(f"- {cat}")
if relation_types:
print("\\nRelation types:")
for rt in sorted(relation_types):
print(f"- {rt}")"""
}
],
output_model=EntityTypeList
)
async def get_entity_types() -> List[str]:
"""List all unique entity types in use.
Returns:
List of unique entity type names used in the knowledge graph
"""
logger.debug("Getting all entity types")
url = "/discovery/entity-types"
response = await client.get(url)
return EntityTypeList.model_validate(response.json())
@mcp.tool(
category="discovery",
@@ -180,131 +91,3 @@ async def get_observation_categories() -> List[str]:
response = await client.get(url)
return ObservationCategoryList.model_validate(response.json())
@mcp.tool(
category="discovery",
description="List all entities of a specific type",
examples=[
{
"name": "Component Analysis",
"description": "Analyze component implementation patterns",
"code": """
# Get all components with relations
components = await list_by_type(
entity_type="component",
include_related=True
)
# Analyze implementation patterns
dependency_patterns = defaultdict(list)
implementation_patterns = defaultdict(list)
for entity in components.entities:
# Analyze dependencies
deps = [r for r in entity.relations
if r.relation_type == "depends_on"]
if deps:
pattern = f"{len(deps)} dependencies"
dependency_patterns[pattern].append(entity.name)
# Analyze implementation details
tech_obs = [o for o in entity.observations
if o.category == "tech"]
if tech_obs:
pattern = f"{len(tech_obs)} technical notes"
implementation_patterns[pattern].append(entity.name)
print("Component Implementation Patterns:\\n")
print("Dependency Patterns:")
for pattern, components in dependency_patterns.items():
print(f"\\n{pattern}:")
for comp in components:
print(f"- {comp}")
print("\\nImplementation Detail Patterns:")
for pattern, components in implementation_patterns.items():
print(f"\\n{pattern}:")
for comp in components:
print(f"- {comp}")"""
},
{
"name": "Feature Coverage",
"description": "Analyze feature implementation status",
"code": """
# Get all features
features = await list_by_type(
entity_type="feature",
include_related=True,
sort_by="updated_at"
)
def analyze_feature_status(feature):
# Check implementation
has_component = any(
r.relation_type == "implemented_by"
for r in feature.relations
)
# Check testing
has_tests = any(
r.to_id.startswith("test/")
for r in feature.relations
)
# Check documentation
has_docs = any(
r.to_id.startswith("document/")
for r in feature.relations
)
# Get latest status note
status_notes = [o for o in feature.observations
if o.category == "note"]
latest_status = status_notes[-1].content if status_notes else None
return {
"implemented": has_component,
"tested": has_tests,
"documented": has_docs,
"status": latest_status
}
# Show feature coverage
print("Feature Implementation Status:\\n")
for feature in features.entities:
status = analyze_feature_status(feature)
print(f"{feature.name}:")
print(f"- Implementation: {'' if status['implemented'] else ''}")
print(f"- Tests: {'' if status['tested'] else ''}")
print(f"- Documentation: {'' if status['documented'] else ''}")
if status['status']:
print(f"- Status: {status['status']}")
print()"""
}
],
output_model=TypedEntityList,
)
async def list_by_type(
entity_type: str,
include_related: bool = False,
sort_by: Optional[str] = "updated_at"
) -> TypedEntityList:
"""List all entities of a specific type.
Args:
entity_type: Type of entities to retrieve
include_related: Whether to include related entities
sort_by: Field to sort results by
Returns:
TypedEntityList containing matching entities and metadata
"""
logger.debug(f"Listing entities of type: {entity_type}")
params = {"include_related": "true" if include_related else "false"}
if sort_by:
params["sort_by"] = sort_by
url = f"/discovery/entities/{entity_type}"
response = await client.get(url, params=params)
return TypedEntityList.model_validate(response.json())
+5 -71
View File
@@ -4,7 +4,7 @@ import pytest
import pytest_asyncio
from httpx import AsyncClient
from basic_memory.models.knowledge import Entity, Observation
from basic_memory.models.knowledge import Entity, Observation, EntityType
from basic_memory.repository.entity_repository import EntityRepository
from basic_memory.schemas import EntityTypeList, ObservationCategoryList, TypedEntityList
@@ -18,7 +18,7 @@ async def test_entities(entity_repository: EntityRepository) -> list[Entity]:
entities = [
Entity(
name="Memory Service",
entity_type="technical_component",
entity_type=EntityType.KNOWLEDGE,
description="Core memory service",
path_id="component/memory_service",
file_path="component/memory_service.md",
@@ -29,7 +29,7 @@ async def test_entities(entity_repository: EntityRepository) -> list[Entity]:
),
Entity(
name="File Format",
entity_type="specification",
entity_type=EntityType.KNOWLEDGE,
description="File format spec",
path_id="spec/file_format",
file_path="spec/file_format.md",
@@ -40,7 +40,7 @@ async def test_entities(entity_repository: EntityRepository) -> list[Entity]:
),
Entity(
name="Technical Decision",
entity_type="decision",
entity_type=EntityType.KNOWLEDGE,
description="Architecture decision",
path_id="decision/tech_choice",
file_path="decision/tech_choice.md",
@@ -52,7 +52,7 @@ async def test_entities(entity_repository: EntityRepository) -> list[Entity]:
# Add another technical component for sorting tests
Entity(
name="API Service",
entity_type="technical_component",
entity_type=EntityType.KNOWLEDGE,
description="API layer",
path_id="component/api_service",
file_path="component/api_service.md",
@@ -66,27 +66,6 @@ async def test_entities(entity_repository: EntityRepository) -> list[Entity]:
return created
async def test_get_entity_types(client: AsyncClient, test_entities):
"""Test getting list of entity types."""
# 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 "technical_component" in data.types
assert "specification" in data.types
assert "decision" 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))
async def test_get_observation_categories(client: AsyncClient, test_entities):
@@ -113,49 +92,4 @@ async def test_get_observation_categories(client: AsyncClient, test_entities):
assert len(data.categories) == len(set(data.categories))
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/technical_component")
assert response.status_code == 200
# Parse response
data = TypedEntityList.model_validate(response.json())
# Check response structure
assert data.entity_type == "technical_component"
assert len(data.entities) == 2
assert data.total == 2
# Verify content
names = {e.name for e in data.entities}
assert "Memory Service" in names
assert "API Service" in names
async def test_list_entities_with_sorting(client: AsyncClient, test_entities):
"""Test listing entities with different sort options."""
# Sort by name
response = await client.get("/discovery/entities/technical_component?sort_by=name")
assert response.status_code == 200
data = TypedEntityList.model_validate(response.json())
names = [e.name for e in data.entities]
assert names == sorted(names) # Should be alphabetical
# Sort by path_id
response = await client.get("/discovery/entities/technical_component?sort_by=path_id")
assert response.status_code == 200
data = TypedEntityList.model_validate(response.json())
path_ids = [e.path_id for e in data.entities]
assert path_ids == sorted(path_ids)
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
assert data.total == 0
+59 -94
View File
@@ -10,7 +10,7 @@ from basic_memory.schemas import (
EntityResponse,
EntityListResponse,
ObservationResponse,
RelationResponse,
RelationResponse, EntityType,
)
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": "test",
"entity_type": EntityType.KNOWLEDGE,
"observations": ["First observation", "Second observation"],
}
# Create an entity
@@ -56,19 +56,20 @@ async def add_observations(client, path_id: str) -> List[ObservationResponse]:
data = response.json()
obs_response = EntityResponse.model_validate(data)
assert len(obs_response.observations) == 2
return obs_response.observations
async def create_related_entities(client) -> List[RelationResponse]: # pyright: ignore [reportReturnType]
# Create two entities to relate
entities = [
{"name": "SourceEntity", "entity_type": "test"},
{"name": "TargetEntity", "entity_type": "test"},
{"name": "SourceEntity", "entity_type": EntityType.KNOWLEDGE},
{"name": "TargetEntity", "entity_type": EntityType.KNOWLEDGE},
]
create_response = await client.post("/knowledge/entities", json={"entities": entities})
created = create_response.json()["entities"]
source_path_id = "test/source_entity"
target_path_id = "test/target_entity"
source_path_id = "source_entity"
target_path_id = "target_entity"
# Create relation between them
response = await client.post(
@@ -113,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": "test"}
data = {"name": "TestEntity", "entity_type": EntityType.KNOWLEDGE}
response = await client.post("/knowledge/entities", json={"entities": [data]})
assert response.status_code == 200
data = response.json()
@@ -126,8 +127,8 @@ async def test_get_entity(client: AsyncClient):
assert response.status_code == 200
entity = response.json()
assert entity["name"] == "TestEntity"
assert entity["entity_type"] == "test"
assert entity["path_id"] == "test/test_entity"
assert entity["entity_type"] == EntityType.KNOWLEDGE
assert entity["path_id"] == "test_entity"
@pytest.mark.asyncio
@@ -140,10 +141,10 @@ 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": "test"}
await client.post("/knowledge/entities", json={"entities": [data]})
data = {"name": "TestEntity", "entity_type": EntityType.KNOWLEDGE}
response = await client.post("/knowledge/entities", json={"entities": [data]})
path_id = "test/test_entity"
path_id = "test_entity"
# Add observations
await add_observations(client, path_id)
@@ -153,44 +154,21 @@ async def test_add_observations(client: AsyncClient):
assert len(entity["observations"]) == 2
@pytest.mark.asyncio
async def test_search_nodes(client: AsyncClient):
"""Should search for entities in the knowledge graph."""
# Create a few entities with different names
entities = [
{"name": "NotFound", "entity_type": "negative"},
{"name": "AlphaTest", "entity_type": "test"},
{"name": "BetaTest", "entity_type": "test"},
{"name": "GammaProduction", "entity_type": "test"}, # match entity_type
]
await client.post("/knowledge/entities", json={"entities": entities})
# Search for "Test" in names
response = await client.post("/knowledge/search", json={"query": "Test"})
# Verify search results
assert response.status_code == 200
data = response.json()
assert data["query"] == "Test"
assert len(data["matches"]) == 3
names = {entity["name"] for entity in data["matches"]}
assert names == {"AlphaTest", "BetaTest", "GammaProduction"}
@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": "test"},
{"name": "BetaTest", "entity_type": "test"},
{"name": "AlphaTest", "entity_type": EntityType.KNOWLEDGE},
{"name": "BetaTest", "entity_type": EntityType.KNOWLEDGE},
]
await client.post("/knowledge/entities", json={"entities": entities})
# Open nodes by path IDs
response = await client.post(
"/knowledge/nodes",
json={"path_ids": ["test/alpha_test"]},
json={"path_ids": ["alpha_test"]},
)
# Verify results
@@ -199,15 +177,15 @@ async def test_open_nodes(client: AsyncClient):
assert len(data["entities"]) == 1
entity = data["entities"][0]
assert entity["name"] == "AlphaTest"
assert entity["entity_type"] == "test"
assert entity["path_id"] == "test/alpha_test"
assert entity["entity_type"] == EntityType.KNOWLEDGE
assert entity["path_id"] == "alpha_test"
@pytest.mark.asyncio
async def test_delete_entity(client: AsyncClient):
"""Test DELETE /knowledge/entities with path ID."""
# Create test entity
entity_data = {"name": "TestEntity", "entity_type": "test"}
entity_data = {"name": "TestEntity", "entity_type": EntityType.KNOWLEDGE}
await client.post("/knowledge/entities", json={"entities": [entity_data]})
# Test deletion
@@ -228,21 +206,21 @@ async def test_delete_entity_bulk(client: AsyncClient):
"""Test bulk entity deletion using path IDs."""
# Create test entities
entities = [
{"name": "Entity1", "entity_type": "test"},
{"name": "Entity2", "entity_type": "test"},
{"name": "Entity1", "entity_type": EntityType.KNOWLEDGE},
{"name": "Entity2", "entity_type": EntityType.KNOWLEDGE},
]
await client.post("/knowledge/entities", json={"entities": entities})
# Test deletion
response = await client.post(
"/knowledge/entities/delete", json={"path_ids": ["test/Entity1", "test/Entity2"]}
"/knowledge/entities/delete", json={"path_ids": ["Entity1", "Entity2"]}
)
assert response.status_code == 200
assert response.json() == {"deleted": True}
# Verify entities are gone
for name in ["Entity1", "Entity2"]:
path_id = quote(f"test/{name}")
path_id = quote(f"{name}")
response = await client.get(f"/knowledge/entities/{path_id}")
assert response.status_code == 404
@@ -251,13 +229,13 @@ 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": "test"}
entity_data = {"name": "TestEntity", "entity_type": EntityType.KNOWLEDGE}
await client.post("/knowledge/entities", json={"entities": [entity_data]})
await add_observations(client, "test/TestEntity")
await add_observations(client, "TestEntity")
# Delete entity
response = await client.post(
"/knowledge/entities/delete", json={"path_ids": ["test/TestEntity"]}
"/knowledge/entities/delete", json={"path_ids": ["TestEntity"]}
)
assert response.status_code == 200
assert response.json() == {"deleted": True}
@@ -271,12 +249,12 @@ 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": "test"}
entity_data = {"name": "TestEntity", "entity_type": EntityType.KNOWLEDGE}
await client.post("/knowledge/entities", json={"entities": [entity_data]})
observations = await add_observations(client, "test/TestEntity") # adds 2
observations = await add_observations(client, "TestEntity") # adds 2
# Delete specific observations
request_data = {"path_id": "test/TestEntity", "observations": [observations[0].content]}
request_data = {"path_id": "TestEntity", "observations": [observations[0].content]}
response = await client.post("/knowledge/observations/delete", json=request_data)
assert response.status_code == 200
data = response.json()
@@ -316,7 +294,7 @@ async def test_delete_relations(client, relation_repository):
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": ["test/non_existent"]}
"/knowledge/entities/delete", json={"path_ids": ["non_existent"]}
)
assert response.status_code == 200
assert response.json() == {"deleted": True}
@@ -326,10 +304,10 @@ 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": "test"}
entity_data = {"name": "TestEntity", "entity_type": EntityType.KNOWLEDGE}
await client.post("/knowledge/entities", json={"entities": [entity_data]})
request_data = {"path_id": "test/TestEntity", "observations": ["Nonexistent observation"]}
request_data = {"path_id": "TestEntity", "observations": ["Nonexistent observation"]}
response = await client.post("/knowledge/observations/delete", json=request_data)
assert response.status_code == 200
@@ -344,8 +322,8 @@ async def test_delete_nonexistent_relations(client: AsyncClient):
request_data = {
"relations": [
{
"from_id": "test/non_existent1",
"to_id": "test/non_existent2",
"from_id": "non_existent1",
"to_id": "non_existent2",
"relation_type": "nonexistent",
}
]
@@ -358,34 +336,21 @@ async def test_delete_nonexistent_relations(client: AsyncClient):
assert del_response.entities == []
# @pytest.mark.asyncio
# async def test_invalid_path_id_format(client: AsyncClient):
# """Test handling of invalid path ID formats."""
# invalid_path_ids = [
# "/missing_type/name",
# "type//extra_separator",
# "",
# ]
# for invalid_id in invalid_path_ids:
# path_id = quote(invalid_id)
# response = await client.get(f"/knowledge/entities/{path_id}")
# assert response.status_code == 404
@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": "test"},
{"name": "NonEntity", "entity_type": "n_a"},
{"name": "MainEntity", "entity_type": EntityType.KNOWLEDGE},
{"name": "NonEntity", "entity_type": EntityType.KNOWLEDGE},
]
await client.post("/knowledge/entities", json={"entities": main_entities})
# 2. Create related entities
related_entities = [
{"name": "RelatedOne", "entity_type": "test"},
{"name": "RelatedTwo", "entity_type": "test"},
{"name": "RelatedOne", "entity_type": EntityType.KNOWLEDGE},
{"name": "RelatedTwo", "entity_type": EntityType.KNOWLEDGE},
]
await client.post("/knowledge/entities", json={"entities": related_entities})
@@ -395,13 +360,13 @@ async def test_full_knowledge_flow(client: AsyncClient):
json={
"relations": [
{
"from_id": "test/main_entity",
"to_id": "test/related_one",
"from_id": "main_entity",
"to_id": "related_one",
"relation_type": "connects_to",
},
{
"from_id": "test/main_entity",
"to_id": "test/related_two",
"from_id": "main_entity",
"to_id": "related_two",
"relation_type": "connects_to",
},
]
@@ -413,7 +378,7 @@ async def test_full_knowledge_flow(client: AsyncClient):
await client.post(
"/knowledge/observations",
json={
"path_id": "test/main_entity",
"path_id": "main_entity",
"observations": [
{"content": "Connected to first related entity", "category": "tech"},
{"content": "Connected to second related entity", "category": "note"},
@@ -423,7 +388,7 @@ async def test_full_knowledge_flow(client: AsyncClient):
)
# 5. Verify full graph structure
path_id = quote("test/MainEntity")
path_id = "MainEntity"
main_get = await client.get(f"/knowledge/entities/{path_id}")
main_entity = main_get.json()
@@ -433,21 +398,21 @@ async def test_full_knowledge_flow(client: AsyncClient):
assert len(main_entity["relations"]) == 2
# 6. Search should find all related entities
search = await client.post("/knowledge/search", json={"query": "Related"})
matches = search.json()["matches"]
search = await client.post("/search/", json={"text": "Related"})
matches = search.json()["results"]
assert (
len(matches) == 3
) # Should find both related entities, and the main one with the observation
len(matches) == 1
)
# 7. Delete main entity
response = await client.post(
"/knowledge/entities/delete", json={"path_ids": ["test/MainEntity", "test/NonEntity"]}
"/knowledge/entities/delete", json={"path_ids": ["MainEntity", "NonEntity"]}
)
assert response.status_code == 200
assert response.json() == {"deleted": True}
# Verify deletion
path_id = quote("test/MainEntity")
path_id = "MainEntity"
response = await client.get(f"/knowledge/entities/{path_id}")
assert response.status_code == 404
@@ -457,7 +422,7 @@ async def test_entity_indexing(client: AsyncClient):
"""Test entity creation includes search indexing."""
data = {
"name": "SearchTest",
"entity_type": "test",
"entity_type": EntityType.KNOWLEDGE,
"observations": ["Unique searchable observation"],
}
@@ -472,7 +437,7 @@ async def test_entity_indexing(client: AsyncClient):
assert search_response.status_code == 200
search_result = SearchResponse.model_validate(search_response.json())
assert len(search_result.results) == 1
assert search_result.results[0].path_id == "test/search_test"
assert search_result.results[0].path_id == "search_test"
assert search_result.results[0].type == SearchItemType.ENTITY.value
@@ -482,7 +447,7 @@ async def test_observation_update_indexing(client: AsyncClient):
# Create entity
data = {
"name": "TestEntity",
"entity_type": "test",
"entity_type": EntityType.KNOWLEDGE,
"observations": ["Initial observation"],
}
response = await client.post("/knowledge/entities", json={"entities": [data]})
@@ -512,7 +477,7 @@ async def test_entity_delete_indexing(client: AsyncClient):
"""Test deleted entities are removed from search index."""
data = {
"name": "DeleteTest",
"entity_type": "test",
"entity_type": EntityType.KNOWLEDGE,
"observations": ["Searchable observation that should be removed"],
}
@@ -547,8 +512,8 @@ async def test_relation_indexing(client: AsyncClient):
"""Test relations are included in search index."""
# Create entities
entities = [
{"name": "SourceTest", "entity_type": "test"},
{"name": "TargetTest", "entity_type": "test"},
{"name": "SourceTest", "entity_type": EntityType.KNOWLEDGE},
{"name": "TargetTest", "entity_type": EntityType.KNOWLEDGE},
]
create_response = await client.post("/knowledge/entities", json={"entities": entities})
assert create_response.status_code == 200
@@ -559,8 +524,8 @@ async def test_relation_indexing(client: AsyncClient):
json={
"relations": [
{
"from_id": "test/source_test",
"to_id": "test/target_test",
"from_id": "source_test",
"to_id": "target_test",
"relation_type": "sphinx_relation",
"context": "Unique sphinx relation context",
}
@@ -576,4 +541,4 @@ async def test_relation_indexing(client: AsyncClient):
search_result = SearchResponse.model_validate(search_response.json())
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 == {"test/source_test", "test/target_test"}
assert path_ids == {"source_test", "target_test"}
+7 -5
View File
@@ -6,6 +6,7 @@ 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
@@ -15,7 +16,8 @@ def test_entity():
class Entity:
id = 1
name = "TestComponent"
entity_type = "component"
entity_type = EntityType.KNOWLEDGE
entity_metadata = { "test": "test"}
path_id = "component/test_component"
file_path = "entities/component/test_component.md"
description = "A test component for search testing"
@@ -108,7 +110,7 @@ async def test_search_with_entity_type_filter(client, indexed_entity):
"/search/",
json={
"text": "test",
"entity_types": ["component"]
"entity_types": [EntityType.KNOWLEDGE]
}
)
assert response.status_code == 200
@@ -120,7 +122,7 @@ async def test_search_with_entity_type_filter(client, indexed_entity):
"/search/",
json={
"text": "test",
"entity_types": ["concept"]
"entity_types": [EntityType.NOTE]
}
)
assert response.status_code == 200
@@ -251,7 +253,7 @@ async def test_multiple_filters(client, indexed_entity):
json={
"text": "test",
"types": [SearchItemType.ENTITY.value],
"entity_types": ["component"],
"entity_types": [EntityType.KNOWLEDGE],
"after_date": datetime(2020, 1, 1, tzinfo=timezone.utc).isoformat()
}
)
@@ -261,4 +263,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"] == "component"
assert result.metadata["entity_type"] == EntityType.KNOWLEDGE
+5 -5
View File
@@ -5,7 +5,7 @@ import pytest
from basic_memory.mcp.tools.documents import create_document
from basic_memory.mcp.tools.knowledge import create_entities
from basic_memory.mcp.tools.activity import get_recent_activity
from basic_memory.schemas.base import Entity
from basic_memory.schemas.base import Entity, EntityType
from basic_memory.schemas.request import CreateEntityRequest, DocumentRequest
from basic_memory.schemas.activity import ActivityType
@@ -61,7 +61,7 @@ async def test_get_recent_activity_filtered(client):
)
entity_request = CreateEntityRequest(
entities=[Entity(name="TestEntity", entity_type="test")]
entities=[Entity(name="TestEntity", entity_type=EntityType.KNOWLEDGE)]
)
entity = await create_entities(entity_request)
@@ -94,7 +94,7 @@ async def test_get_recent_activity_content_handling(client):
entity_request = CreateEntityRequest(
entities=[Entity(
name="ContentEntity",
entity_type="test",
entity_type=EntityType.KNOWLEDGE,
description="Entity description"
)]
)
@@ -130,8 +130,8 @@ async def test_get_recent_activity_multiple_types(client):
entity_request = CreateEntityRequest(
entities=[
Entity(name="Entity1", entity_type="test"),
Entity(name="Entity2", entity_type="test")
Entity(name="Entity1", entity_type=EntityType.KNOWLEDGE),
Entity(name="Entity2", entity_type=EntityType.KNOWLEDGE)
]
)
entities = await create_entities(entity_request)
+6 -6
View File
@@ -3,7 +3,7 @@
import pytest
from basic_memory.mcp.tools.knowledge import create_entities, add_observations
from basic_memory.schemas.base import ObservationCategory, Entity
from basic_memory.schemas.base import ObservationCategory, Entity, EntityType
from basic_memory.schemas.request import CreateEntityRequest, AddObservationsRequest, ObservationCreate
@@ -12,7 +12,7 @@ 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="test")]
entities=[Entity(name="TestEntity", entity_type=EntityType.KNOWLEDGE)]
)
result = await create_entities(entity_request)
entity_id = result.entities[0].path_id
@@ -38,7 +38,7 @@ 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="test")]
entities=[Entity(name="TestEntity", entity_type=EntityType.KNOWLEDGE)]
)
result = await create_entities(entity_request)
entity_id = result.entities[0].path_id
@@ -80,7 +80,7 @@ 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="test")]
entities=[Entity(name="TestEntity", entity_type=EntityType.KNOWLEDGE)]
)
result = await create_entities(entity_request)
entity_id = result.entities[0].path_id
@@ -118,7 +118,7 @@ async def test_add_observations_preserves_existing(client):
entities=[
Entity(
name="TestEntity",
entity_type="test",
entity_type=EntityType.KNOWLEDGE,
observations=["Initial observation"]
)
]
@@ -150,7 +150,7 @@ 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="test")]
entities=[Entity(name="TestEntity", entity_type=EntityType.KNOWLEDGE)]
)
result = await create_entities(entity_request)
entity_id = result.entities[0].path_id
+11 -11
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
from basic_memory.schemas.base import ObservationCategory, Entity, EntityType
from basic_memory.schemas.request import CreateEntityRequest
@@ -14,7 +14,7 @@ async def test_create_basic_entity(client):
entities=[
Entity(
name="TestEntity",
entity_type="test",
entity_type=EntityType.KNOWLEDGE,
description="A test entity",
observations=["First observation"]
)
@@ -29,8 +29,8 @@ async def test_create_basic_entity(client):
# Check the created entity
entity = result.entities[0]
assert entity.name == "TestEntity"
assert entity.entity_type == "test"
assert entity.path_id == "test/test_entity"
assert entity.entity_type == EntityType.KNOWLEDGE
assert entity.path_id == "test_entity"
assert entity.description == "A test entity"
# Check observations
@@ -50,7 +50,7 @@ async def test_create_entity_with_multiple_observations(client):
entities=[
Entity(
name="TestEntity",
entity_type="test",
entity_type=EntityType.KNOWLEDGE,
description="A test entity",
observations=[
"First observation",
@@ -85,12 +85,12 @@ async def test_create_multiple_entities(client):
entities=[
Entity(
name="Entity1",
entity_type="test",
entity_type=EntityType.KNOWLEDGE,
observations=["Observation 1"]
),
Entity(
name="Entity2",
entity_type="test",
entity_type=EntityType.KNOWLEDGE,
observations=["Observation 2"]
)
]
@@ -115,7 +115,7 @@ async def test_create_entity_without_observations(client):
entities=[
Entity(
name="TestEntity",
entity_type="test",
entity_type=EntityType.KNOWLEDGE,
description="A test entity without observations"
)
]
@@ -135,7 +135,7 @@ async def test_create_minimal_entity(client):
entities=[
Entity(
name="MinimalEntity",
entity_type="test"
entity_type=EntityType.KNOWLEDGE
)
]
)
@@ -144,8 +144,8 @@ async def test_create_minimal_entity(client):
entity = result.entities[0]
assert entity.name == "MinimalEntity"
assert entity.entity_type == "test"
assert entity.path_id == "test/minimal_entity"
assert entity.entity_type == EntityType.KNOWLEDGE
assert entity.path_id == "minimal_entity"
assert entity.description is None
assert len(entity.observations) == 0
assert len(entity.relations) == 0
+42 -42
View File
@@ -3,7 +3,7 @@
import pytest
from basic_memory.mcp.tools.knowledge import create_entities, create_relations
from basic_memory.schemas.base import Relation, Entity
from basic_memory.schemas.base import Relation, Entity, EntityType
from basic_memory.schemas.request import CreateEntityRequest, CreateRelationsRequest
from basic_memory.services.exceptions import EntityNotFoundError
@@ -14,8 +14,8 @@ async def test_create_basic_relation(client):
# First create test entities
entity_request = CreateEntityRequest(
entities=[
Entity(name="SourceEntity", entity_type="test"),
Entity(name="TargetEntity", entity_type="test")
Entity(name="SourceEntity", entity_type=EntityType.KNOWLEDGE),
Entity(name="TargetEntity", entity_type=EntityType.KNOWLEDGE)
]
)
await create_entities(entity_request)
@@ -24,8 +24,8 @@ async def test_create_basic_relation(client):
relation_request = CreateRelationsRequest(
relations=[
Relation(
from_id="test/source_entity",
to_id="test/target_entity",
from_id="source_entity",
to_id="target_entity",
relation_type="depends_on"
)
]
@@ -35,8 +35,8 @@ async def test_create_basic_relation(client):
assert len(result.entities) == 2
# Find source and target entities
source = next(e for e in result.entities if e.path_id == "test/source_entity")
target = next(e for e in result.entities if e.path_id == "test/target_entity")
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
@@ -44,14 +44,14 @@ async def test_create_basic_relation(client):
# Source's relation shows it depends_on target
source_relation = source.relations[0]
assert source_relation.from_id == "test/source_entity"
assert source_relation.to_id == "test/target_entity"
assert source_relation.from_id == "source_entity"
assert source_relation.to_id == "target_entity"
assert source_relation.relation_type == "depends_on"
# Target's relation is the same, allowing backwards traversal
target_relation = target.relations[0]
assert target_relation.from_id == "test/source_entity"
assert target_relation.to_id == "test/target_entity"
assert target_relation.from_id == "source_entity"
assert target_relation.to_id == "target_entity"
assert target_relation.relation_type == "depends_on"
@@ -61,8 +61,8 @@ async def test_create_relation_with_context(client):
# Create test entities
entity_request = CreateEntityRequest(
entities=[
Entity(name="Source", entity_type="test"),
Entity(name="Target", entity_type="test")
Entity(name="Source", entity_type=EntityType.KNOWLEDGE),
Entity(name="Target", entity_type=EntityType.KNOWLEDGE)
]
)
await create_entities(entity_request)
@@ -70,8 +70,8 @@ async def test_create_relation_with_context(client):
relation_request = CreateRelationsRequest(
relations=[
Relation(
from_id="test/source",
to_id="test/target",
from_id="source",
to_id="target",
relation_type="implements",
context="Implementation details"
)
@@ -79,8 +79,8 @@ async def test_create_relation_with_context(client):
)
result = await create_relations(relation_request)
source = next(e for e in result.entities if e.path_id == "test/source")
target = next(e for e in result.entities if e.path_id == "test/target")
source = next(e for e in result.entities if e.path_id == "source")
target = next(e for e in result.entities if e.path_id == "target")
# Both entities should have the relation with context
assert len(source.relations) == 1
@@ -95,9 +95,9 @@ async def test_create_multiple_relations(client):
# Create test entities
entity_request = CreateEntityRequest(
entities=[
Entity(name="Entity1", entity_type="test"),
Entity(name="Entity2", entity_type="test"),
Entity(name="Entity3", entity_type="test")
Entity(name="Entity1", entity_type=EntityType.KNOWLEDGE),
Entity(name="Entity2", entity_type=EntityType.KNOWLEDGE),
Entity(name="Entity3", entity_type=EntityType.KNOWLEDGE)
]
)
await create_entities(entity_request)
@@ -105,13 +105,13 @@ async def test_create_multiple_relations(client):
relation_request = CreateRelationsRequest(
relations=[
Relation(
from_id="test/entity1",
to_id="test/entity2",
from_id="entity1",
to_id="entity2",
relation_type="connects_to"
),
Relation(
from_id="test/entity2",
to_id="test/entity3",
from_id="entity2",
to_id="entity3",
relation_type="depends_on"
)
]
@@ -122,9 +122,9 @@ async def test_create_multiple_relations(client):
assert len(result.entities) == 3
# Get entities
entity1 = next(e for e in result.entities if e.path_id == "test/entity1")
entity2 = next(e for e in result.entities if e.path_id == "test/entity2")
entity3 = next(e for e in result.entities if e.path_id == "test/entity3")
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
@@ -144,8 +144,8 @@ async def test_create_bidirectional_relations(client):
# Create test entities
entity_request = CreateEntityRequest(
entities=[
Entity(name="Service", entity_type="test"),
Entity(name="Database", entity_type="test")
Entity(name="Service", entity_type=EntityType.KNOWLEDGE),
Entity(name="Database", entity_type=EntityType.KNOWLEDGE)
]
)
await create_entities(entity_request)
@@ -154,21 +154,21 @@ async def test_create_bidirectional_relations(client):
relation_request = CreateRelationsRequest(
relations=[
Relation(
from_id="test/service",
to_id="test/database",
from_id="service",
to_id="database",
relation_type="depends_on"
),
Relation(
from_id="test/database",
to_id="test/service",
from_id="database",
to_id="service",
relation_type="supports"
)
]
)
result = await create_relations(relation_request)
service = next(e for e in result.entities if e.path_id == "test/service")
database = next(e for e in result.entities if e.path_id == "test/database")
service = next(e for e in result.entities if e.path_id == "service")
database = next(e for e in result.entities if e.path_id == "database")
# Each entity should have both relations for full navigation
assert len(service.relations) == 2
@@ -189,7 +189,7 @@ async def test_create_relation_with_invalid_entity(client):
# Create only one of the needed entities
entity_request = CreateEntityRequest(
entities=[
Entity(name="RealEntity", entity_type="test")
Entity(name="RealEntity", entity_type=EntityType.KNOWLEDGE)
]
)
await create_entities(entity_request)
@@ -197,8 +197,8 @@ async def test_create_relation_with_invalid_entity(client):
relation_request = CreateRelationsRequest(
relations=[
Relation(
from_id="test/real_entity",
to_id="test/non_existent_entity",
from_id="real_entity",
to_id="non_existent_entity",
relation_type="depends_on"
)
]
@@ -215,16 +215,16 @@ async def test_create_duplicate_relation(client):
# Create test entities
entity_request = CreateEntityRequest(
entities=[
Entity(name="Source", entity_type="test"),
Entity(name="Target", entity_type="test")
Entity(name="Source", entity_type=EntityType.KNOWLEDGE),
Entity(name="Target", entity_type=EntityType.KNOWLEDGE)
]
)
await create_entities(entity_request)
# Create relation
relation = Relation(
from_id="test/source",
to_id="test/target",
from_id="source",
to_id="target",
relation_type="connects_to"
)
relation_request = CreateRelationsRequest(relations=[relation])
+5 -84
View File
@@ -3,67 +3,13 @@
import pytest
from basic_memory.mcp.tools.discovery import (
get_entity_types,
get_observation_categories,
list_by_type,
)
from basic_memory.schemas import Entity, CreateEntityRequest, EntityTypeList, ObservationCategoryList
from basic_memory.schemas import Entity, CreateEntityRequest, ObservationCategoryList, EntityType
from basic_memory.mcp.tools.knowledge import create_entities, add_observations
from basic_memory.schemas.request import ObservationCreate, AddObservationsRequest
@pytest.mark.asyncio
async def test_get_entity_types(client):
"""Test getting list of entity types."""
# First create some test entities
request = CreateEntityRequest(
entities=[
Entity(
name="Memory Service",
entity_type="technical_component",
path_id="component/memory_service",
description="Core memory service",
observations=["Using SQLite for storage", "Local-first architecture"]
),
Entity(
name="File Format",
entity_type="specification",
path_id="specification/file_format",
description="File format spec",
observations=["Support for frontmatter", "UTF-8 encoding"]
),
Entity(
name="Tech Choice",
entity_type="decision",
path_id="decision/tech_choice",
description="Technology decision",
observations=["Team discussed options", "Selected for scalability"]
)
]
)
await create_entities(request)
# Get entity types
result = await get_entity_types()
entity_types = EntityTypeList.model_validate(result)
# Verify the result
assert "technical_component" in entity_types.types
assert "specification" in entity_types.types
assert "decision" in entity_types.types
@pytest.mark.asyncio
async def test_get_entity_types_empty(client):
"""Test getting entity types when no entities exist."""
# Get types (should work with empty DB)
result = await get_entity_types()
entity_types = EntityTypeList.model_validate(result)
# Should return empty list, not error
assert len(entity_types.types) == 0
@pytest.mark.asyncio
async def test_get_observation_categories(client):
"""Test getting list of observation categories."""
@@ -72,7 +18,7 @@ async def test_get_observation_categories(client):
entities=[
Entity(
name="Test Entity",
entity_type="test_observation",
entity_type=EntityType.KNOWLEDGE,
description="Test entity",
observations=[],
)
@@ -95,7 +41,7 @@ async def test_get_observation_categories(client):
# Get categories
result = await get_observation_categories()
observation_categories = ObservationCategoryList.model_validate(result)
# Verify results
assert "tech" in observation_categories.categories
assert "design" in observation_categories.categories
@@ -103,36 +49,11 @@ async def test_get_observation_categories(client):
assert "note" in observation_categories.categories
@pytest.mark.asyncio
async def test_list_by_type_with_sorting(client):
"""Test listing entities with different sort options."""
# Sort by name
result = await list_by_type("technical_component", sort_by="name")
names = [e.name for e in result.entities]
assert names == sorted(names)
# Sort by path_id
result = await list_by_type("technical_component", sort_by="path_id")
path_ids = [e.path_id for e in result.entities]
assert path_ids == sorted(path_ids)
@pytest.mark.asyncio
async def test_list_by_type_empty(client):
"""Test listing entities for a type that doesn't exist."""
result = await list_by_type("nonexistent_type")
assert result.entity_type == "nonexistent_type"
assert len(result.entities) == 0
assert result.total == 0
@pytest.mark.asyncio
async def test_get_observation_categories_empty(client):
"""Test getting observation categories when no observations exist."""
result = await get_observation_categories()
observation_categories = ObservationCategoryList.model_validate(result)
# Should return empty list, not error
observation_categories.categories == []
observation_categories.categories == []
+11 -11
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
from basic_memory.schemas.base import Entity, ObservationCategory, EntityType
from basic_memory.schemas.request import CreateEntityRequest
from basic_memory.services.exceptions import EntityNotFoundError
@@ -16,7 +16,7 @@ async def test_get_basic_entity(client):
entities=[
Entity(
name="TestEntity",
entity_type="test",
entity_type=EntityType.KNOWLEDGE,
description="A test entity",
observations=["First observation"]
)
@@ -30,8 +30,8 @@ async def test_get_basic_entity(client):
# Verify entity details
assert entity.name == "TestEntity"
assert entity.entity_type == "test"
assert entity.path_id == "test/test_entity"
assert entity.entity_type == EntityType.KNOWLEDGE
assert entity.path_id == "test_entity"
assert entity.description == "A test entity"
# Check observations
@@ -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="test"),
Entity(name="TargetEntity", entity_type="test")
Entity(name="SourceEntity", entity_type=EntityType.KNOWLEDGE),
Entity(name="TargetEntity", entity_type=EntityType.KNOWLEDGE)
]
)
await create_entities(entity_request)
@@ -61,8 +61,8 @@ async def test_get_entity_with_relations(client):
relation_request = CreateRelationsRequest(
relations=[
Relation(
from_id="test/source_entity",
to_id="test/target_entity",
from_id="source_entity",
to_id="target_entity",
relation_type="depends_on"
)
]
@@ -70,10 +70,10 @@ async def test_get_entity_with_relations(client):
await create_relations(relation_request)
# Get and verify source entity
source = await get_entity("test/source_entity")
source = await get_entity("source_entity")
assert len(source.relations) == 1
relation = source.relations[0]
assert relation.to_id == "test/target_entity"
assert relation.to_id == "target_entity"
assert relation.relation_type == "depends_on"
@@ -85,7 +85,7 @@ async def test_get_entity_with_categorized_observations(client):
entities=[
Entity(
name="TestEntity",
entity_type="test",
entity_type=EntityType.KNOWLEDGE,
description="Test entity with categories"
)
]
+10 -10
View File
@@ -5,7 +5,7 @@ import pytest
from basic_memory.mcp.tools.search import open_nodes
from basic_memory.mcp.tools.knowledge import create_entities
from basic_memory.schemas import EntityListResponse
from basic_memory.schemas.base import Entity
from basic_memory.schemas.base import Entity, EntityType
from basic_memory.schemas.request import CreateEntityRequest, OpenNodesRequest
@@ -17,12 +17,12 @@ async def test_open_multiple_entities(client):
entities=[
Entity(
name="Entity1",
entity_type="test",
entity_type=EntityType.KNOWLEDGE,
description="First test entity"
),
Entity(
name="Entity2",
entity_type="test",
entity_type=EntityType.KNOWLEDGE,
description="Second test entity"
)
]
@@ -52,7 +52,7 @@ async def test_open_nodes_with_details(client):
entities=[
Entity(
name="DetailedEntity",
entity_type="test",
entity_type=EntityType.KNOWLEDGE,
description="Test entity with details",
observations=["First observation", "Second observation"]
)
@@ -69,7 +69,7 @@ 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 == "test"
assert entity.entity_type == EntityType.KNOWLEDGE
assert entity.description == "Test entity with details"
assert len(entity.observations) == 2
@@ -82,12 +82,12 @@ async def test_open_nodes_with_relations(client):
entities=[
Entity(
name="Service",
entity_type="test",
entity_type=EntityType.KNOWLEDGE,
description="A service"
),
Entity(
name="Database",
entity_type="test",
entity_type=EntityType.KNOWLEDGE,
description="A database"
)
]
@@ -129,7 +129,7 @@ async def test_open_nonexistent_nodes(client):
entities=[
Entity(
name="RealEntity",
entity_type="test"
entity_type=EntityType.KNOWLEDGE
)
]
)
@@ -138,7 +138,7 @@ async def test_open_nonexistent_nodes(client):
# Try to open both real and non-existent
request = OpenNodesRequest(
path_ids=[real_path_id, "test/nonexistent"]
path_ids=[real_path_id, "nonexistent"]
)
result = await open_nodes(request)
response = EntityListResponse.model_validate(result)
@@ -156,7 +156,7 @@ async def test_open_single_node(client):
entities=[
Entity(
name="SingleEntity",
entity_type="test"
entity_type=EntityType.KNOWLEDGE
)
]
)
@@ -7,6 +7,7 @@ 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
@@ -89,7 +90,7 @@ 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="test",
entity_type=EntityType.KNOWLEDGE,
description="Test entity",
path_id="test/test_entity",
file_path="test/test_entity.md",
@@ -118,7 +119,7 @@ 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="test",
entity_type=EntityType.KNOWLEDGE,
description="Test entity",
path_id="test/test_entity",
file_path="test/test_entity.md",
@@ -146,7 +147,7 @@ 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="test",
entity_type=EntityType.KNOWLEDGE,
description="Test entity",
path_id="test/test_entity",
file_path="test/test_entity.md",
@@ -176,7 +177,7 @@ 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="test",
entity_type=EntityType.KNOWLEDGE,
description="Test entity",
path_id="test/test_entity",
file_path="test/test_entity.md",
@@ -217,7 +218,7 @@ 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="test",
entity_type=EntityType.KNOWLEDGE,
description="Test entity",
path_id="test/test_entity",
file_path="test/test_entity.md",
@@ -264,7 +265,7 @@ 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="test",
entity_type=EntityType.KNOWLEDGE,
description="Test entity",
path_id="test/test_entity",
file_path="test/test_entity.md",
+4 -3
View File
@@ -6,6 +6,7 @@ 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
@@ -14,7 +15,7 @@ async def source_entity(session_maker):
"""Create a source entity for testing relations."""
entity = Entity(
name="test_source",
entity_type="source",
entity_type=EntityType.KNOWLEDGE,
path_id="source/test_source",
file_path="source/test_source.md",
description="Source entity",
@@ -30,7 +31,7 @@ async def target_entity(session_maker):
"""Create a target entity for testing relations."""
entity = Entity(
name="test_target",
entity_type="target",
entity_type=EntityType.KNOWLEDGE,
path_id="target/test_target",
file_path="target/test_target.md",
description="Target entity",
@@ -59,7 +60,7 @@ async def related_entity(entity_repository):
"""Create a second entity for testing relations"""
entity_data = {
"name": "Related Entity",
"entity_type": "test",
"entity_type": EntityType.KNOWLEDGE,
"path_id": "test/related_entity",
"file_path": "test/related_entity.md",
"description": "A related test entity",
+18 -48
View File
@@ -16,10 +16,10 @@ from basic_memory.schemas.base import to_snake_case
def test_entity_in_minimal():
"""Test creating EntityIn with minimal required fields."""
data = {"name": "test_entity", "entity_type": "test"}
data = {"name": "test_entity", "entity_type": "knowledge"}
entity = Entity.model_validate(data)
assert entity.name == "test_entity"
assert entity.entity_type == "test"
assert entity.entity_type == "knowledge"
assert entity.description is None
assert entity.observations == []
@@ -28,13 +28,13 @@ def test_entity_in_complete():
"""Test creating EntityIn with all fields."""
data = {
"name": "test_entity",
"entity_type": "test",
"entity_type": "knowledge",
"description": "A test entity",
"observations": ["Test observation"],
}
entity = Entity.model_validate(data)
assert entity.name == "test_entity"
assert entity.entity_type == "test"
assert entity.entity_type == "knowledge"
assert entity.description == "A test entity"
assert len(entity.observations) == 1
assert entity.observations[0] == "Test observation"
@@ -84,8 +84,8 @@ def test_create_entities_input():
"""Test CreateEntitiesInput validation."""
data = {
"entities": [
{"name": "entity1", "entity_type": "test"},
{"name": "entity2", "entity_type": "test", "description": "test description"},
{"name": "entity1", "entity_type": "knowledge"},
{"name": "entity2", "entity_type": "knowledge", "description": "test description"},
]
}
create_input = CreateEntityRequest.model_validate(data)
@@ -103,7 +103,7 @@ def test_entity_out_from_attributes():
db_data = {
"path_id": "test/test",
"name": "test",
"entity_type": "test",
"entity_type": "knowledge",
"description": "test description",
"observations": [{"id": 1, "content": "test obs", "context": None}],
"relations": [
@@ -120,7 +120,7 @@ def test_entity_out_from_attributes():
def test_optional_fields():
"""Test handling of optional fields."""
# Create with no optional fields
entity = Entity.model_validate({"name": "test", "entity_type": "test"})
entity = Entity.model_validate({"name": "test", "entity_type": "knowledge"})
assert entity.description is None
assert entity.observations == []
@@ -128,7 +128,7 @@ def test_optional_fields():
entity = Entity.model_validate(
{
"name": "test",
"entity_type": "test",
"entity_type": "knowledge",
"description": None,
"observations": [],
}
@@ -138,7 +138,7 @@ def test_optional_fields():
# Create with some optional fields
entity = Entity.model_validate(
{"name": "test", "entity_type": "test", "description": "test", "observations": []}
{"name": "test", "entity_type": "knowledge", "description": "test", "observations": []}
)
assert entity.description == "test"
assert entity.observations == []
@@ -187,20 +187,20 @@ def test_path_id_generation():
"""Test path_id property generates correct paths."""
test_cases = [
(
{"name": "BasicMemory", "entity_type": "Project"},
"project/basic_memory"
{"name": "BasicMemory", "entity_type": "knowledge"},
"basic_memory"
),
(
{"name": "Memory Service", "entity_type": "Component"},
"component/memory_service"
{"name": "Memory Service", "entity_type": "knowledge"},
"memory_service"
),
(
{"name": "API Gateway", "entity_type": "Service"},
"service/api_gateway"
{"name": "API Gateway", "entity_type": "knowledge"},
"api_gateway"
),
(
{"name": "TestCase1", "entity_type": "Test"},
"test/test_case1"
{"name": "TestCase1", "entity_type": "knowledge"},
"test_case1"
),
]
@@ -208,33 +208,3 @@ def test_path_id_generation():
entity = Entity.model_validate(input_data)
assert entity.path_id == expected_path, f"Failed for input: {input_data}"
# def test_path_id_validation():
# """Test path ID format validation."""
# valid_paths = [
# "project/basic_memory",
# "test/test_case_1",
# "component/api_gateway",
# ]
#
# invalid_paths = [
# "no_separator", # Missing /
# "/missing_type", # Missing type
# "type/", # Missing name
# "type//double", # Double separator
# "../path/traversal", # Path traversal attempt
# "type/name/extra", # Too many parts
# "", # Empty string
# ]
#
# # Test valid paths
# for path in valid_paths:
# try:
# Relation.model_validate({"from_id": path, "to_id": path, "relation_type": "test"})
# except ValidationError as e:
# assert False, f"Valid path {path} failed validation: {e}"
#
# # Test invalid paths
# for path in invalid_paths:
# with pytest.raises(ValidationError):
# Relation.model_validate({"from_id": path, "to_id": "test/valid", "relation_type": "test"})
+3 -2
View File
@@ -5,6 +5,7 @@ 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
@@ -18,14 +19,14 @@ async def test_entities(
async with session_maker() as session:
entity1 = Entity(
name="test_entity_1",
entity_type="test",
entity_type=EntityType.KNOWLEDGE,
path_id="test/test_entity_1",
file_path="test/test_entity_1.md",
description="Test entity 1",
)
entity2 = Entity(
name="test_entity_2",
entity_type="test",
entity_type=EntityType.KNOWLEDGE,
path_id="test/test_entity_2",
file_path="test/test_entity_2.md",
description="Test entity 2",
+3 -2
View File
@@ -15,7 +15,8 @@ def test_entity():
class Entity:
id = 1
name = "TestComponent"
entity_type = "component"
entity_type = "knowledge"
entity_metadata = { "test": "test"}
path_id = "component/test_component"
file_path = "entities/component/test_component.md"
description = "A test component for search"
@@ -75,7 +76,7 @@ async def test_search_filtering(search_service, test_entity):
SearchQuery(
text="test",
types=[SearchItemType.ENTITY],
entity_types=["component"]
entity_types=["knowledge"]
)
)
assert len(results) == 1
+7 -6
View File
@@ -14,6 +14,7 @@ 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
@@ -21,7 +22,7 @@ from basic_memory.sync.knowledge_sync_service import KnowledgeSyncService
def test_frontmatter() -> EntityFrontmatter:
"""Create test frontmatter."""
return EntityFrontmatter(
type="concept",
type="knowledge",
id="concept/test_entity",
created=datetime.now(),
modified=datetime.now(),
@@ -64,7 +65,7 @@ async def test_create_entity_without_relations(
# Check basic fields
assert entity.name == "Test Entity"
assert entity.entity_type == "concept"
assert entity.entity_type == EntityType.KNOWLEDGE
assert entity.path_id == "concept/test_entity"
assert entity.description == "A test entity description"
@@ -119,13 +120,13 @@ async def test_update_entity_relations(
# Create target entities that relations point to
other_entity = EntityModel(
name="Other Entity",
entity_type="concept",
entity_type=EntityType.KNOWLEDGE,
path_id="concept/other_entity",
file_path="concept/other_entity.md",
)
another_entity = EntityModel(
name="Another Entity",
entity_type="concept",
entity_type=EntityType.KNOWLEDGE,
path_id="concept/another_entity",
file_path="concept/another_entity.md",
)
@@ -162,13 +163,13 @@ async def test_two_pass_sync_flow(
# Create target entities first
other_entity = EntityModel(
name="Other Entity",
entity_type="concept",
entity_type=EntityType.KNOWLEDGE,
path_id="concept/other_entity",
file_path="concept/other_entity.md",
)
another_entity = EntityModel(
name="Another Entity",
entity_type="concept",
entity_type=EntityType.KNOWLEDGE,
path_id="concept/another_entity",
file_path="concept/another_entity.md",
)
+13 -12
View File
@@ -6,6 +6,7 @@ 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
@@ -27,7 +28,7 @@ async def test_sync_knowledge(
# New entity with relation
new_content = """
---
type: concept
type: knowledge
id: concept/test_concept
created: 2023-01-01
modified: 2023-01-01
@@ -48,7 +49,7 @@ A test concept.
other = Entity(
path_id="concept/other",
name="Other",
entity_type="concept",
entity_type=EntityType.KNOWLEDGE,
file_path="concept/other.md",
checksum="12345678",
)
@@ -63,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 == "concept"
assert test_concept.entity_type == EntityType.KNOWLEDGE
# Verify relation was not created
# because file for related entity was not found
@@ -82,7 +83,7 @@ async def test_sync_entity_with_nonexistent_relations(
# Create entity that references entities we haven't created yet
content = """
---
type: concept
type: knowledge
id: concept/depends_on_future
created: 2024-01-01
modified: 2024-01-01
@@ -119,7 +120,7 @@ async def test_sync_entity_circular_relations(
# Create entity A that depends on B
content_a = """
---
type: concept
type: knowledge
id: concept/entity_a
created: 2024-01-01
modified: 2024-01-01
@@ -137,7 +138,7 @@ modified: 2024-01-01
# Create entity B that depends on A
content_b = """
---
type: concept
type: knowledge
id: concept/entity_b
created: 2024-01-01
modified: 2024-01-01
@@ -193,7 +194,7 @@ async def test_sync_entity_duplicate_relations(
# Create target entity first
target_content = """
---
type: concept
type: knowledge
id: concept/target
created: 2024-01-01
modified: 2024-01-01
@@ -209,7 +210,7 @@ modified: 2024-01-01
# Create entity with duplicate relations
content = """
---
type: concept
type: knowledge
id: concept/duplicate_relations
created: 2024-01-01
modified: 2024-01-01
@@ -254,7 +255,7 @@ async def test_sync_entity_with_invalid_category(
content = """
---
type: concept
type: knowledge
id: concept/invalid_category
created: 2024-01-01
modified: 2024-01-01
@@ -297,7 +298,7 @@ async def test_sync_entity_with_order_dependent_relations(
entities = {
"a": """
---
type: concept
type: knowledge
id: concept/entity_a
created: 2024-01-01
modified: 2024-01-01
@@ -314,7 +315,7 @@ modified: 2024-01-01
""",
"b": """
---
type: concept
type: knowledge
id: concept/entity_b
created: 2024-01-01
modified: 2024-01-01
@@ -329,7 +330,7 @@ modified: 2024-01-01
""",
"c": """
---
type: concept
type: knowledge
id: concept/entity_c
created: 2024-01-01
modified: 2024-01-01
+5 -4
View File
@@ -7,6 +7,7 @@ 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
@@ -60,7 +61,7 @@ async def test_sync_null_checksum_cleanup(
entity = Entity(
path_id="concept/incomplete",
name="Incomplete",
entity_type="concept",
entity_type=EntityType.KNOWLEDGE,
file_path="concept/incomplete.md",
checksum=None, # Null checksum
)
@@ -69,7 +70,7 @@ async def test_sync_null_checksum_cleanup(
# Create corresponding file
content = """
---
type: concept
type: knowledge
id: concept/incomplete
created: 2024-01-01
modified: 2024-01-01
@@ -99,7 +100,7 @@ async def test_sync_mixed_document_types(sync_service: SyncService, test_config:
# Create a knowledge file
knowledge_content = """
---
type: concept
type: knowledge
id: concept/test
created: 2024-01-01
modified: 2024-01-01
@@ -134,7 +135,7 @@ async def test_sync_performance_large_files(sync_service: SyncService, test_conf
observations = [f"- Observation {i}" for i in range(100)]
knowledge_content = f"""
---
type: concept
type: knowledge
id: concept/large
created: 2024-01-01
modified: 2024-01-01