Skip to main content
Glama
test_semantic_links.py39.5 kB
"""Comprehensive test suite for semantic link types in the Zettelkasten MCP implementation.""" import datetime import pytest from unittest.mock import patch, MagicMock, ANY from zettelkasten_mcp.models.schema import LinkType, Note, NoteType, Tag, Link from zettelkasten_mcp.services.zettel_service import ZettelService from zettelkasten_mcp.server.mcp_server import ZettelkastenMcpServer class TestSemanticLinks: """Test suite for semantic links in the Zettelkasten system.""" @pytest.fixture def mock_datetime_now(self): """Mock datetime.now() to return a fixed timestamp.""" with patch("datetime.datetime") as mock_dt: mock_dt.now.return_value = datetime.datetime(2025, 1, 1, 12, 0, 0) # Ensure constructor still works correctly mock_dt.side_effect = lambda *args, **kw: datetime.datetime(*args, **kw) yield mock_dt def test_all_link_types_creation(self, zettel_service): """Test creating links with all semantic link types.""" # Create two test notes source_note = zettel_service.create_note( title="Source Note", content="Source note content", note_type=NoteType.PERMANENT, tags=["test", "source"] ) target_note = zettel_service.create_note( title="Target Note", content="Target note content", note_type=NoteType.PERMANENT, tags=["test", "target"] ) # Create links with each link type link_types = [ (LinkType.REFERENCE, "Reference link"), (LinkType.EXTENDS, "Extends link"), (LinkType.EXTENDED_BY, "Extended by link"), (LinkType.REFINES, "Refines link"), (LinkType.REFINED_BY, "Refined by link"), (LinkType.CONTRADICTS, "Contradicts link"), (LinkType.CONTRADICTED_BY, "Contradicted by link"), (LinkType.QUESTIONS, "Questions link"), (LinkType.QUESTIONED_BY, "Questioned by link"), (LinkType.SUPPORTS, "Supports link"), (LinkType.SUPPORTED_BY, "Supported by link"), (LinkType.RELATED, "Related link") ] # Need separate target notes to avoid uniqueness constraint target_notes = [] for i in range(len(link_types)): t_note = zettel_service.create_note( title=f"Target Note {i+1}", content=f"Target note {i+1} content", note_type=NoteType.PERMANENT, tags=["test", "target"] ) target_notes.append(t_note) # Create links with each link type for i, (link_type, description) in enumerate(link_types): # Create link source, target = zettel_service.create_link( source_id=source_note.id, target_id=target_notes[i].id, link_type=link_type, description=description ) # Verify source note's links matching_links = [link for link in source.links if link.target_id == target_notes[i].id] assert len(matching_links) == 1 assert matching_links[0].link_type == link_type assert matching_links[0].description == description # Verify link retrieval through repository updated_source = zettel_service.get_note(source_note.id) matching_links = [link for link in updated_source.links if link.target_id == target_notes[i].id] assert len(matching_links) == 1 assert matching_links[0].link_type == link_type assert matching_links[0].description == description def test_bidirectional_semantic_links(self, zettel_service): """Test creating bidirectional links with proper semantic inverses.""" # Create two test notes source_note = zettel_service.create_note( title="Bidirectional Source Note", content="Source note content for bidirectional links", note_type=NoteType.PERMANENT, tags=["test", "bidirectional"] ) # Test pairs of semantic inverse relationships inverse_pairs = [ # (from_source, expected_inverse) (LinkType.EXTENDS, LinkType.EXTENDED_BY), (LinkType.EXTENDED_BY, LinkType.EXTENDS), (LinkType.REFINES, LinkType.REFINED_BY), (LinkType.REFINED_BY, LinkType.REFINES), (LinkType.CONTRADICTS, LinkType.CONTRADICTED_BY), (LinkType.CONTRADICTED_BY, LinkType.CONTRADICTS), (LinkType.QUESTIONS, LinkType.QUESTIONED_BY), (LinkType.QUESTIONED_BY, LinkType.QUESTIONS), (LinkType.SUPPORTS, LinkType.SUPPORTED_BY), (LinkType.SUPPORTED_BY, LinkType.SUPPORTS), # Symmetric relationships (LinkType.REFERENCE, LinkType.REFERENCE), (LinkType.RELATED, LinkType.RELATED) ] # Need separate target notes to avoid uniqueness constraint target_notes = [] for i in range(len(inverse_pairs)): t_note = zettel_service.create_note( title=f"Bidirectional Target {i+1}", content=f"Target note {i+1} for bidirectional testing", note_type=NoteType.PERMANENT, tags=["test", "bidirectional"] ) target_notes.append(t_note) # Test each pair for i, (source_type, expected_inverse) in enumerate(inverse_pairs): # Create bidirectional link source, target = zettel_service.create_link( source_id=source_note.id, target_id=target_notes[i].id, link_type=source_type, description=f"Testing {source_type.value} relationship", bidirectional=True ) # Verify outgoing link from source source_links = [link for link in source.links if link.target_id == target_notes[i].id] assert len(source_links) == 1 assert source_links[0].link_type == source_type # Verify incoming link to target (inverse relationship) target_links = [link for link in target.links if link.target_id == source_note.id] assert len(target_links) == 1 assert target_links[0].link_type == expected_inverse # Verify through get_linked_notes (outgoing) outgoing_links = zettel_service.get_linked_notes(source_note.id, "outgoing") assert any( note.id == target_notes[i].id for note in outgoing_links ) # Verify through get_linked_notes (incoming) incoming_links = zettel_service.get_linked_notes(target_notes[i].id, "incoming") assert any( note.id == source_note.id for note in incoming_links ) def test_custom_bidirectional_link_types(self, zettel_service): """Test bidirectional links with custom inverse types.""" # Create test notes source_note = zettel_service.create_note( title="Custom Bidirectional Source", content="Source note content for custom bidirectional links", note_type=NoteType.PERMANENT, tags=["test", "custom-bidirectional"] ) target_note = zettel_service.create_note( title="Custom Bidirectional Target", content="Target note content for custom bidirectional links", note_type=NoteType.PERMANENT, tags=["test", "custom-bidirectional"] ) # Create bidirectional link with explicit inverse type # The second parameter should be a specific bidirectional_type, not a boolean # However, since the ZettelService interface doesn't support this directly, # we'll need to modify the test or mock the lower-level function # For now, we'll test this using two separate directional links source, _ = zettel_service.create_link( source_id=source_note.id, target_id=target_note.id, link_type=LinkType.EXTENDS, description="Forward direction" ) target, _ = zettel_service.create_link( source_id=target_note.id, target_id=source_note.id, link_type=LinkType.QUESTIONS, # Custom inverse type (not the expected EXTENDED_BY) description="Custom backward direction" ) # Verify links source_note = zettel_service.get_note(source_note.id) target_note = zettel_service.get_note(target_note.id) # Check outgoing link from source source_links = [link for link in source_note.links if link.target_id == target_note.id] assert len(source_links) == 1 assert source_links[0].link_type == LinkType.EXTENDS # Check outgoing link from target (custom inverse) target_links = [link for link in target_note.links if link.target_id == source_note.id] assert len(target_links) == 1 assert target_links[0].link_type == LinkType.QUESTIONS # Custom inverse type def test_links_persistence_through_save_and_load(self, zettel_service): """Test that link types persist correctly when notes are saved and reloaded.""" # Create test notes source_note = zettel_service.create_note( title="Persistence Source Note", content="Source note for testing persistence", note_type=NoteType.PERMANENT, tags=["test", "persistence"] ) target_note = zettel_service.create_note( title="Persistence Target Note", content="Target note for testing persistence", note_type=NoteType.PERMANENT, tags=["test", "persistence"] ) # Create link with a semantic type source, _ = zettel_service.create_link( source_id=source_note.id, target_id=target_note.id, link_type=LinkType.REFINES, description="Testing persistence of semantic links" ) # Save the source note with link updated_source = zettel_service.update_note( note_id=source_note.id, title=source_note.title, content=source_note.content + "\nAdditional content to trigger save." ) # Verify link is still present with correct type after save assert any( link.target_id == target_note.id and link.link_type == LinkType.REFINES for link in updated_source.links ) # Load the note from repository loaded_note = zettel_service.get_note(source_note.id) # Verify link is present with correct type after load matching_links = [link for link in loaded_note.links if link.target_id == target_note.id] assert len(matching_links) == 1 assert matching_links[0].link_type == LinkType.REFINES assert matching_links[0].description == "Testing persistence of semantic links" def test_multiple_link_types_between_notes(self, zettel_service): """Test creating multiple different link types between the same two notes.""" # Create test notes source_note = zettel_service.create_note( title="Multi-Link Source", content="Source note for multiple link types", note_type=NoteType.PERMANENT, tags=["test", "multi-link"] ) target_note = zettel_service.create_note( title="Multi-Link Target", content="Target note for multiple link types", note_type=NoteType.PERMANENT, tags=["test", "multi-link"] ) # Create multiple links with different types link_types = [ LinkType.EXTENDS, LinkType.SUPPORTS, LinkType.QUESTIONS ] for i, link_type in enumerate(link_types): source, _ = zettel_service.create_link( source_id=source_note.id, target_id=target_note.id, link_type=link_type, description=f"Link {i+1}: {link_type.value}" ) # Refresh the note from repository updated_source = zettel_service.get_note(source_note.id) # Verify all link types are present target_links = [link for link in updated_source.links if link.target_id == target_note.id] assert len(target_links) == len(link_types) # Verify each link type is present for link_type in link_types: assert any(link.link_type == link_type for link in target_links) def test_updating_link(self, zettel_service): """Test updating an existing link's type or description.""" # Create test notes source_note = zettel_service.create_note( title="Link Update Source", content="Source note for link update testing", note_type=NoteType.PERMANENT, tags=["test", "link-update"] ) target_note = zettel_service.create_note( title="Link Update Target", content="Target note for link update testing", note_type=NoteType.PERMANENT, tags=["test", "link-update"] ) # Create initial link source, _ = zettel_service.create_link( source_id=source_note.id, target_id=target_note.id, link_type=LinkType.REFERENCE, description="Initial link description" ) # Remove the link source, _ = zettel_service.remove_link( source_id=source_note.id, target_id=target_note.id, link_type=LinkType.REFERENCE ) # Verify link was removed assert not any( link.target_id == target_note.id for link in source.links ) # Create new link with different type source, _ = zettel_service.create_link( source_id=source_note.id, target_id=target_note.id, link_type=LinkType.EXTENDS, description="Updated link description" ) # Verify link was updated matching_links = [link for link in source.links if link.target_id == target_note.id] assert len(matching_links) == 1 assert matching_links[0].link_type == LinkType.EXTENDS assert matching_links[0].description == "Updated link description" # Load the note from repository to verify persistence loaded_note = zettel_service.get_note(source_note.id) matching_links = [link for link in loaded_note.links if link.target_id == target_note.id] assert len(matching_links) == 1 assert matching_links[0].link_type == LinkType.EXTENDS assert matching_links[0].description == "Updated link description" def test_removing_links(self, zettel_service): """Test removing links with specific semantic types.""" # Create test notes source_note = zettel_service.create_note( title="Link Removal Source", content="Source note for link removal testing", note_type=NoteType.PERMANENT, tags=["test", "link-removal"] ) target_note = zettel_service.create_note( title="Link Removal Target", content="Target note for link removal testing", note_type=NoteType.PERMANENT, tags=["test", "link-removal"] ) # Create multiple links with different types link_types = [ LinkType.EXTENDS, LinkType.SUPPORTS, LinkType.QUESTIONS ] for link_type in link_types: zettel_service.create_link( source_id=source_note.id, target_id=target_note.id, link_type=link_type, description=f"{link_type.value} description" ) # Refresh the source note source_note = zettel_service.get_note(source_note.id) # Verify all links were created target_links = [link for link in source_note.links if link.target_id == target_note.id] assert len(target_links) == len(link_types) # Remove specific link type zettel_service.remove_link( source_id=source_note.id, target_id=target_note.id, link_type=LinkType.SUPPORTS ) # Refresh the source note source_note = zettel_service.get_note(source_note.id) # Verify only the specified link was removed target_links = [link for link in source_note.links if link.target_id == target_note.id] assert len(target_links) == len(link_types) - 1 assert not any(link.link_type == LinkType.SUPPORTS for link in target_links) assert any(link.link_type == LinkType.EXTENDS for link in target_links) assert any(link.link_type == LinkType.QUESTIONS for link in target_links) # Remove all remaining links to target zettel_service.remove_link( source_id=source_note.id, target_id=target_note.id ) # Refresh the source note source_note = zettel_service.get_note(source_note.id) # Verify all links to target were removed target_links = [link for link in source_note.links if link.target_id == target_note.id] assert len(target_links) == 0 def test_bidirectional_link_removal(self, zettel_service): """Test removing bidirectional links.""" # Create test notes source_note = zettel_service.create_note( title="Bidirectional Removal Source", content="Source note for bidirectional link removal", note_type=NoteType.PERMANENT, tags=["test", "bidirectional-removal"] ) target_note = zettel_service.create_note( title="Bidirectional Removal Target", content="Target note for bidirectional link removal", note_type=NoteType.PERMANENT, tags=["test", "bidirectional-removal"] ) # Create bidirectional link source, target = zettel_service.create_link( source_id=source_note.id, target_id=target_note.id, link_type=LinkType.EXTENDS, description="Bidirectional link for removal testing", bidirectional=True ) # Verify bidirectional link was created assert any( link.target_id == target_note.id and link.link_type == LinkType.EXTENDS for link in source.links ) assert any( link.target_id == source_note.id and link.link_type == LinkType.EXTENDED_BY for link in target.links ) # Remove bidirectional link source, target = zettel_service.remove_link( source_id=source_note.id, target_id=target_note.id, bidirectional=True ) # Verify bidirectional link was removed assert not any( link.target_id == target_note.id for link in source.links ) assert not any( link.target_id == source_note.id for link in target.links ) def test_parsing_link_types_from_markdown(self, note_repository): """Test that link types are correctly parsed from markdown content.""" # Create markdown content with links section markdown_content = """--- id: '20250101120000' title: Markdown Parsing Test tags: test, markdown, parsing --- # Markdown Parsing Test Test content for parsing links from markdown. ## Links - extends [[20250101120001]] Test extends link - contradicts [[20250101120002]] Test contradicts link - supports [[20250101120003]] Test supports link """ # Parse note from markdown note = note_repository._parse_note_from_markdown(markdown_content) # Verify links were parsed with correct types assert len(note.links) == 3 # Verify each link type link_types = {link.target_id: link.link_type for link in note.links} assert link_types["20250101120001"] == LinkType.EXTENDS assert link_types["20250101120002"] == LinkType.CONTRADICTS assert link_types["20250101120003"] == LinkType.SUPPORTS def test_markdown_generation_with_links(self, zettel_service): """Test that markdown is generated with the correct link types.""" # Create test notes with explicit datetime objects rather than mocks original_created_at = datetime.datetime.now() original_updated_at = datetime.datetime.now() source_note = zettel_service.create_note( title="Markdown Generation Source", content="Source note for markdown generation testing", note_type=NoteType.PERMANENT, tags=["test", "markdown-generation"] ) # Explicitly set the datetime attributes to real objects, not mocks source_note.created_at = original_created_at source_note.updated_at = original_updated_at source_note.add_link( target_id="target123", link_type=LinkType.EXTENDS, description="Extends link" ) # Access the repository's _note_to_markdown function markdown = zettel_service.repository._note_to_markdown(source_note) # Verify markdown contains the link with correct type assert "## Links" in markdown assert "- extends [[target123]] Extends link" in markdown def test_link_type_uniqueness_constraint(self, zettel_service): """Test the uniqueness constraint for link types between notes.""" # Create test notes source_note = zettel_service.create_note( title="Uniqueness Source", content="Source note for uniqueness constraint testing", tags=["test", "uniqueness"] ) target_note = zettel_service.create_note( title="Uniqueness Target", content="Target note for uniqueness constraint testing", tags=["test", "uniqueness"] ) # Create a link source, _ = zettel_service.create_link( source_id=source_note.id, target_id=target_note.id, link_type=LinkType.EXTENDS, description="First link" ) # Try to create the same link type again (should be ignored) source, _ = zettel_service.create_link( source_id=source_note.id, target_id=target_note.id, link_type=LinkType.EXTENDS, description="Duplicate link type" ) # Verify only one link of this type exists matching_links = [ link for link in source.links if link.target_id == target_note.id and link.link_type == LinkType.EXTENDS ] assert len(matching_links) == 1 # Description should not be updated since the original link is preserved assert matching_links[0].description == "First link" def test_get_linked_notes_outgoing(self, zettel_service): """Test retrieving outgoing linked notes with correct link types.""" # Create test notes central_note = zettel_service.create_note( title="Central Note", content="Note with multiple outgoing links", note_type=NoteType.HUB, tags=["test", "linked-notes"] ) # Create several target notes target_notes = [] link_types = [ LinkType.EXTENDS, LinkType.REFINES, LinkType.SUPPORTS ] for i, link_type in enumerate(link_types): note = zettel_service.create_note( title=f"Target Note {i+1}", content=f"Target {i+1} content", note_type=NoteType.PERMANENT, tags=["test", "target"] ) target_notes.append(note) # Create link zettel_service.create_link( source_id=central_note.id, target_id=note.id, link_type=link_type, description=f"Testing {link_type.value} relationship" ) # Get outgoing linked notes linked_notes = zettel_service.get_linked_notes(central_note.id, "outgoing") # Verify correct number of linked notes assert len(linked_notes) == len(link_types) # Verify all target notes are included linked_ids = [note.id for note in linked_notes] for target in target_notes: assert target.id in linked_ids # Testing that link types are correctly displayed would require # direct testing of the MCP server output function, which is # handled in the next test # Fix for test_mcp_get_linked_notes_tool def test_mcp_get_linked_notes_tool(self, zettel_service): """Test that zk_get_linked_notes correctly displays semantic link types.""" # Create test notes central_note = zettel_service.create_note( title="MCP Tool Test Central Note", content="Note for testing MCP get linked notes tool", note_type=NoteType.HUB, tags=["test", "mcp-tool"] ) target_note = zettel_service.create_note( title="MCP Tool Test Target Note", content="Target for testing MCP get linked notes tool", note_type=NoteType.PERMANENT, tags=["test", "mcp-tool"] ) # Create link with semantic type zettel_service.create_link( source_id=central_note.id, target_id=target_note.id, link_type=LinkType.EXTENDS, description="Testing extends relationship" ) # Create MCP server server = ZettelkastenMcpServer() server.zettel_service = zettel_service # Access the tool function directly as a method of the server result = server.zk_get_linked_notes( note_id=central_note.id, direction="outgoing" ) # Verify result contains correct link type assert "Found" in result assert "linked notes for " + central_note.id in result assert "Link type: extends" in result assert "Testing extends relationship" in result # Fix for test_mcp_create_link_tool def test_mcp_create_link_tool(self, zettel_service): """Test that zk_create_link correctly creates links with semantic types.""" # Create test notes source_note = zettel_service.create_note( title="MCP Create Link Source", content="Note for testing MCP create link tool", note_type=NoteType.PERMANENT, tags=["test", "mcp-create-link"] ) target_note = zettel_service.create_note( title="MCP Create Link Target", content="Target for testing MCP create link tool", note_type=NoteType.PERMANENT, tags=["test", "mcp-create-link"] ) # Create MCP server with mocked functions server = ZettelkastenMcpServer() server.zettel_service = zettel_service # Use our test zettel_service # Call the tool function directly as a method of the server result = server.zk_create_link( source_id=source_note.id, target_id=target_note.id, link_type="supports", description="Testing supports relationship via MCP tool", bidirectional=True ) # Verify link was created with correct type assert "Bidirectional link created" in result # Verify link through repository updated_source = zettel_service.get_note(source_note.id) updated_target = zettel_service.get_note(target_note.id) # Check source note's links source_links = [link for link in updated_source.links if link.target_id == target_note.id] assert len(source_links) == 1 assert source_links[0].link_type == LinkType.SUPPORTS assert source_links[0].description == "Testing supports relationship via MCP tool" # Check target note's links (inverse relationship) target_links = [link for link in updated_target.links if link.target_id == source_note.id] assert len(target_links) == 1 assert target_links[0].link_type == LinkType.SUPPORTED_BY def test_link_cycle_detection(self, zettel_service): """Test scenarios involving cycles of semantic links.""" # Create a cycle of notes with semantic links note_a = zettel_service.create_note( title="Cycle Note A", content="First note in a cycle", note_type=NoteType.PERMANENT, tags=["test", "cycle"] ) note_b = zettel_service.create_note( title="Cycle Note B", content="Second note in a cycle", note_type=NoteType.PERMANENT, tags=["test", "cycle"] ) note_c = zettel_service.create_note( title="Cycle Note C", content="Third note in a cycle", note_type=NoteType.PERMANENT, tags=["test", "cycle"] ) # Create a cycle: A extends B, B refines C, C contradicts A zettel_service.create_link( source_id=note_a.id, target_id=note_b.id, link_type=LinkType.EXTENDS, description="A extends B" ) zettel_service.create_link( source_id=note_b.id, target_id=note_c.id, link_type=LinkType.REFINES, description="B refines C" ) zettel_service.create_link( source_id=note_c.id, target_id=note_a.id, link_type=LinkType.CONTRADICTS, description="C contradicts A" ) # Verify links were created correctly note_a = zettel_service.get_note(note_a.id) note_b = zettel_service.get_note(note_b.id) note_c = zettel_service.get_note(note_c.id) # Verify A -> B link a_outgoing = [link for link in note_a.links if link.target_id == note_b.id] assert len(a_outgoing) == 1 assert a_outgoing[0].link_type == LinkType.EXTENDS # Verify B -> C link b_outgoing = [link for link in note_b.links if link.target_id == note_c.id] assert len(b_outgoing) == 1 assert b_outgoing[0].link_type == LinkType.REFINES # Verify C -> A link c_outgoing = [link for link in note_c.links if link.target_id == note_a.id] assert len(c_outgoing) == 1 assert c_outgoing[0].link_type == LinkType.CONTRADICTS # Test that we can still find links through the cycle # From A, we should be able to traverse to both B and C a_links = zettel_service.get_linked_notes(note_a.id, "both") a_linked_ids = [note.id for note in a_links] assert note_b.id in a_linked_ids assert note_c.id in a_linked_ids def test_semantic_links_in_search_results(self, zettel_service): """Test that searching finds notes with specific semantic links.""" # Create test notes hub_note = zettel_service.create_note( title="Search Hub Note", content="Hub note for testing search with semantic links", note_type=NoteType.HUB, tags=["test", "search", "hub"] ) linked_note = zettel_service.create_note( title="Linked Search Note", content="Note for testing search with semantic links", note_type=NoteType.PERMANENT, tags=["test", "search", "linked"] ) unlinked_note = zettel_service.create_note( title="Unlinked Search Note", content="Note for testing search with semantic links", note_type=NoteType.PERMANENT, tags=["test", "search", "unlinked"] ) # Create semantic link zettel_service.create_link( source_id=hub_note.id, target_id=linked_note.id, link_type=LinkType.EXTENDS, description="Link for search testing" ) # Create search service from zettelkasten_mcp.services.search_service import SearchService search_service = SearchService(zettel_service) # Search for linked notes linked_notes = zettel_service.get_linked_notes(hub_note.id, "outgoing") assert len(linked_notes) == 1 assert linked_notes[0].id == linked_note.id # Search for notes by tag to verify all test notes are found search_results = zettel_service.get_notes_by_tag("search") assert len(search_results) == 3 def test_find_similar_notes_with_semantic_links(self, zettel_service): """Test similarity detection based on semantic link types.""" # Create test notes source_note = zettel_service.create_note( title="Similar Source Note", content="Source note for testing similarity", note_type=NoteType.PERMANENT, tags=["test", "similarity", "source"] ) # Create target notes with different semantic relationships target1 = zettel_service.create_note( title="Similar Target 1", content="First target for similarity testing", note_type=NoteType.PERMANENT, tags=["test", "similarity", "extends-target"] ) target2 = zettel_service.create_note( title="Similar Target 2", content="Second target for similarity testing", note_type=NoteType.PERMANENT, tags=["test", "similarity", "contradicts-target"] ) target3 = zettel_service.create_note( title="Similar Target 3", content="Third target for similarity testing", note_type=NoteType.PERMANENT, tags=["test", "similarity", "unrelated-target"] ) # Create semantic links zettel_service.create_link( source_id=source_note.id, target_id=target1.id, link_type=LinkType.EXTENDS, description="Source extends Target 1" ) zettel_service.create_link( source_id=source_note.id, target_id=target2.id, link_type=LinkType.CONTRADICTS, description="Source contradicts Target 2" ) # Don't link to target3 at all # Find similar notes similar_notes = zettel_service.find_similar_notes(source_note.id, 0.0) # Convert to IDs for easier comparison similar_ids = [note_tuple[0].id for note_tuple in similar_notes] # Both linked notes should appear in results assert target1.id in similar_ids assert target2.id in similar_ids # The similarity scores might vary, but we should have at least the # two linked notes in the results assert len(similar_notes) >= 2 def test_central_notes_with_semantic_links(self, zettel_service): """Test the find_central_notes function with semantic links.""" from zettelkasten_mcp.services.search_service import SearchService search_service = SearchService(zettel_service) # Create a network of notes with semantic links hub = zettel_service.create_note( title="Central Hub Note", content="Hub note with many connections", note_type=NoteType.HUB, tags=["test", "central", "hub"] ) satellite1 = zettel_service.create_note( title="Satellite Note 1", content="First satellite note", note_type=NoteType.PERMANENT, tags=["test", "central", "satellite"] ) satellite2 = zettel_service.create_note( title="Satellite Note 2", content="Second satellite note", note_type=NoteType.PERMANENT, tags=["test", "central", "satellite"] ) satellite3 = zettel_service.create_note( title="Satellite Note 3", content="Third satellite note", note_type=NoteType.PERMANENT, tags=["test", "central", "satellite"] ) orphan = zettel_service.create_note( title="Orphan Note", content="Note with no connections", note_type=NoteType.PERMANENT, tags=["test", "central", "orphan"] ) # Create links with different semantic types zettel_service.create_link( source_id=hub.id, target_id=satellite1.id, link_type=LinkType.EXTENDS, description="Hub extends Satellite 1" ) zettel_service.create_link( source_id=hub.id, target_id=satellite2.id, link_type=LinkType.CONTRADICTS, description="Hub contradicts Satellite 2" ) zettel_service.create_link( source_id=hub.id, target_id=satellite3.id, link_type=LinkType.SUPPORTS, description="Hub supports Satellite 3" ) # Also create links between satellites zettel_service.create_link( source_id=satellite1.id, target_id=satellite2.id, link_type=LinkType.REFINES, description="Satellite 1 refines Satellite 2" ) # Find central notes central_notes = search_service.find_central_notes(limit=10) # Convert to a dictionary of ID to connection count central_dict = {note.id: count for note, count in central_notes} # Hub should have the most connections assert hub.id in central_dict assert central_dict[hub.id] >= 3 # Hub has 3 outgoing links # Check positions hub_position = None satellite1_position = None for i, (note, _) in enumerate(central_notes): if note.id == hub.id: hub_position = i elif note.id == satellite1.id: satellite1_position = i assert hub_position is not None if satellite1_position is not None: assert hub_position < satellite1_position # Hub should be ranked higher # Orphan should not be in central notes assert orphan.id not in central_dict

Latest Blog Posts

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/Liam-Deacon/zettelkasten-mcp'

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