Files
journal_organizer/tests/integration/test_obsidian_skills.py
T
windyboy f7e54692a9 Initial project setup: Obsidian intelligent journal organizer
- Add core agent architecture with Command + Skill pattern
- Implement Claude API integration for content analysis
- Add Obsidian REST API integration for vault operations
- Create conversational interface (v2.0) with natural language processing
- Add comprehensive configuration management and validation
- Include project documentation and developer guides
- Set up testing framework with unit, integration, and property tests
- Add Kiro specs for Claude API configuration and code quality improvements
- Configure project steering files for development guidelines
2025-12-31 17:55:10 +08:00

458 lines
16 KiB
Python

"""
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