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
This commit is contained in:
@@ -0,0 +1,458 @@
|
||||
"""
|
||||
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
|
||||
Reference in New Issue
Block a user