Files
journal_organizer/tests/integration/test_organize_command.py
T

488 lines
19 KiB
Python
Raw Normal View History

"""
Integration tests for OrganizeCommand.
Tests Commands with full skill chains 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, Agent
from commands.organize_command import OrganizeCommand
class TestOrganizeCommand:
"""Integration tests for OrganizeCommand"""
@pytest.fixture
def command(self):
"""Create OrganizeCommand instance"""
return OrganizeCommand()
@pytest.fixture
def context(self, sample_config):
"""Create command context for organize command"""
return CommandContext(
command_name="organize",
args={
"date": "2024-01-15",
"vault_path": sample_config["obsidian"]["vault_path"],
"daily_folder": "Daily"
},
config=sample_config
)
@pytest.fixture
def sample_journal_content(self):
"""Sample journal content for testing"""
return """# 2024-01-15 Daily Journal
## 今天的经历
- 完成了项目的重要里程碑,团队协作非常顺利
- 与客户进行了产品演示,获得了积极反馈
- 参加了技术分享会议,学到了新的架构模式
## 学到的东西
- 学会了新的Python异步编程技巧,提升了代码效率
- 理解了微服务架构的最佳实践
- 掌握了更好的错误处理和日志记录模式
## 遇到的问题
- API调用偶尔超时,影响用户体验
- 配置文件格式需要改进,当前格式不够灵活
- 团队沟通中存在信息不对称问题
## 今天的成就
- 成功部署了新版本到生产环境
- 解决了困扰团队一周的性能问题
- 获得了客户的正面评价
## 明天的计划
- 优化API调用的重试机制
- 重构配置管理模块
- 组织团队同步会议
"""
@pytest.mark.asyncio
async def test_organize_command_full_workflow_success(self, command, context, sample_journal_content):
"""Test complete organize command workflow with all skills"""
# Mock Claude API responses
analyze_response = {
"experiences": [
{
"title": "项目里程碑完成",
"description": "成功完成了项目的重要里程碑,团队协作非常顺利",
"category": "项目管理",
"importance": "high"
},
{
"title": "客户产品演示",
"description": "与客户进行了产品演示,获得了积极反馈",
"category": "客户关系",
"importance": "high"
}
],
"lessons": [
{
"title": "Python异步编程技巧",
"description": "学会了新的Python异步编程技巧,提升了代码效率",
"category": "技术学习",
"application": "可以应用到当前项目的API调用优化中"
},
{
"title": "微服务架构最佳实践",
"description": "理解了微服务架构的最佳实践",
"category": "架构设计",
"application": "用于指导下一个项目的架构设计"
}
],
"problems": [
{
"title": "API调用超时",
"description": "API调用偶尔超时,影响用户体验",
"severity": "medium",
"suggested_solution": "实现重试机制和超时处理"
}
],
"achievements": [
{
"title": "生产环境部署",
"description": "成功部署了新版本到生产环境",
"impact": "提升了系统稳定性和性能"
}
]
}
# Mock transformation responses
experience_note = """# 项目里程碑完成
## 经验描述
成功完成了项目的重要里程碑,团队协作非常顺利。这次经历展现了良好的项目管理能力和团队协作精神。
## 关键要点
- 项目管理技能得到提升
- 团队协作效率显著改善
- 里程碑按时完成
## 应用场景
这个经验可以应用到未来的项目管理中,特别是在设定和跟踪项目里程碑方面。
## 相关标签
#项目管理 #里程碑 #团队协作
---
*创建时间: 2024-01-15*
*来源: [[Daily/2024-01-15]]*
"""
lesson_note = """# Python异步编程技巧
## 学习内容
学会了新的Python异步编程技巧,提升了代码效率。
## 关键概念
- 异步编程模式
- 性能优化技巧
- 代码效率提升
## 实际应用
可以应用到当前项目的API调用优化中,提升系统响应速度。
## 相关标签
#技术学习 #Python #异步编程
---
*创建时间: 2024-01-15*
*来源: [[Daily/2024-01-15]]*
"""
with aioresponses() as m:
# Mock Obsidian API calls
# 1. Read daily journal
m.get(
"https://localhost:27123/vault/Daily/2024-01-15.md",
payload={
"content": sample_journal_content,
"stat": {"ctime": 1642204800000, "mtime": 1642204800000, "size": len(sample_journal_content)}
},
status=200
)
# 2. Write experience note
m.put(
"https://localhost:27123/vault/Knowledge/Experiences/项目里程碑完成.md",
payload={
"path": "Knowledge/Experiences/项目里程碑完成.md",
"stat": {"ctime": 1642204900000, "mtime": 1642204900000, "size": len(experience_note)}
},
status=200
)
# 3. Write lesson note
m.put(
"https://localhost:27123/vault/Knowledge/Lessons/Python异步编程技巧.md",
payload={
"path": "Knowledge/Lessons/Python异步编程技巧.md",
"stat": {"ctime": 1642204900000, "mtime": 1642204900000, "size": len(lesson_note)}
},
status=200
)
# 4. Additional notes for other categories (problems, achievements)
m.put(
"https://localhost:27123/vault/Knowledge/Problems/API调用超时.md",
payload={"path": "Knowledge/Problems/API调用超时.md", "stat": {}},
status=200
)
m.put(
"https://localhost:27123/vault/Knowledge/Achievements/生产环境部署.md",
payload={"path": "Knowledge/Achievements/生产环境部署.md", "stat": {}},
status=200
)
# Mock Claude API
with patch('skills.claude_skill.anthropic') as mock_anthropic:
mock_client = Mock()
mock_anthropic.Anthropic.return_value = mock_client
# Mock analyze response
mock_analyze_message = Mock()
mock_analyze_message.content = [Mock(text=json.dumps(analyze_response, ensure_ascii=False))]
# Mock transform responses (one for each item to be transformed)
mock_transform_responses = [
Mock(content=[Mock(text=experience_note)]),
Mock(content=[Mock(text=experience_note)]), # Second experience
Mock(content=[Mock(text=lesson_note)]),
Mock(content=[Mock(text=lesson_note)]), # Second lesson
Mock(content=[Mock(text="# API调用超时\n\n问题描述...")]), # Problem note
Mock(content=[Mock(text="# 生产环境部署\n\n成就描述...")]) # Achievement note
]
# Set up responses: first analyze, then multiple transforms
mock_client.messages.create.side_effect = [mock_analyze_message] + mock_transform_responses
# Execute the organize command
result = await command.execute(context)
# Verify overall success
assert result.success is True
assert "organized successfully" in result.message.lower() or "completed" in result.message.lower()
# Verify data structure
assert "summary" in result.data
assert "created_notes" in result.data
# Verify created notes
created_notes = result.data["created_notes"]
assert len(created_notes) > 0
# Should have created notes for experiences, lessons, problems, achievements
note_types = [note.get("type") for note in created_notes]
expected_types = ["experience", "lesson", "problem", "achievement"]
for expected_type in expected_types:
assert any(expected_type in note_type for note_type in note_types if note_type)
# Verify API calls were made
assert mock_client.messages.create.call_count >= 2 # At least analyze + some transforms
@pytest.mark.asyncio
async def test_organize_command_journal_not_found(self, command, context):
"""Test organize command when daily journal doesn't exist"""
with aioresponses() as m:
# Mock journal file not found
m.get(
"https://localhost:27123/vault/Daily/2024-01-15.md",
status=404,
payload={"error": "File not found"}
)
result = await command.execute(context)
assert result.success is False
assert "not found" in result.error.lower() or "missing" in result.error.lower()
@pytest.mark.asyncio
async def test_organize_command_claude_api_error(self, command, context, sample_journal_content):
"""Test organize command when Claude API fails"""
with aioresponses() as m:
# Mock successful journal read
m.get(
"https://localhost:27123/vault/Daily/2024-01-15.md",
payload={
"content": sample_journal_content,
"stat": {"ctime": 1642204800000, "mtime": 1642204800000, "size": len(sample_journal_content)}
},
status=200
)
# Mock Claude API error
with patch('skills.claude_skill.anthropic') as mock_anthropic:
mock_client = Mock()
mock_anthropic.Anthropic.return_value = mock_client
mock_client.messages.create.side_effect = Exception("Claude API rate limit exceeded")
result = await command.execute(context)
assert result.success is False
assert "api" in result.error.lower() or "claude" in result.error.lower()
@pytest.mark.asyncio
async def test_organize_command_partial_success(self, command, context, sample_journal_content):
"""Test organize command with partial success (some notes created, some failed)"""
analyze_response = {
"experiences": [
{
"title": "Test Experience",
"description": "Test description",
"category": "Test",
"importance": "medium"
}
],
"lessons": [],
"problems": [],
"achievements": []
}
with aioresponses() as m:
# Mock successful journal read
m.get(
"https://localhost:27123/vault/Daily/2024-01-15.md",
payload={
"content": sample_journal_content,
"stat": {"ctime": 1642204800000, "mtime": 1642204800000, "size": len(sample_journal_content)}
},
status=200
)
# Mock successful experience note creation
m.put(
"https://localhost:27123/vault/Knowledge/Experiences/Test Experience.md",
payload={
"path": "Knowledge/Experiences/Test Experience.md",
"stat": {"ctime": 1642204900000, "mtime": 1642204900000, "size": 100}
},
status=200
)
# Mock Claude API
with patch('skills.claude_skill.anthropic') as mock_anthropic:
mock_client = Mock()
mock_anthropic.Anthropic.return_value = mock_client
# Mock analyze response
mock_analyze_message = Mock()
mock_analyze_message.content = [Mock(text=json.dumps(analyze_response, ensure_ascii=False))]
# Mock transform response
mock_transform_message = Mock()
mock_transform_message.content = [Mock(text="# Test Experience\n\nTransformed content")]
mock_client.messages.create.side_effect = [mock_analyze_message, mock_transform_message]
result = await command.execute(context)
# Should succeed even with minimal content
assert result.success is True
assert len(result.data["created_notes"]) >= 1
@pytest.mark.asyncio
async def test_organize_command_empty_journal(self, command, context):
"""Test organize command with empty journal content"""
with aioresponses() as m:
# Mock journal with empty content
m.get(
"https://localhost:27123/vault/Daily/2024-01-15.md",
payload={
"content": "",
"stat": {"ctime": 1642204800000, "mtime": 1642204800000, "size": 0}
},
status=200
)
result = await command.execute(context)
assert result.success is False
assert "empty" in result.error.lower() or "content" in result.error.lower()
class TestOrganizeCommandWithAgent:
"""Integration tests for OrganizeCommand within Agent context"""
@pytest.fixture
def agent(self, sample_config):
"""Create Agent with OrganizeCommand registered"""
agent = Agent("test_agent", sample_config)
agent.register_command(OrganizeCommand())
return agent
@pytest.mark.asyncio
async def test_agent_execute_organize_command(self, agent, sample_journal_content):
"""Test executing organize command through Agent"""
with aioresponses() as m:
# Mock Obsidian API
m.get(
"https://localhost:27123/vault/Daily/2024-01-15.md",
payload={
"content": sample_journal_content,
"stat": {"ctime": 1642204800000, "mtime": 1642204800000, "size": len(sample_journal_content)}
},
status=200
)
# Mock note creation (simplified - just one note)
m.put(
"https://localhost:27123/vault/Knowledge/Experiences/Test.md",
payload={"path": "Knowledge/Experiences/Test.md", "stat": {}},
status=200
)
# Mock Claude API
with patch('skills.claude_skill.anthropic') as mock_anthropic:
mock_client = Mock()
mock_anthropic.Anthropic.return_value = mock_client
# Minimal response for testing
analyze_response = {
"experiences": [{"title": "Test", "description": "Test", "category": "Test"}],
"lessons": [],
"problems": [],
"achievements": []
}
mock_analyze_message = Mock()
mock_analyze_message.content = [Mock(text=json.dumps(analyze_response, ensure_ascii=False))]
mock_transform_message = Mock()
mock_transform_message.content = [Mock(text="# Test\n\nTest content")]
mock_client.messages.create.side_effect = [mock_analyze_message, mock_transform_message]
# Execute command through agent
result = await agent.execute_command(
"organize",
args={"date": "2024-01-15"},
options={"verbose": True}
)
assert result.success is True
assert result.data is not None
@pytest.mark.asyncio
async def test_agent_execute_organize_by_alias(self, agent, sample_journal_content):
"""Test executing organize command by alias through Agent"""
with aioresponses() as m:
# Mock minimal successful workflow
m.get(
"https://localhost:27123/vault/Daily/2024-01-15.md",
payload={
"content": sample_journal_content,
"stat": {"ctime": 1642204800000, "mtime": 1642204800000, "size": len(sample_journal_content)}
},
status=200
)
# Mock Claude API with minimal response
with patch('skills.claude_skill.anthropic') as mock_anthropic:
mock_client = Mock()
mock_anthropic.Anthropic.return_value = mock_client
analyze_response = {"experiences": [], "lessons": [], "problems": [], "achievements": []}
mock_message = Mock()
mock_message.content = [Mock(text=json.dumps(analyze_response))]
mock_client.messages.create.return_value = mock_message
# Execute by alias
result = await agent.execute_command("org", args={"date": "2024-01-15"})
assert result.success is True
@pytest.mark.asyncio
async def test_agent_command_info(self, agent):
"""Test getting command information through Agent"""
commands_info = agent.get_commands_info()
assert "organize" in commands_info["commands"]
organize_info = commands_info["commands"]["organize"]
assert organize_info["name"] == "organize"
assert organize_info["description"] == "分析和整理日记内容,提取经验和要点"
assert "org" in organize_info["aliases"]
assert "organize-journal" in organize_info["aliases"]
# Verify skills are registered
assert len(organize_info["skills"]) > 0
skill_names = list(organize_info["skills"].keys())
expected_skills = ["obsidian_read", "obsidian_write", "obsidian_append", "claude_analyze", "claude_transform"]
for expected_skill in expected_skills:
assert any(expected_skill in skill_name for skill_name in skill_names)