""" Integration tests for Obsidian Skills. Tests Skills with mocked API responses to verify end-to-end functionality. """ import pytest import json from unittest.mock import AsyncMock, patch, Mock from aioresponses import aioresponses from agent_core import CommandContext, SkillResult from skills.obsidian_skill import ( ObsidianReadSkill, ObsidianWriteSkill, ObsidianAppendSkill, ObsidianListFilesSkill ) class TestObsidianReadSkill: """Integration tests for ObsidianReadSkill""" @pytest.fixture def skill(self): """Create ObsidianReadSkill instance""" return ObsidianReadSkill() @pytest.fixture def context(self, sample_config): """Create command context with Obsidian configuration""" return CommandContext( command_name="test_read", args={"file_path": "Daily/2024-01-15.md"}, config=sample_config ) @pytest.mark.asyncio async def test_read_note_success(self, skill, context): """Test successful note reading""" mock_response = { "content": "# 2024-01-15 Daily Journal\n\nTest content", "stat": { "ctime": 1642204800000, "mtime": 1642204800000, "size": 45 } } with aioresponses() as m: m.get( "https://localhost:27123/vault/Daily/2024-01-15.md", payload=mock_response, status=200 ) result = await skill.execute(context) assert result.success is True assert result.data["content"] == mock_response["content"] assert result.data["file_path"] == "Daily/2024-01-15.md" assert "stat" in result.data @pytest.mark.asyncio async def test_read_note_not_found(self, skill, context): """Test reading non-existent note""" with aioresponses() as m: m.get( "https://localhost:27123/vault/Daily/2024-01-15.md", status=404, payload={"error": "File not found"} ) result = await skill.execute(context) assert result.success is False assert "not found" in result.error.lower() @pytest.mark.asyncio async def test_read_note_api_error(self, skill, context): """Test API connection error""" with aioresponses() as m: m.get( "https://localhost:27123/vault/Daily/2024-01-15.md", exception=Exception("Connection failed") ) result = await skill.execute(context) assert result.success is False assert "connection" in result.error.lower() or "api" in result.error.lower() @pytest.mark.asyncio async def test_read_note_invalid_response(self, skill, context): """Test handling of invalid API response""" with aioresponses() as m: m.get( "https://localhost:27123/vault/Daily/2024-01-15.md", payload="invalid json response", status=200 ) result = await skill.execute(context) assert result.success is False assert "response" in result.error.lower() class TestObsidianWriteSkill: """Integration tests for ObsidianWriteSkill""" @pytest.fixture def skill(self): """Create ObsidianWriteSkill instance""" return ObsidianWriteSkill() @pytest.fixture def context(self, sample_config): """Create command context with write parameters""" return CommandContext( command_name="test_write", args={ "file_path": "Knowledge/Experiences/test-experience.md", "content": "# Test Experience\n\nThis is a test experience note." }, config=sample_config ) @pytest.mark.asyncio async def test_write_note_success(self, skill, context): """Test successful note writing""" mock_response = { "path": "Knowledge/Experiences/test-experience.md", "stat": { "ctime": 1642204800000, "mtime": 1642204800000, "size": 45 } } with aioresponses() as m: m.put( "https://localhost:27123/vault/Knowledge/Experiences/test-experience.md", payload=mock_response, status=200 ) result = await skill.execute(context) assert result.success is True assert result.data["file_path"] == "Knowledge/Experiences/test-experience.md" assert "created" in result.message.lower() or "written" in result.message.lower() @pytest.mark.asyncio async def test_write_note_permission_error(self, skill, context): """Test write permission error""" with aioresponses() as m: m.put( "https://localhost:27123/vault/Knowledge/Experiences/test-experience.md", status=403, payload={"error": "Permission denied"} ) result = await skill.execute(context) assert result.success is False assert "permission" in result.error.lower() @pytest.mark.asyncio async def test_write_note_missing_content(self, skill, sample_config): """Test writing note without content""" context = CommandContext( command_name="test_write", args={"file_path": "test.md"}, # Missing content config=sample_config ) result = await skill.execute(context) assert result.success is False assert "content" in result.error.lower() class TestObsidianAppendSkill: """Integration tests for ObsidianAppendSkill""" @pytest.fixture def skill(self): """Create ObsidianAppendSkill instance""" return ObsidianAppendSkill() @pytest.fixture def context(self, sample_config): """Create command context with append parameters""" return CommandContext( command_name="test_append", args={ "file_path": "Daily/2024-01-15.md", "content": "\n\n## Additional Notes\n\nAppended content." }, config=sample_config ) @pytest.mark.asyncio async def test_append_to_existing_note_success(self, skill, context): """Test successful content appending to existing note""" # Mock reading existing content existing_content = "# 2024-01-15 Daily Journal\n\nExisting content" read_response = { "content": existing_content, "stat": {"ctime": 1642204800000, "mtime": 1642204800000, "size": 45} } # Mock writing updated content write_response = { "path": "Daily/2024-01-15.md", "stat": {"ctime": 1642204800000, "mtime": 1642204900000, "size": 90} } with aioresponses() as m: # Mock GET request for reading existing content m.get( "https://localhost:27123/vault/Daily/2024-01-15.md", payload=read_response, status=200 ) # Mock PUT request for writing updated content m.put( "https://localhost:27123/vault/Daily/2024-01-15.md", payload=write_response, status=200 ) result = await skill.execute(context) assert result.success is True assert result.data["file_path"] == "Daily/2024-01-15.md" assert "appended" in result.message.lower() @pytest.mark.asyncio async def test_append_to_nonexistent_note(self, skill, context): """Test appending to non-existent note (should create new note)""" write_response = { "path": "Daily/2024-01-15.md", "stat": {"ctime": 1642204800000, "mtime": 1642204800000, "size": 45} } with aioresponses() as m: # Mock GET request returning 404 (file doesn't exist) m.get( "https://localhost:27123/vault/Daily/2024-01-15.md", status=404 ) # Mock PUT request for creating new file m.put( "https://localhost:27123/vault/Daily/2024-01-15.md", payload=write_response, status=200 ) result = await skill.execute(context) assert result.success is True assert "created" in result.message.lower() class TestObsidianListFilesSkill: """Integration tests for ObsidianListFilesSkill""" @pytest.fixture def skill(self): """Create ObsidianListFilesSkill instance""" return ObsidianListFilesSkill() @pytest.fixture def context(self, sample_config): """Create command context with list parameters""" return CommandContext( command_name="test_list", args={"folder_path": "Daily"}, config=sample_config ) @pytest.mark.asyncio async def test_list_files_success(self, skill, context): """Test successful file listing""" mock_response = { "files": [ { "path": "Daily/2024-01-15.md", "name": "2024-01-15.md", "stat": {"ctime": 1642204800000, "mtime": 1642204800000, "size": 45} }, { "path": "Daily/2024-01-14.md", "name": "2024-01-14.md", "stat": {"ctime": 1642118400000, "mtime": 1642118400000, "size": 38} } ] } with aioresponses() as m: m.get( "https://localhost:27123/vault/Daily/", payload=mock_response, status=200 ) result = await skill.execute(context) assert result.success is True assert len(result.data["files"]) == 2 assert result.data["folder_path"] == "Daily" assert any(file["name"] == "2024-01-15.md" for file in result.data["files"]) @pytest.mark.asyncio async def test_list_files_empty_folder(self, skill, context): """Test listing files in empty folder""" mock_response = {"files": []} with aioresponses() as m: m.get( "https://localhost:27123/vault/Daily/", payload=mock_response, status=200 ) result = await skill.execute(context) assert result.success is True assert len(result.data["files"]) == 0 @pytest.mark.asyncio async def test_list_files_folder_not_found(self, skill, context): """Test listing files in non-existent folder""" with aioresponses() as m: m.get( "https://localhost:27123/vault/Daily/", status=404, payload={"error": "Folder not found"} ) result = await skill.execute(context) assert result.success is False assert "not found" in result.error.lower() class TestObsidianSkillsIntegration: """Integration tests combining multiple Obsidian skills""" @pytest.mark.asyncio async def test_read_write_workflow(self, sample_config): """Test complete read-modify-write workflow""" read_skill = ObsidianReadSkill() write_skill = ObsidianWriteSkill() # Read existing content read_context = CommandContext( command_name="read", args={"file_path": "Daily/2024-01-15.md"}, config=sample_config ) # Write modified content write_context = CommandContext( command_name="write", args={ "file_path": "Knowledge/Processed/2024-01-15-summary.md", "content": "# Summary\n\nProcessed content from daily journal." }, config=sample_config ) with aioresponses() as m: # Mock read response m.get( "https://localhost:27123/vault/Daily/2024-01-15.md", payload={ "content": "# 2024-01-15 Daily Journal\n\nOriginal content", "stat": {"ctime": 1642204800000, "mtime": 1642204800000, "size": 45} }, status=200 ) # Mock write response m.put( "https://localhost:27123/vault/Knowledge/Processed/2024-01-15-summary.md", payload={ "path": "Knowledge/Processed/2024-01-15-summary.md", "stat": {"ctime": 1642204900000, "mtime": 1642204900000, "size": 60} }, status=200 ) # Execute read read_result = await read_skill.execute(read_context) assert read_result.success is True # Execute write (in real scenario, content would be processed) write_result = await write_skill.execute(write_context) assert write_result.success is True # Verify workflow completed successfully assert read_result.data["content"] is not None assert write_result.data["file_path"] == "Knowledge/Processed/2024-01-15-summary.md" @pytest.mark.asyncio async def test_list_and_read_multiple_files(self, sample_config): """Test listing files and reading multiple files""" list_skill = ObsidianListFilesSkill() read_skill = ObsidianReadSkill() list_context = CommandContext( command_name="list", args={"folder_path": "Daily"}, config=sample_config ) with aioresponses() as m: # Mock list response m.get( "https://localhost:27123/vault/Daily/", payload={ "files": [ {"path": "Daily/2024-01-15.md", "name": "2024-01-15.md"}, {"path": "Daily/2024-01-14.md", "name": "2024-01-14.md"} ] }, status=200 ) # Mock read responses for each file m.get( "https://localhost:27123/vault/Daily/2024-01-15.md", payload={"content": "Content 1", "stat": {}}, status=200 ) m.get( "https://localhost:27123/vault/Daily/2024-01-14.md", payload={"content": "Content 2", "stat": {}}, status=200 ) # List files list_result = await list_skill.execute(list_context) assert list_result.success is True assert len(list_result.data["files"]) == 2 # Read each file for file_info in list_result.data["files"]: read_context = CommandContext( command_name="read", args={"file_path": file_info["path"]}, config=sample_config ) read_result = await read_skill.execute(read_context) assert read_result.success is True assert read_result.data["content"] is not None