Files
deepagent/tests/researcher/test_integration.py
HyunjunJeon 6f01c834ba feat: Deep Research Agent 확장 - Ralph Loop, 깊이 설정, 테스트 스위트 추가
- research_agent/tools.py: 한글 Docstring 및 ASCII 흐름도 추가
- research_agent/researcher/depth.py: ResearchDepth enum 및 DepthConfig 추가
- research_agent/researcher/ralph_loop.py: Ralph Loop 반복 연구 패턴 구현
- research_agent/researcher/runner.py: 연구 실행기 (CLI 지원)
- tests/researcher/: 91개 테스트 (실제 API 호출 포함)
- scripts/run_ai_trend_research.py: AI 트렌드 연구 스크립트 + 도구 궤적 로깅
2026-01-12 15:49:43 +09:00

315 lines
12 KiB
Python

"""E2E 통합 테스트 - ResearchRunner 전체 플로우 검증."""
import asyncio
import tempfile
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from research_agent.researcher.depth import ResearchDepth
from research_agent.researcher.ralph_loop import Finding, ResearchSession
from research_agent.researcher.runner import ResearchRunner
class TestE2EResearchFlow:
"""전체 연구 플로우 E2E 테스트."""
@pytest.fixture
def temp_workspace(self):
"""임시 워크스페이스 생성."""
with tempfile.TemporaryDirectory() as tmpdir:
yield Path(tmpdir)
@pytest.fixture
def mock_agent_response_incomplete(self):
"""완료되지 않은 에이전트 응답."""
return {
"messages": [
MagicMock(
content="I found some information about the topic. "
"Still need to investigate more aspects."
)
]
}
@pytest.fixture
def mock_agent_response_complete(self):
"""완료된 에이전트 응답."""
return {
"messages": [
MagicMock(
content="Research is comprehensive. "
"<promise>RESEARCH_COMPLETE</promise>"
)
]
}
def test_runner_initialization_creates_session(self, temp_workspace):
"""Runner 초기화 시 세션이 생성되는지 확인."""
with patch.object(ResearchSession, "WORKSPACE", temp_workspace):
runner = ResearchRunner("Test query", depth="quick")
assert runner.session is not None
assert runner.query == "Test query"
assert runner.depth == ResearchDepth.QUICK
def test_session_initialization_creates_files(self, temp_workspace):
"""세션 초기화 시 필요한 파일들이 생성되는지 확인."""
with patch.object(ResearchSession, "WORKSPACE", temp_workspace):
runner = ResearchRunner("Test query", depth="quick")
runner.session.initialize()
assert runner.session.session_dir.exists()
assert (runner.session.session_dir / "TODO.md").exists()
assert (runner.session.session_dir / "FINDINGS.md").exists()
todo_content = (runner.session.session_dir / "TODO.md").read_text()
assert "Test query" in todo_content
def test_iteration_prompt_contains_query(self, temp_workspace):
"""반복 프롬프트에 쿼리가 포함되는지 확인."""
with patch.object(ResearchSession, "WORKSPACE", temp_workspace):
runner = ResearchRunner("Context Engineering 분석", depth="deep")
prompt = runner._build_iteration_prompt(1)
assert "Context Engineering 분석" in prompt
assert "Iteration 1/5" in prompt
assert "RESEARCH_COMPLETE" in prompt
def test_completion_detection_by_promise(self, temp_workspace):
"""promise 태그로 완료 감지."""
with patch.object(ResearchSession, "WORKSPACE", temp_workspace):
runner = ResearchRunner("Test", depth="quick")
result = {
"messages": [
MagicMock(content="Done <promise>RESEARCH_COMPLETE</promise>")
]
}
assert runner._check_completion(result) is True
def test_completion_detection_by_coverage(self, temp_workspace):
"""coverage 기반 완료 감지."""
with patch.object(ResearchSession, "WORKSPACE", temp_workspace):
runner = ResearchRunner("Test", depth="quick")
runner.session.ralph_loop.state.coverage_score = 0.95
result = {"messages": [MagicMock(content="Still working...")]}
assert runner._check_completion(result) is True
def test_no_completion_when_incomplete(self, temp_workspace):
"""완료 조건 미충족 시 False 반환."""
with patch.object(ResearchSession, "WORKSPACE", temp_workspace):
runner = ResearchRunner("Test", depth="deep")
runner.session.ralph_loop.state.coverage_score = 0.3
runner.session.ralph_loop.state.iteration = 1
result = {"messages": [MagicMock(content="Working on it...")]}
assert runner._check_completion(result) is False
class TestE2EWithMockedAgent:
"""Mock 에이전트를 사용한 E2E 테스트."""
@pytest.fixture
def temp_workspace(self):
"""임시 워크스페이스 생성."""
with tempfile.TemporaryDirectory() as tmpdir:
yield Path(tmpdir)
def test_single_iteration_completion(self, temp_workspace):
"""단일 반복으로 완료되는 케이스."""
with patch.object(ResearchSession, "WORKSPACE", temp_workspace):
runner = ResearchRunner("Quick test", depth="quick")
mock_agent = AsyncMock()
mock_agent.ainvoke.return_value = {
"messages": [MagicMock(content="<promise>RESEARCH_COMPLETE</promise>")]
}
runner.agent = mock_agent
async def run_test():
runner.session.initialize()
result = await runner._execute_iteration(1)
is_complete = runner._check_completion(result)
return is_complete
is_complete = asyncio.get_event_loop().run_until_complete(run_test())
assert is_complete is True
def test_multiple_iterations_until_completion(self, temp_workspace):
"""여러 반복 후 완료되는 케이스."""
with patch.object(ResearchSession, "WORKSPACE", temp_workspace):
runner = ResearchRunner("Deep test", depth="deep")
call_count = 0
async def mock_invoke(*args, **kwargs):
nonlocal call_count
call_count += 1
if call_count >= 3:
return {
"messages": [
MagicMock(content="<promise>RESEARCH_COMPLETE</promise>")
]
}
return {"messages": [MagicMock(content="Still researching...")]}
mock_agent = AsyncMock()
mock_agent.ainvoke = mock_invoke
runner.agent = mock_agent
async def run_test():
runner.session.initialize()
iteration = 1
max_iter = 5
while iteration <= max_iter:
result = await runner._execute_iteration(iteration)
if runner._check_completion(result):
break
iteration += 1
return iteration
final_iteration = asyncio.get_event_loop().run_until_complete(run_test())
assert final_iteration == 3
assert call_count == 3
class TestFilesystemStateChanges:
"""파일시스템 상태 변화 검증."""
@pytest.fixture
def temp_workspace(self):
"""임시 워크스페이스 생성."""
with tempfile.TemporaryDirectory() as tmpdir:
yield Path(tmpdir)
def test_findings_file_updated_on_add(self, temp_workspace):
"""Finding 추가 시 파일이 업데이트되는지 확인."""
with patch.object(ResearchSession, "WORKSPACE", temp_workspace):
session = ResearchSession("Test query")
session.initialize()
finding = Finding(
content="Important discovery about LLMs",
source_url="https://example.com/article",
source_title="LLM Research Paper",
confidence=0.9,
)
session.add_finding(finding)
findings_content = (session.session_dir / "FINDINGS.md").read_text()
assert "Important discovery about LLMs" in findings_content
assert "https://example.com/article" in findings_content
assert "LLM Research Paper" in findings_content
def test_coverage_updates_with_findings(self, temp_workspace):
"""Finding 추가 시 coverage가 업데이트되는지 확인."""
with patch.object(ResearchSession, "WORKSPACE", temp_workspace):
session = ResearchSession("Test query")
session.initialize()
initial_coverage = session.ralph_loop.state.coverage_score
assert initial_coverage == 0.0
for i in range(5):
finding = Finding(
content=f"Finding {i}",
source_url=f"https://example.com/{i}",
source_title=f"Source {i}",
confidence=0.8,
)
session.add_finding(finding)
assert session.ralph_loop.state.coverage_score > initial_coverage
assert session.ralph_loop.state.findings_count == 5
def test_summary_created_on_finalize(self, temp_workspace):
"""finalize 시 SUMMARY.md가 생성되는지 확인."""
with patch.object(ResearchSession, "WORKSPACE", temp_workspace):
session = ResearchSession("Test query")
session.initialize()
finding = Finding(
content="Test finding",
source_url="https://example.com",
source_title="Test Source",
confidence=0.9,
)
session.add_finding(finding)
summary_path = session.finalize()
assert summary_path.exists()
summary_content = summary_path.read_text()
assert "Test query" in summary_content
assert "Total Findings: 1" in summary_content
class TestCompletionConditions:
"""완료 조건 동작 확인."""
@pytest.fixture
def temp_workspace(self):
"""임시 워크스페이스 생성."""
with tempfile.TemporaryDirectory() as tmpdir:
yield Path(tmpdir)
def test_max_iterations_limit(self, temp_workspace):
"""최대 반복 횟수 제한 동작 확인."""
with patch.object(ResearchSession, "WORKSPACE", temp_workspace):
runner = ResearchRunner("Test", depth="quick")
assert runner.config.max_ralph_iterations == 1
runner.session.ralph_loop.state.iteration = 1
assert runner.session.ralph_loop.is_complete() is True
def test_coverage_threshold_completion(self, temp_workspace):
"""coverage threshold 도달 시 완료."""
with patch.object(ResearchSession, "WORKSPACE", temp_workspace):
runner = ResearchRunner("Test", depth="deep")
runner.session.ralph_loop.state.coverage_score = 0.84
assert runner.session.ralph_loop.is_complete() is False
runner.session.ralph_loop.state.coverage_score = 0.85
assert runner.session.ralph_loop.is_complete() is True
def test_iteration_increment(self, temp_workspace):
"""반복 증가 동작 확인."""
with patch.object(ResearchSession, "WORKSPACE", temp_workspace):
session = ResearchSession("Test query")
session.initialize()
initial_iteration = session.ralph_loop.state.iteration
assert initial_iteration == 1
session.complete_iteration()
assert session.ralph_loop.state.iteration == 2
def test_state_file_persistence(self, temp_workspace):
"""상태 파일 영속성 확인."""
state_file = temp_workspace / ".claude" / "research-ralph-loop.local.md"
with patch.object(ResearchSession, "WORKSPACE", temp_workspace):
with patch(
"research_agent.researcher.ralph_loop.ResearchRalphLoop.STATE_FILE",
state_file,
):
session = ResearchSession("Test query")
session.initialize()
assert state_file.exists()
state_content = state_file.read_text()
assert "active: true" in state_content
assert "iteration: 1" in state_content