Skip to main content
Glama

Obsidian MCP Server

by suhailnajeeb
test_auto_search.py9.74 kB
"""Tests for auto-search functionality in rename_note and move_note.""" import pytest import asyncio from unittest.mock import AsyncMock, MagicMock, patch from obsidian_mcp.tools.organization import rename_note, move_note from obsidian_mcp.models import Note, NoteMetadata @pytest.fixture def mock_vault(): """Create a mock vault for testing.""" vault = AsyncMock() return vault @pytest.fixture def mock_get_vault(mock_vault): """Patch get_vault to return our mock.""" with patch('obsidian_mcp.tools.organization.get_vault', return_value=mock_vault): with patch('obsidian_mcp.tools.link_management.get_vault', return_value=mock_vault): yield mock_vault @pytest.mark.asyncio async def test_rename_note_auto_search_single_match(mock_get_vault): """Test rename_note auto-searches when exact path not found and finds single match.""" vault = mock_get_vault # Setup mock note note = Note( path="Projects/My Note.md", content="# My Note\n\nContent here.", metadata=NoteMetadata() ) # Mock search_notes to return single result with patch('obsidian_mcp.tools.organization.search_notes') as mock_search: mock_search.return_value = { "results": [{"path": "Projects/My Note.md"}], "count": 1 } # First read fails, second succeeds after search vault.read_note.side_effect = [ FileNotFoundError(), # Initial path not found note, # Found after search FileNotFoundError() # Destination doesn't exist ] vault.write_note.return_value = None vault.delete_note.return_value = None # Test rename with just filename result = await rename_note( "My Note.md", "New Note.md", update_links=False ) assert result["success"] is True assert result["old_path"] == "Projects/My Note.md" assert result["new_path"] == "Projects/New Note.md" # Verify search was called mock_search.assert_called_once_with("path:My Note.md", max_results=10, ctx=None) @pytest.mark.asyncio async def test_rename_note_auto_search_multiple_matches(mock_get_vault): """Test rename_note shows helpful error when multiple matches found.""" vault = mock_get_vault # Mock search_notes to return multiple results with patch('obsidian_mcp.tools.organization.search_notes') as mock_search: mock_search.return_value = { "results": [ {"path": "Projects/My Note.md"}, {"path": "Archive/My Note.md"}, {"path": "Daily/My Note.md"} ], "count": 3 } vault.read_note.side_effect = FileNotFoundError() # Test rename with just filename with pytest.raises(ValueError) as exc_info: await rename_note("My Note.md", "New Note.md") error_msg = str(exc_info.value) assert "Multiple notes found with name 'My Note.md'" in error_msg assert "Projects/My Note.md" in error_msg assert "Archive/My Note.md" in error_msg assert "Daily/My Note.md" in error_msg @pytest.mark.asyncio async def test_rename_note_auto_search_no_matches(mock_get_vault): """Test rename_note fails appropriately when no matches found.""" vault = mock_get_vault # Mock search_notes to return no results with patch('obsidian_mcp.tools.organization.search_notes') as mock_search: mock_search.return_value = { "results": [], "count": 0 } vault.read_note.side_effect = FileNotFoundError() # Test rename with just filename with pytest.raises(FileNotFoundError) as exc_info: await rename_note("NonExistent.md", "New Note.md") assert "Note not found" in str(exc_info.value) @pytest.mark.asyncio async def test_move_note_auto_search_single_match(mock_get_vault): """Test move_note auto-searches when source not found and finds single match.""" vault = mock_get_vault # Setup mock note note = Note( path="Inbox/Todo.md", content="# Todo List\n\nItems here.", metadata=NoteMetadata() ) # Mock search_notes with patch('obsidian_mcp.tools.organization.search_notes') as mock_search: mock_search.return_value = { "results": [{"path": "Inbox/Todo.md"}], "count": 1 } vault.read_note.side_effect = [ FileNotFoundError(), # Initial path not found note, # Found after search FileNotFoundError() # Destination doesn't exist ] vault.write_note.return_value = None vault.delete_note.return_value = None # Test move with just filename result = await move_note( "Todo.md", "Archive/Todo.md" ) assert result["success"] is True assert result["source"] == "Inbox/Todo.md" assert result["destination"] == "Archive/Todo.md" # Verify operations vault.write_note.assert_called_once_with("Archive/Todo.md", note.content, overwrite=False) vault.delete_note.assert_called_once_with("Inbox/Todo.md") @pytest.mark.asyncio async def test_move_note_auto_search_with_rename(mock_get_vault): """Test move_note with auto-search when also renaming.""" vault = mock_get_vault # Setup mock note note = Note( path="Inbox/Draft.md", content="# Draft\n\nDraft content.", metadata=NoteMetadata() ) linking_note = Note( path="Index.md", content="Check out [[Draft]] for details.", metadata=NoteMetadata() ) # Mock search_notes and get_backlinks with patch('obsidian_mcp.tools.organization.search_notes') as mock_search: with patch('obsidian_mcp.tools.organization.get_backlinks') as mock_backlinks: mock_search.return_value = { "results": [{"path": "Inbox/Draft.md"}], "count": 1 } mock_backlinks.return_value = { 'findings': [ {'source_path': 'Index.md', 'link_text': 'Draft', 'link_type': 'wiki'} ] } vault.read_note.side_effect = [ FileNotFoundError(), # Initial path not found note, # Found after search FileNotFoundError(), # Destination doesn't exist linking_note # For link update ] vault.write_note.return_value = None vault.delete_note.return_value = None # Test move with rename using just filename result = await move_note( "Draft.md", "Projects/Final Document.md", update_links=True ) assert result["success"] is True assert result["source"] == "Inbox/Draft.md" assert result["destination"] == "Projects/Final Document.md" assert result["renamed"] is True assert result["details"]["links_updated"] == 1 @pytest.mark.asyncio async def test_auto_search_preserves_directory_context(mock_get_vault): """Test that auto-search maintains the found directory when renaming.""" vault = mock_get_vault # Note in a nested directory note = Note( path="Projects/2024/Q1/Sprint Planning.md", content="# Sprint Planning", metadata=NoteMetadata() ) with patch('obsidian_mcp.tools.organization.search_notes') as mock_search: mock_search.return_value = { "results": [{"path": "Projects/2024/Q1/Sprint Planning.md"}], "count": 1 } vault.read_note.side_effect = [ FileNotFoundError(), note, FileNotFoundError() ] vault.write_note.return_value = None vault.delete_note.return_value = None # Rename with just filename result = await rename_note( "Sprint Planning.md", "Q1 Retrospective.md", update_links=False ) # Should preserve the found directory assert result["new_path"] == "Projects/2024/Q1/Q1 Retrospective.md" @pytest.mark.asyncio async def test_auto_search_with_context_logging(mock_get_vault): """Test that auto-search logs helpful context messages.""" vault = mock_get_vault ctx = MagicMock() note = Note( path="Daily/2024-01-15.md", content="Daily note", metadata=NoteMetadata() ) with patch('obsidian_mcp.tools.organization.search_notes') as mock_search: mock_search.return_value = { "results": [{"path": "Daily/2024-01-15.md"}], "count": 1 } vault.read_note.side_effect = [ FileNotFoundError(), note, FileNotFoundError() ] vault.write_note.return_value = None vault.delete_note.return_value = None await rename_note( "2024-01-15.md", "2024-01-15-reviewed.md", update_links=False, ctx=ctx ) # Check that helpful messages were logged ctx.info.assert_any_call("Note not found at 2024-01-15.md, searching for filename...") ctx.info.assert_any_call("Found unique match at: Daily/2024-01-15.md") if __name__ == "__main__": pytest.main([__file__, "-v"])

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/suhailnajeeb/obsidian-mcp'

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