Skip to main content
Glama

basic-memory

test_knowledge_router.py45.9 kB
"""Tests for knowledge graph API routes.""" from urllib.parse import quote import pytest from httpx import AsyncClient from basic_memory.schemas import ( Entity, EntityResponse, ) from basic_memory.schemas.search import SearchItemType, SearchResponse from basic_memory.utils import normalize_newlines @pytest.mark.asyncio async def test_create_entity(client: AsyncClient, file_service, project_url): """Should create entity successfully.""" data = { "title": "TestEntity", "folder": "test", "entity_type": "test", "content": "TestContent", "project": "Test Project Context", } # Create an entity print(f"Requesting with data: {data}") # Use the permalink version of the project name in the path response = await client.post(f"{project_url}/knowledge/entities", json=data) # Print response for debugging print(f"Response status: {response.status_code}") print(f"Response content: {response.text}") # Verify creation assert response.status_code == 200 entity = EntityResponse.model_validate(response.json()) assert entity.permalink == "test/test-entity" assert entity.file_path == "test/TestEntity.md" assert entity.entity_type == data["entity_type"] assert entity.content_type == "text/markdown" # Verify file has new content but preserved metadata file_path = file_service.get_entity_path(entity) file_content, _ = await file_service.read_file(file_path) assert data["content"] in file_content @pytest.mark.asyncio async def test_create_entity_observations_relations(client: AsyncClient, file_service, project_url): """Should create entity successfully.""" data = { "title": "TestEntity", "folder": "test", "content": """ # TestContent ## Observations - [note] This is notable #tag1 (testing) - related to [[SomeOtherThing]] """, } # Create an entity response = await client.post(f"{project_url}/knowledge/entities", json=data) # Verify creation assert response.status_code == 200 entity = EntityResponse.model_validate(response.json()) assert entity.permalink == "test/test-entity" assert entity.file_path == "test/TestEntity.md" assert entity.entity_type == "note" assert entity.content_type == "text/markdown" assert len(entity.observations) == 1 assert entity.observations[0].category == "note" assert entity.observations[0].content == "This is notable #tag1" assert entity.observations[0].tags == ["tag1"] assert entity.observations[0].context == "testing" assert len(entity.relations) == 1 assert entity.relations[0].relation_type == "related to" assert entity.relations[0].from_id == "test/test-entity" assert entity.relations[0].to_id is None # Verify file has new content but preserved metadata file_path = file_service.get_entity_path(entity) file_content, _ = await file_service.read_file(file_path) assert data["content"].strip() in file_content @pytest.mark.asyncio async def test_relation_resolution_after_creation(client: AsyncClient, project_url): """Test that relation resolution works after creating entities and handles exceptions gracefully.""" # Create first entity with unresolved relation entity1_data = { "title": "EntityOne", "folder": "test", "entity_type": "test", "content": "This entity references [[EntityTwo]]", } response1 = await client.put( f"{project_url}/knowledge/entities/test/entity-one", json=entity1_data ) assert response1.status_code == 201 entity1 = response1.json() # Verify relation exists but is unresolved assert len(entity1["relations"]) == 1 assert entity1["relations"][0]["to_id"] is None assert entity1["relations"][0]["to_name"] == "EntityTwo" # Create the referenced entity entity2_data = { "title": "EntityTwo", "folder": "test", "entity_type": "test", "content": "This is the referenced entity", } response2 = await client.put( f"{project_url}/knowledge/entities/test/entity-two", json=entity2_data ) assert response2.status_code == 201 # Verify the original entity's relation was resolved response_check = await client.get(f"{project_url}/knowledge/entities/test/entity-one") assert response_check.status_code == 200 updated_entity1 = response_check.json() # The relation should now be resolved via the automatic resolution after entity creation resolved_relations = [r for r in updated_entity1["relations"] if r["to_id"] is not None] assert ( len(resolved_relations) >= 0 ) # May or may not be resolved immediately depending on timing @pytest.mark.asyncio async def test_relation_resolution_exception_handling(client: AsyncClient, project_url): """Test that relation resolution exceptions are handled gracefully.""" import unittest.mock # Create an entity that would trigger relation resolution entity_data = { "title": "ExceptionTest", "folder": "test", "entity_type": "test", "content": "This entity has a [[Relation]]", } # Mock the sync service to raise an exception during relation resolution # We'll patch at the module level where it's imported with unittest.mock.patch( "basic_memory.api.routers.knowledge_router.SyncServiceDep", side_effect=lambda: unittest.mock.AsyncMock(), ) as mock_sync_service_dep: # Configure the mock sync service to raise an exception mock_sync_service = unittest.mock.AsyncMock() mock_sync_service.resolve_relations.side_effect = Exception("Sync service failed") mock_sync_service_dep.return_value = mock_sync_service # This should still succeed even though relation resolution fails response = await client.put( f"{project_url}/knowledge/entities/test/exception-test", json=entity_data ) assert response.status_code == 201 entity = response.json() # Verify the entity was still created successfully assert entity["title"] == "ExceptionTest" assert len(entity["relations"]) == 1 # Relation should still be there, just unresolved @pytest.mark.asyncio async def test_get_entity_by_permalink(client: AsyncClient, project_url): """Should retrieve an entity by path ID.""" # First create an entity data = {"title": "TestEntity", "folder": "test", "entity_type": "test"} response = await client.post(f"{project_url}/knowledge/entities", json=data) assert response.status_code == 200 data = response.json() # Now get it by permalink permalink = data["permalink"] response = await client.get(f"{project_url}/knowledge/entities/{permalink}") # Verify retrieval assert response.status_code == 200 entity = response.json() assert entity["title"] == "TestEntity" assert entity["file_path"] == "test/TestEntity.md" assert entity["entity_type"] == "test" assert entity["permalink"] == "test/test-entity" @pytest.mark.asyncio async def test_get_entity_by_file_path(client: AsyncClient, project_url): """Should retrieve an entity by path ID.""" # First create an entity data = {"title": "TestEntity", "folder": "test", "entity_type": "test"} response = await client.post(f"{project_url}/knowledge/entities", json=data) assert response.status_code == 200 data = response.json() # Now get it by path file_path = data["file_path"] response = await client.get(f"{project_url}/knowledge/entities/{file_path}") # Verify retrieval assert response.status_code == 200 entity = response.json() assert entity["title"] == "TestEntity" assert entity["file_path"] == "test/TestEntity.md" assert entity["entity_type"] == "test" assert entity["permalink"] == "test/test-entity" @pytest.mark.asyncio async def test_get_entities(client: AsyncClient, project_url): """Should open multiple entities by path IDs.""" # Create a few entities with different names await client.post( f"{project_url}/knowledge/entities", json={"title": "AlphaTest", "folder": "", "entity_type": "test"}, ) await client.post( f"{project_url}/knowledge/entities", json={"title": "BetaTest", "folder": "", "entity_type": "test"}, ) # Open nodes by path IDs response = await client.get( f"{project_url}/knowledge/entities?permalink=alpha-test&permalink=beta-test", ) # Verify results assert response.status_code == 200 data = response.json() assert len(data["entities"]) == 2 entity_0 = data["entities"][0] assert entity_0["title"] == "AlphaTest" assert entity_0["file_path"] == "AlphaTest.md" assert entity_0["entity_type"] == "test" assert entity_0["permalink"] == "alpha-test" entity_1 = data["entities"][1] assert entity_1["title"] == "BetaTest" assert entity_1["file_path"] == "BetaTest.md" assert entity_1["entity_type"] == "test" assert entity_1["permalink"] == "beta-test" @pytest.mark.asyncio async def test_delete_entity(client: AsyncClient, project_url): """Test DELETE /knowledge/entities with path ID.""" # Create test entity entity_data = {"file_path": "TestEntity", "entity_type": "test"} await client.post(f"{project_url}/knowledge/entities", json=entity_data) # Test deletion response = await client.post( f"{project_url}/knowledge/entities/delete", json={"permalinks": ["test-entity"]} ) assert response.status_code == 200 assert response.json() == {"deleted": True} # Verify entity is gone permalink = quote("test/TestEntity") response = await client.get(f"{project_url}/knowledge/entities/{permalink}") assert response.status_code == 404 @pytest.mark.asyncio async def test_delete_single_entity(client: AsyncClient, project_url): """Test DELETE /knowledge/entities with path ID.""" # Create test entity entity_data = {"title": "TestEntity", "folder": "", "entity_type": "test"} await client.post(f"{project_url}/knowledge/entities", json=entity_data) # Test deletion response = await client.delete(f"{project_url}/knowledge/entities/test-entity") assert response.status_code == 200 assert response.json() == {"deleted": True} # Verify entity is gone permalink = quote("test/TestEntity") response = await client.get(f"{project_url}/knowledge/entities/{permalink}") assert response.status_code == 404 @pytest.mark.asyncio async def test_delete_single_entity_by_title(client: AsyncClient, project_url): """Test DELETE /knowledge/entities with file path.""" # Create test entity entity_data = {"title": "TestEntity", "folder": "", "entity_type": "test"} response = await client.post(f"{project_url}/knowledge/entities", json=entity_data) assert response.status_code == 200 data = response.json() # Test deletion response = await client.delete(f"{project_url}/knowledge/entities/TestEntity") assert response.status_code == 200 assert response.json() == {"deleted": True} # Verify entity is gone file_path = quote(data["file_path"]) response = await client.get(f"{project_url}/knowledge/entities/{file_path}") assert response.status_code == 404 @pytest.mark.asyncio async def test_delete_single_entity_not_found(client: AsyncClient, project_url): """Test DELETE /knowledge/entities with path ID.""" # Test deletion response = await client.delete(f"{project_url}/knowledge/entities/test-not-found") assert response.status_code == 200 assert response.json() == {"deleted": False} @pytest.mark.asyncio async def test_delete_entity_bulk(client: AsyncClient, project_url): """Test bulk entity deletion using path IDs.""" # Create test entities await client.post( f"{project_url}/knowledge/entities", json={"file_path": "Entity1", "entity_type": "test"} ) await client.post( f"{project_url}/knowledge/entities", json={"file_path": "Entity2", "entity_type": "test"} ) # Test deletion response = await client.post( f"{project_url}/knowledge/entities/delete", json={"permalinks": ["Entity1", "Entity2"]} ) assert response.status_code == 200 assert response.json() == {"deleted": True} # Verify entities are gone for name in ["Entity1", "Entity2"]: permalink = quote(f"{name}") response = await client.get(f"{project_url}/knowledge/entities/{permalink}") assert response.status_code == 404 @pytest.mark.asyncio async def test_delete_nonexistent_entity(client: AsyncClient, project_url): """Test deleting a nonexistent entity by path ID.""" response = await client.post( f"{project_url}/knowledge/entities/delete", json={"permalinks": ["non_existent"]} ) assert response.status_code == 200 assert response.json() == {"deleted": True} @pytest.mark.asyncio async def test_entity_indexing(client: AsyncClient, project_url): """Test entity creation includes search indexing.""" # Create entity response = await client.post( f"{project_url}/knowledge/entities", json={ "title": "SearchTest", "folder": "", "entity_type": "test", "observations": ["Unique searchable observation"], }, ) assert response.status_code == 200 # Verify it's searchable search_response = await client.post( f"{project_url}/search/", json={"text": "search", "entity_types": [SearchItemType.ENTITY.value]}, ) assert search_response.status_code == 200 search_result = SearchResponse.model_validate(search_response.json()) assert len(search_result.results) == 1 assert search_result.results[0].permalink == "search-test" assert search_result.results[0].type == SearchItemType.ENTITY.value @pytest.mark.asyncio async def test_entity_delete_indexing(client: AsyncClient, project_url): """Test deleted entities are removed from search index.""" # Create entity response = await client.post( f"{project_url}/knowledge/entities", json={ "title": "DeleteTest", "folder": "", "entity_type": "test", "observations": ["Searchable observation that should be removed"], }, ) assert response.status_code == 200 entity = response.json() # Verify it's initially searchable search_response = await client.post( f"{project_url}/search/", json={"text": "delete", "entity_types": [SearchItemType.ENTITY.value]}, ) search_result = SearchResponse.model_validate(search_response.json()) assert len(search_result.results) == 1 # Delete entity delete_response = await client.post( f"{project_url}/knowledge/entities/delete", json={"permalinks": [entity["permalink"]]} ) assert delete_response.status_code == 200 # Verify it's no longer searchable search_response = await client.post( f"{project_url}/search/", json={"text": "delete", "types": [SearchItemType.ENTITY.value]} ) search_result = SearchResponse.model_validate(search_response.json()) assert len(search_result.results) == 0 @pytest.mark.asyncio async def test_update_entity_basic(client: AsyncClient, project_url): """Test basic entity field updates.""" # Create initial entity response = await client.post( f"{project_url}/knowledge/entities", json={ "title": "test", "folder": "", "entity_type": "test", "content": "Initial summary", "entity_metadata": {"status": "draft"}, }, ) entity_response = response.json() # Update fields entity = Entity(**entity_response, folder="") entity.entity_metadata["status"] = "final" entity.content = "Updated summary" response = await client.put( f"{project_url}/knowledge/entities/{entity.permalink}", json=entity.model_dump() ) assert response.status_code == 200 updated = response.json() # Verify updates assert updated["entity_metadata"]["status"] == "final" # Preserved response = await client.get(f"{project_url}/resource/{updated['permalink']}?content=true") # raw markdown content fetched = response.text assert "Updated summary" in fetched @pytest.mark.asyncio async def test_update_entity_content(client: AsyncClient, project_url): """Test updating content for different entity types.""" # Create a note entity response = await client.post( f"{project_url}/knowledge/entities", json={"title": "test-note", "folder": "", "entity_type": "note", "summary": "Test note"}, ) note = response.json() # Update fields entity = Entity(**note, folder="") entity.content = "# Updated Note\n\nNew content." response = await client.put( f"{project_url}/knowledge/entities/{note['permalink']}", json=entity.model_dump() ) assert response.status_code == 200 updated = response.json() # Verify through get request to check file response = await client.get(f"{project_url}/resource/{updated['permalink']}?content=true") # raw markdown content fetched = response.text assert "# Updated Note" in fetched assert "New content" in fetched @pytest.mark.asyncio async def test_update_entity_type_conversion(client: AsyncClient, project_url): """Test converting between note and knowledge types.""" # Create a note note_data = { "title": "test-note", "folder": "", "entity_type": "note", "summary": "Test note", "content": "# Test Note\n\nInitial content.", } response = await client.post(f"{project_url}/knowledge/entities", json=note_data) note = response.json() # Update fields entity = Entity(**note, folder="") entity.entity_type = "test" response = await client.put( f"{project_url}/knowledge/entities/{note['permalink']}", json=entity.model_dump() ) assert response.status_code == 200 updated = response.json() # Verify conversion assert updated["entity_type"] == "test" # Get latest to verify file format response = await client.get(f"{project_url}/knowledge/entities/{updated['permalink']}") knowledge = response.json() assert knowledge.get("content") is None @pytest.mark.asyncio async def test_update_entity_metadata(client: AsyncClient, project_url): """Test updating entity metadata.""" # Create entity data = { "title": "test", "folder": "", "entity_type": "test", "entity_metadata": {"status": "draft"}, } response = await client.post(f"{project_url}/knowledge/entities", json=data) entity_response = response.json() # Update fields entity = Entity(**entity_response, folder="") entity.entity_metadata["status"] = "final" entity.entity_metadata["reviewed"] = True # Update metadata response = await client.put( f"{project_url}/knowledge/entities/{entity.permalink}", json=entity.model_dump() ) assert response.status_code == 200 updated = response.json() # Verify metadata was merged, not replaced assert updated["entity_metadata"]["status"] == "final" assert updated["entity_metadata"]["reviewed"] in (True, "True") @pytest.mark.asyncio async def test_update_entity_not_found_does_create(client: AsyncClient, project_url): """Test updating non-existent entity does a create""" data = { "title": "nonexistent", "folder": "", "entity_type": "test", "observations": ["First observation", "Second observation"], } entity = Entity(**data) response = await client.put( f"{project_url}/knowledge/entities/nonexistent", json=entity.model_dump() ) assert response.status_code == 201 @pytest.mark.asyncio async def test_update_entity_incorrect_permalink(client: AsyncClient, project_url): """Test updating non-existent entity does a create""" data = { "title": "Test Entity", "folder": "", "entity_type": "test", "observations": ["First observation", "Second observation"], } entity = Entity(**data) response = await client.put( f"{project_url}/knowledge/entities/nonexistent", json=entity.model_dump() ) assert response.status_code == 400 @pytest.mark.asyncio async def test_update_entity_search_index(client: AsyncClient, project_url): """Test search index is updated after entity changes.""" # Create entity data = { "title": "test", "folder": "", "entity_type": "test", "content": "Initial searchable content", } response = await client.post(f"{project_url}/knowledge/entities", json=data) entity_response = response.json() # Update fields entity = Entity(**entity_response, folder="") entity.content = "Updated with unique sphinx marker" response = await client.put( f"{project_url}/knowledge/entities/{entity.permalink}", json=entity.model_dump() ) assert response.status_code == 200 # Search should find new content search_response = await client.post( f"{project_url}/search/", json={"text": "sphinx marker", "entity_types": [SearchItemType.ENTITY.value]}, ) results = search_response.json()["results"] assert len(results) == 1 assert results[0]["permalink"] == entity.permalink # PATCH edit entity endpoint tests @pytest.mark.asyncio async def test_edit_entity_append(client: AsyncClient, project_url): """Test appending content to an entity via PATCH endpoint.""" # Create test entity response = await client.post( f"{project_url}/knowledge/entities", json={ "title": "Test Note", "folder": "test", "entity_type": "note", "content": "Original content", }, ) assert response.status_code == 200 entity = response.json() # Edit entity with append operation response = await client.patch( f"{project_url}/knowledge/entities/{entity['permalink']}", json={"operation": "append", "content": "Appended content"}, ) if response.status_code != 200: print(f"PATCH failed with status {response.status_code}") print(f"Response content: {response.text}") assert response.status_code == 200 updated = response.json() # Verify content was appended by reading the file response = await client.get(f"{project_url}/resource/{updated['permalink']}?content=true") file_content = response.text assert "Original content" in file_content assert "Appended content" in file_content assert file_content.index("Original content") < file_content.index("Appended content") @pytest.mark.asyncio async def test_edit_entity_prepend(client: AsyncClient, project_url): """Test prepending content to an entity via PATCH endpoint.""" # Create test entity response = await client.post( f"{project_url}/knowledge/entities", json={ "title": "Test Note", "folder": "test", "entity_type": "note", "content": "Original content", }, ) assert response.status_code == 200 entity = response.json() # Edit entity with prepend operation response = await client.patch( f"{project_url}/knowledge/entities/{entity['permalink']}", json={"operation": "prepend", "content": "Prepended content"}, ) if response.status_code != 200: print(f"PATCH prepend failed with status {response.status_code}") print(f"Response content: {response.text}") assert response.status_code == 200 updated = response.json() # Verify the entire file content structure response = await client.get(f"{project_url}/resource/{updated['permalink']}?content=true") file_content = response.text # Expected content with frontmatter preserved and content prepended to body expected_content = normalize_newlines("""--- title: Test Note type: note permalink: test/test-note --- Prepended content Original content""") assert file_content.strip() == expected_content.strip() @pytest.mark.asyncio async def test_edit_entity_find_replace(client: AsyncClient, project_url): """Test find and replace operation via PATCH endpoint.""" # Create test entity response = await client.post( f"{project_url}/knowledge/entities", json={ "title": "Test Note", "folder": "test", "entity_type": "note", "content": "This is old content that needs updating", }, ) assert response.status_code == 200 entity = response.json() # Edit entity with find_replace operation response = await client.patch( f"{project_url}/knowledge/entities/{entity['permalink']}", json={"operation": "find_replace", "content": "new content", "find_text": "old content"}, ) assert response.status_code == 200 updated = response.json() # Verify content was replaced response = await client.get(f"{project_url}/resource/{updated['permalink']}?content=true") file_content = response.text assert "old content" not in file_content assert "This is new content that needs updating" in file_content @pytest.mark.asyncio async def test_edit_entity_find_replace_with_expected_replacements( client: AsyncClient, project_url ): """Test find and replace with expected_replacements parameter.""" # Create test entity with repeated text response = await client.post( f"{project_url}/knowledge/entities", json={ "title": "Sample Note", "folder": "docs", "entity_type": "note", "content": "The word banana appears here. Another banana word here.", }, ) assert response.status_code == 200 entity = response.json() # Edit entity with find_replace operation, expecting 2 replacements response = await client.patch( f"{project_url}/knowledge/entities/{entity['permalink']}", json={ "operation": "find_replace", "content": "apple", "find_text": "banana", "expected_replacements": 2, }, ) assert response.status_code == 200 updated = response.json() # Verify both instances were replaced response = await client.get(f"{project_url}/resource/{updated['permalink']}?content=true") file_content = response.text assert "The word apple appears here. Another apple word here." in file_content @pytest.mark.asyncio async def test_edit_entity_replace_section(client: AsyncClient, project_url): """Test replacing a section via PATCH endpoint.""" # Create test entity with sections content = """# Main Title ## Section 1 Original section 1 content ## Section 2 Original section 2 content""" response = await client.post( f"{project_url}/knowledge/entities", json={ "title": "Sample Note", "folder": "docs", "entity_type": "note", "content": content, }, ) assert response.status_code == 200 entity = response.json() # Edit entity with replace_section operation response = await client.patch( f"{project_url}/knowledge/entities/{entity['permalink']}", json={ "operation": "replace_section", "content": "New section 1 content", "section": "## Section 1", }, ) assert response.status_code == 200 updated = response.json() # Verify section was replaced response = await client.get(f"{project_url}/resource/{updated['permalink']}?content=true") file_content = response.text assert "New section 1 content" in file_content assert "Original section 1 content" not in file_content assert "Original section 2 content" in file_content # Other sections preserved @pytest.mark.asyncio async def test_edit_entity_not_found(client: AsyncClient, project_url): """Test editing a non-existent entity returns 400.""" response = await client.patch( f"{project_url}/knowledge/entities/non-existent", json={"operation": "append", "content": "content"}, ) assert response.status_code == 400 assert "Entity not found" in response.json()["detail"] @pytest.mark.asyncio async def test_edit_entity_invalid_operation(client: AsyncClient, project_url): """Test editing with invalid operation returns 400.""" # Create test entity response = await client.post( f"{project_url}/knowledge/entities", json={ "title": "Test Note", "folder": "test", "entity_type": "note", "content": "Original content", }, ) assert response.status_code == 200 entity = response.json() # Try invalid operation response = await client.patch( f"{project_url}/knowledge/entities/{entity['permalink']}", json={"operation": "invalid_operation", "content": "content"}, ) assert response.status_code == 422 assert "invalid_operation" in response.json()["detail"][0]["input"] @pytest.mark.asyncio async def test_edit_entity_find_replace_missing_find_text(client: AsyncClient, project_url): """Test find_replace without find_text returns 400.""" # Create test entity response = await client.post( f"{project_url}/knowledge/entities", json={ "title": "Test Note", "folder": "test", "entity_type": "note", "content": "Original content", }, ) assert response.status_code == 200 entity = response.json() # Try find_replace without find_text response = await client.patch( f"{project_url}/knowledge/entities/{entity['permalink']}", json={"operation": "find_replace", "content": "new content"}, ) assert response.status_code == 400 assert "find_text is required" in response.json()["detail"] @pytest.mark.asyncio async def test_edit_entity_replace_section_missing_section(client: AsyncClient, project_url): """Test replace_section without section parameter returns 400.""" # Create test entity response = await client.post( f"{project_url}/knowledge/entities", json={ "title": "Test Note", "folder": "test", "entity_type": "note", "content": "Original content", }, ) assert response.status_code == 200 entity = response.json() # Try replace_section without section response = await client.patch( f"{project_url}/knowledge/entities/{entity['permalink']}", json={"operation": "replace_section", "content": "new content"}, ) assert response.status_code == 400 assert "section is required" in response.json()["detail"] @pytest.mark.asyncio async def test_edit_entity_find_replace_not_found(client: AsyncClient, project_url): """Test find_replace when text is not found returns 400.""" # Create test entity response = await client.post( f"{project_url}/knowledge/entities", json={ "title": "Test Note", "folder": "test", "entity_type": "note", "content": "This is some content", }, ) assert response.status_code == 200 entity = response.json() # Try to replace text that doesn't exist response = await client.patch( f"{project_url}/knowledge/entities/{entity['permalink']}", json={"operation": "find_replace", "content": "new content", "find_text": "nonexistent"}, ) assert response.status_code == 400 assert "Text to replace not found" in response.json()["detail"] @pytest.mark.asyncio async def test_edit_entity_find_replace_wrong_expected_count(client: AsyncClient, project_url): """Test find_replace with wrong expected_replacements count returns 400.""" # Create test entity with repeated text response = await client.post( f"{project_url}/knowledge/entities", json={ "title": "Sample Note", "folder": "docs", "entity_type": "note", "content": "The word banana appears here. Another banana word here.", }, ) assert response.status_code == 200 entity = response.json() # Try to replace with wrong expected count response = await client.patch( f"{project_url}/knowledge/entities/{entity['permalink']}", json={ "operation": "find_replace", "content": "replacement", "find_text": "banana", "expected_replacements": 1, # Wrong - there are actually 2 }, ) assert response.status_code == 400 assert "Expected 1 occurrences" in response.json()["detail"] assert "but found 2" in response.json()["detail"] @pytest.mark.asyncio async def test_edit_entity_search_reindex(client: AsyncClient, project_url): """Test that edited entities are reindexed for search.""" # Create test entity response = await client.post( f"{project_url}/knowledge/entities", json={ "title": "Search Test", "folder": "test", "entity_type": "note", "content": "Original searchable content", }, ) assert response.status_code == 200 entity = response.json() # Edit the entity response = await client.patch( f"{project_url}/knowledge/entities/{entity['permalink']}", json={"operation": "append", "content": " with unique zebra marker"}, ) assert response.status_code == 200 # Search should find the new content search_response = await client.post( f"{project_url}/search/", json={"text": "zebra marker", "entity_types": ["entity"]}, ) results = search_response.json()["results"] assert len(results) == 1 assert results[0]["permalink"] == entity["permalink"] # Move entity endpoint tests @pytest.mark.asyncio async def test_move_entity_success(client: AsyncClient, project_url): """Test successfully moving an entity to a new location.""" # Create test entity response = await client.post( f"{project_url}/knowledge/entities", json={ "title": "TestNote", "folder": "source", "entity_type": "note", "content": "Test content", }, ) assert response.status_code == 200 entity = response.json() original_permalink = entity["permalink"] # Move entity move_data = { "identifier": original_permalink, "destination_path": "target/MovedNote.md", } response = await client.post(f"{project_url}/knowledge/move", json=move_data) assert response.status_code == 200 response_model = EntityResponse.model_validate(response.json()) assert response_model.file_path == "target/MovedNote.md" # Verify original entity no longer exists response = await client.get(f"{project_url}/knowledge/entities/{original_permalink}") assert response.status_code == 404 # Verify entity exists at new location response = await client.get(f"{project_url}/knowledge/entities/target/moved-note") assert response.status_code == 200 moved_entity = response.json() assert moved_entity["file_path"] == "target/MovedNote.md" assert moved_entity["permalink"] == "target/moved-note" # Verify file content using resource endpoint response = await client.get(f"{project_url}/resource/target/moved-note?content=true") assert response.status_code == 200 file_content = response.text assert "Test content" in file_content @pytest.mark.asyncio async def test_move_entity_with_folder_creation(client: AsyncClient, project_url): """Test moving entity creates necessary folders.""" # Create test entity response = await client.post( f"{project_url}/knowledge/entities", json={ "title": "TestNote", "folder": "", "entity_type": "note", "content": "Test content", }, ) assert response.status_code == 200 entity = response.json() # Move to deeply nested path move_data = { "identifier": entity["permalink"], "destination_path": "deeply/nested/folder/MovedNote.md", } response = await client.post(f"{project_url}/knowledge/move", json=move_data) assert response.status_code == 200 # Verify entity exists at new location response = await client.get(f"{project_url}/knowledge/entities/deeply/nested/folder/moved-note") assert response.status_code == 200 moved_entity = response.json() assert moved_entity["file_path"] == "deeply/nested/folder/MovedNote.md" @pytest.mark.asyncio async def test_move_entity_with_observations_and_relations(client: AsyncClient, project_url): """Test moving entity preserves observations and relations.""" # Create test entity with complex content content = """# Complex Entity ## Observations - [note] Important observation #tag1 - [feature] Key feature #feature - relation to [[SomeOtherEntity]] - depends on [[Dependency]] Some additional content.""" response = await client.post( f"{project_url}/knowledge/entities", json={ "title": "ComplexEntity", "folder": "source", "entity_type": "note", "content": content, }, ) assert response.status_code == 200 entity = response.json() # Verify original observations and relations assert len(entity["observations"]) == 2 assert len(entity["relations"]) == 2 # Move entity move_data = { "identifier": entity["permalink"], "destination_path": "target/MovedComplex.md", } response = await client.post(f"{project_url}/knowledge/move", json=move_data) assert response.status_code == 200 # Verify moved entity preserves data response = await client.get(f"{project_url}/knowledge/entities/target/moved-complex") assert response.status_code == 200 moved_entity = response.json() # Check observations preserved assert len(moved_entity["observations"]) == 2 obs_categories = {obs["category"] for obs in moved_entity["observations"]} assert obs_categories == {"note", "feature"} # Check relations preserved assert len(moved_entity["relations"]) == 2 rel_types = {rel["relation_type"] for rel in moved_entity["relations"]} assert rel_types == {"relation to", "depends on"} # Verify file content preserved response = await client.get(f"{project_url}/resource/target/moved-complex?content=true") assert response.status_code == 200 file_content = response.text assert "Important observation #tag1" in file_content assert "[[SomeOtherEntity]]" in file_content @pytest.mark.asyncio async def test_move_entity_search_reindexing(client: AsyncClient, project_url): """Test that moved entities are properly reindexed for search.""" # Create searchable entity response = await client.post( f"{project_url}/knowledge/entities", json={ "title": "SearchableNote", "folder": "source", "entity_type": "note", "content": "Unique searchable elephant content", }, ) assert response.status_code == 200 entity = response.json() # Move entity move_data = { "identifier": entity["permalink"], "destination_path": "target/MovedSearchable.md", } response = await client.post(f"{project_url}/knowledge/move", json=move_data) assert response.status_code == 200 # Search should find entity at new location search_response = await client.post( f"{project_url}/search/", json={"text": "elephant", "entity_types": [SearchItemType.ENTITY.value]}, ) results = search_response.json()["results"] assert len(results) == 1 assert results[0]["permalink"] == "target/moved-searchable" @pytest.mark.asyncio async def test_move_entity_not_found(client: AsyncClient, project_url): """Test moving non-existent entity returns 400 error.""" move_data = { "identifier": "non-existent-entity", "destination_path": "target/SomeFile.md", } response = await client.post(f"{project_url}/knowledge/move", json=move_data) assert response.status_code == 400 assert "Entity not found" in response.json()["detail"] @pytest.mark.asyncio async def test_move_entity_invalid_destination_path(client: AsyncClient, project_url): """Test moving entity with invalid destination path.""" # Create test entity response = await client.post( f"{project_url}/knowledge/entities", json={ "title": "TestNote", "folder": "", "entity_type": "note", "content": "Test content", }, ) assert response.status_code == 200 entity = response.json() # Test various invalid paths invalid_paths = [ "/absolute/path.md", # Absolute path "../parent/path.md", # Parent directory "", # Empty string " ", # Whitespace only ] for invalid_path in invalid_paths: move_data = { "identifier": entity["permalink"], "destination_path": invalid_path, } response = await client.post(f"{project_url}/knowledge/move", json=move_data) assert response.status_code == 422 # Validation error @pytest.mark.asyncio async def test_move_entity_destination_exists(client: AsyncClient, project_url): """Test moving entity to existing destination returns error.""" # Create source entity response = await client.post( f"{project_url}/knowledge/entities", json={ "title": "SourceNote", "folder": "source", "entity_type": "note", "content": "Source content", }, ) assert response.status_code == 200 source_entity = response.json() # Create destination entity response = await client.post( f"{project_url}/knowledge/entities", json={ "title": "DestinationNote", "folder": "target", "entity_type": "note", "content": "Destination content", }, ) assert response.status_code == 200 # Try to move source to existing destination move_data = { "identifier": source_entity["permalink"], "destination_path": "target/DestinationNote.md", } response = await client.post(f"{project_url}/knowledge/move", json=move_data) assert response.status_code == 400 assert "already exists" in response.json()["detail"] @pytest.mark.asyncio async def test_move_entity_missing_identifier(client: AsyncClient, project_url): """Test move request with missing identifier.""" move_data = { "destination_path": "target/SomeFile.md", } response = await client.post(f"{project_url}/knowledge/move", json=move_data) assert response.status_code == 422 # Validation error @pytest.mark.asyncio async def test_move_entity_missing_destination(client: AsyncClient, project_url): """Test move request with missing destination path.""" move_data = { "identifier": "some-entity", } response = await client.post(f"{project_url}/knowledge/move", json=move_data) assert response.status_code == 422 # Validation error @pytest.mark.asyncio async def test_move_entity_by_file_path(client: AsyncClient, project_url): """Test moving entity using file path as identifier.""" # Create test entity response = await client.post( f"{project_url}/knowledge/entities", json={ "title": "TestNote", "folder": "source", "entity_type": "note", "content": "Test content", }, ) assert response.status_code == 200 entity = response.json() # Move using file path as identifier move_data = { "identifier": entity["file_path"], "destination_path": "target/MovedByPath.md", } response = await client.post(f"{project_url}/knowledge/move", json=move_data) assert response.status_code == 200 # Verify entity exists at new location response = await client.get(f"{project_url}/knowledge/entities/target/moved-by-path") assert response.status_code == 200 moved_entity = response.json() assert moved_entity["file_path"] == "target/MovedByPath.md" @pytest.mark.asyncio async def test_move_entity_by_title(client: AsyncClient, project_url): """Test moving entity using title as identifier.""" # Create test entity with unique title response = await client.post( f"{project_url}/knowledge/entities", json={ "title": "UniqueTestTitle", "folder": "source", "entity_type": "note", "content": "Test content", }, ) assert response.status_code == 200 # Move using title as identifier move_data = { "identifier": "UniqueTestTitle", "destination_path": "target/MovedByTitle.md", } response = await client.post(f"{project_url}/knowledge/move", json=move_data) assert response.status_code == 200 # Verify entity exists at new location response = await client.get(f"{project_url}/knowledge/entities/target/moved-by-title") assert response.status_code == 200 moved_entity = response.json() assert moved_entity["file_path"] == "target/MovedByTitle.md" assert moved_entity["title"] == "UniqueTestTitle"

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/basicmachines-co/basic-memory'

If you have feedback or need assistance with the MCP directory API, please join our Discord server