Skip to content

Commit a62cd0a

Browse files
GeneAIclaude
authored andcommitted
test: Add workflow tests for health_check and code_review
Added comprehensive test suites for: - HealthCheckWorkflow: 20 tests covering initialization, basic checks, stage routing, fix handling, and health data persistence - CodeReviewWorkflow: 25 tests covering initialization, stage skipping, LLM calls, classification logic, and tier routing Coverage improvements: - health_check.py: 0% → 69.9% - code_review.py: 7.9% → 34.2% 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <[email protected]>
1 parent 0750f70 commit a62cd0a

File tree

2 files changed

+786
-0
lines changed

2 files changed

+786
-0
lines changed

tests/test_code_review_workflow.py

Lines changed: 349 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,349 @@
1+
"""
2+
Tests for CodeReviewWorkflow.
3+
4+
Tests the tiered code review pipeline with classification,
5+
security scanning, and conditional architectural review.
6+
"""
7+
8+
from unittest.mock import AsyncMock, MagicMock, patch
9+
10+
import pytest
11+
12+
from src.empathy_os.workflows.base import ModelTier
13+
from src.empathy_os.workflows.code_review import CodeReviewWorkflow
14+
15+
16+
class TestCodeReviewWorkflowInit:
17+
"""Tests for CodeReviewWorkflow initialization."""
18+
19+
def test_default_init(self):
20+
"""Test default initialization."""
21+
workflow = CodeReviewWorkflow()
22+
23+
assert workflow.name == "code-review"
24+
assert workflow.file_threshold == 10
25+
assert len(workflow.core_modules) > 0
26+
assert workflow.use_crew is False
27+
assert workflow.stages == ["classify", "scan", "architect_review"]
28+
29+
def test_custom_file_threshold(self):
30+
"""Test custom file threshold."""
31+
workflow = CodeReviewWorkflow(file_threshold=5)
32+
33+
assert workflow.file_threshold == 5
34+
35+
def test_custom_core_modules(self):
36+
"""Test custom core modules."""
37+
core_modules = ["src/main.py", "src/api/"]
38+
workflow = CodeReviewWorkflow(core_modules=core_modules)
39+
40+
assert workflow.core_modules == core_modules
41+
42+
def test_crew_mode_stages(self):
43+
"""Test stages when crew mode is enabled."""
44+
workflow = CodeReviewWorkflow(use_crew=True)
45+
46+
assert workflow.use_crew is True
47+
assert "crew_review" in workflow.stages
48+
assert workflow.stages == ["classify", "crew_review", "scan", "architect_review"]
49+
50+
def test_tier_map(self):
51+
"""Test tier mapping for stages."""
52+
workflow = CodeReviewWorkflow()
53+
54+
assert workflow.tier_map["classify"] == ModelTier.CHEAP
55+
assert workflow.tier_map["scan"] == ModelTier.CAPABLE
56+
assert workflow.tier_map["architect_review"] == ModelTier.PREMIUM
57+
58+
def test_crew_config(self):
59+
"""Test crew configuration."""
60+
config = {"memory_enabled": True}
61+
workflow = CodeReviewWorkflow(use_crew=True, crew_config=config)
62+
63+
assert workflow.crew_config == config
64+
65+
66+
class TestCodeReviewWorkflowSkipStage:
67+
"""Tests for stage skipping logic."""
68+
69+
def test_should_not_skip_classify(self):
70+
"""Classify stage should never be skipped."""
71+
workflow = CodeReviewWorkflow()
72+
73+
skip, reason = workflow.should_skip_stage("classify", {})
74+
75+
assert skip is False
76+
assert reason is None
77+
78+
def test_should_not_skip_scan(self):
79+
"""Scan stage should never be skipped."""
80+
workflow = CodeReviewWorkflow()
81+
82+
skip, reason = workflow.should_skip_stage("scan", {})
83+
84+
assert skip is False
85+
assert reason is None
86+
87+
def test_should_skip_architect_review_simple(self):
88+
"""Architect review should be skipped for simple changes."""
89+
workflow = CodeReviewWorkflow()
90+
workflow._needs_architect_review = False
91+
92+
skip, reason = workflow.should_skip_stage("architect_review", {})
93+
94+
assert skip is True
95+
assert "Simple change" in reason
96+
97+
def test_should_not_skip_architect_review_complex(self):
98+
"""Architect review should not be skipped for complex changes."""
99+
workflow = CodeReviewWorkflow()
100+
workflow._needs_architect_review = True
101+
102+
skip, reason = workflow.should_skip_stage("architect_review", {})
103+
104+
assert skip is False
105+
assert reason is None
106+
107+
108+
class TestCodeReviewWorkflowStages:
109+
"""Tests for workflow stage routing."""
110+
111+
@pytest.mark.asyncio
112+
async def test_run_stage_classify(self):
113+
"""Test classify stage routing."""
114+
workflow = CodeReviewWorkflow()
115+
116+
with patch.object(workflow, "_classify", new_callable=AsyncMock) as mock:
117+
mock.return_value = ({"change_type": "feature"}, 100, 50)
118+
119+
result = await workflow.run_stage("classify", ModelTier.CHEAP, {"diff": "..."})
120+
121+
mock.assert_called_once()
122+
123+
@pytest.mark.asyncio
124+
async def test_run_stage_scan(self):
125+
"""Test scan stage routing."""
126+
workflow = CodeReviewWorkflow()
127+
128+
with patch.object(workflow, "_scan", new_callable=AsyncMock) as mock:
129+
mock.return_value = ({"security_issues": []}, 200, 100)
130+
131+
result = await workflow.run_stage("scan", ModelTier.CAPABLE, {"diff": "..."})
132+
133+
mock.assert_called_once()
134+
135+
@pytest.mark.asyncio
136+
async def test_run_stage_architect_review(self):
137+
"""Test architect review stage routing."""
138+
workflow = CodeReviewWorkflow()
139+
140+
with patch.object(workflow, "_architect_review", new_callable=AsyncMock) as mock:
141+
mock.return_value = ({"recommendations": []}, 300, 200)
142+
143+
result = await workflow.run_stage(
144+
"architect_review", ModelTier.PREMIUM, {"diff": "..."}
145+
)
146+
147+
mock.assert_called_once()
148+
149+
@pytest.mark.asyncio
150+
async def test_run_stage_crew_review(self):
151+
"""Test crew review stage routing."""
152+
workflow = CodeReviewWorkflow(use_crew=True)
153+
154+
with patch.object(workflow, "_crew_review", new_callable=AsyncMock) as mock:
155+
mock.return_value = ({"crew_analysis": {}}, 500, 300)
156+
157+
result = await workflow.run_stage(
158+
"crew_review", ModelTier.PREMIUM, {"diff": "..."}
159+
)
160+
161+
mock.assert_called_once()
162+
163+
@pytest.mark.asyncio
164+
async def test_run_stage_invalid(self):
165+
"""Test invalid stage raises error."""
166+
workflow = CodeReviewWorkflow()
167+
168+
with pytest.raises(ValueError, match="Unknown stage"):
169+
await workflow.run_stage("invalid", ModelTier.CHEAP, {})
170+
171+
172+
class TestCodeReviewLLMCalls:
173+
"""Tests for LLM call handling."""
174+
175+
def test_get_client_no_api_key(self):
176+
"""Test client returns None without API key."""
177+
workflow = CodeReviewWorkflow()
178+
workflow._api_key = None
179+
180+
client = workflow._get_client()
181+
182+
assert client is None
183+
184+
def test_get_model_for_tier(self):
185+
"""Test getting model for tier."""
186+
workflow = CodeReviewWorkflow()
187+
188+
# Should return a valid model string
189+
model = workflow._get_model_for_tier(ModelTier.CHEAP)
190+
assert isinstance(model, str)
191+
assert len(model) > 0
192+
193+
model = workflow._get_model_for_tier(ModelTier.CAPABLE)
194+
assert isinstance(model, str)
195+
196+
model = workflow._get_model_for_tier(ModelTier.PREMIUM)
197+
assert isinstance(model, str)
198+
199+
@pytest.mark.asyncio
200+
async def test_call_llm_no_client(self):
201+
"""Test LLM call returns simulation when no client."""
202+
workflow = CodeReviewWorkflow()
203+
workflow._api_key = None
204+
205+
content, input_tokens, output_tokens = await workflow._call_llm(
206+
ModelTier.CHEAP, "You are a classifier", "Classify this diff"
207+
)
208+
209+
assert "[Simulated" in content
210+
assert input_tokens > 0
211+
assert output_tokens == 100
212+
213+
@pytest.mark.asyncio
214+
async def test_call_llm_with_client(self):
215+
"""Test LLM call with mocked client."""
216+
workflow = CodeReviewWorkflow()
217+
workflow._api_key = "test-key"
218+
219+
mock_client = MagicMock()
220+
mock_response = MagicMock()
221+
mock_response.content = [MagicMock(text="Feature change detected")]
222+
mock_response.usage.input_tokens = 50
223+
mock_response.usage.output_tokens = 25
224+
mock_client.messages.create.return_value = mock_response
225+
226+
with patch.object(workflow, "_get_client", return_value=mock_client):
227+
content, input_tokens, output_tokens = await workflow._call_llm(
228+
ModelTier.CHEAP, "You are a classifier", "Classify this diff"
229+
)
230+
231+
assert content == "Feature change detected"
232+
assert input_tokens == 50
233+
assert output_tokens == 25
234+
235+
@pytest.mark.asyncio
236+
async def test_call_llm_error_handling(self):
237+
"""Test LLM call error handling."""
238+
workflow = CodeReviewWorkflow()
239+
workflow._api_key = "test-key"
240+
241+
mock_client = MagicMock()
242+
mock_client.messages.create.side_effect = Exception("API Error")
243+
244+
with patch.object(workflow, "_get_client", return_value=mock_client):
245+
content, input_tokens, output_tokens = await workflow._call_llm(
246+
ModelTier.CHEAP, "System", "Message"
247+
)
248+
249+
assert "Error calling LLM" in content
250+
assert input_tokens == 0
251+
assert output_tokens == 0
252+
253+
254+
class TestCodeReviewWorkflowClassification:
255+
"""Tests for change classification logic."""
256+
257+
@pytest.mark.asyncio
258+
async def test_classify_sets_change_type(self):
259+
"""Test that classify sets change type."""
260+
workflow = CodeReviewWorkflow()
261+
262+
with patch.object(workflow, "_call_llm", new_callable=AsyncMock) as mock_llm:
263+
mock_llm.return_value = ("feature", 100, 50)
264+
265+
input_data = {
266+
"diff": "def new_feature(): pass",
267+
"files_changed": ["src/feature.py"],
268+
}
269+
270+
result, _, _ = await workflow.run_stage("classify", ModelTier.CHEAP, input_data)
271+
272+
# Check that classify was called
273+
mock_llm.assert_called_once()
274+
275+
@pytest.mark.asyncio
276+
async def test_classify_triggers_architect_review_many_files(self):
277+
"""Test that many files trigger architect review."""
278+
workflow = CodeReviewWorkflow(file_threshold=5)
279+
280+
with patch.object(workflow, "_call_llm", new_callable=AsyncMock) as mock_llm:
281+
mock_llm.return_value = ("refactor", 100, 50)
282+
283+
input_data = {
284+
"diff": "...",
285+
"files_changed": [f"file{i}.py" for i in range(10)], # 10 files > threshold
286+
}
287+
288+
await workflow.run_stage("classify", ModelTier.CHEAP, input_data)
289+
290+
assert workflow._needs_architect_review is True
291+
292+
@pytest.mark.asyncio
293+
async def test_classify_triggers_architect_review_core_module(self):
294+
"""Test that core module changes trigger architect review."""
295+
workflow = CodeReviewWorkflow(core_modules=["src/core/"])
296+
297+
with patch.object(workflow, "_call_llm", new_callable=AsyncMock) as mock_llm:
298+
mock_llm.return_value = ("fix", 100, 50)
299+
300+
input_data = {
301+
"diff": "...",
302+
"files_changed": ["src/core/main.py"], # Core module
303+
"is_core_module": True,
304+
}
305+
306+
await workflow.run_stage("classify", ModelTier.CHEAP, input_data)
307+
308+
assert workflow._needs_architect_review is True
309+
310+
311+
class TestCodeReviewWorkflowIntegration:
312+
"""Integration tests for CodeReviewWorkflow."""
313+
314+
@pytest.mark.asyncio
315+
async def test_simple_change_skips_architect(self):
316+
"""Test that simple changes skip architect review."""
317+
workflow = CodeReviewWorkflow(file_threshold=10)
318+
319+
# Set up mocks
320+
with patch.object(workflow, "_call_llm", new_callable=AsyncMock) as mock_llm:
321+
mock_llm.return_value = ("docs: update readme", 50, 25)
322+
323+
# Mock base workflow execute
324+
with patch.object(
325+
workflow.__class__.__bases__[0], "execute", new_callable=AsyncMock
326+
) as mock_execute:
327+
mock_result = MagicMock()
328+
mock_result.success = True
329+
mock_result.stages_run = ["classify", "scan"] # No architect_review
330+
mock_result.stages_skipped = ["architect_review"]
331+
mock_execute.return_value = mock_result
332+
333+
result = await workflow.execute(
334+
diff="# Updated README",
335+
files_changed=["README.md"],
336+
is_core_module=False,
337+
)
338+
339+
assert "architect_review" in result.stages_skipped
340+
341+
@pytest.mark.asyncio
342+
async def test_complex_change_includes_architect(self):
343+
"""Test that complex changes include architect review."""
344+
workflow = CodeReviewWorkflow(file_threshold=5)
345+
workflow._needs_architect_review = True
346+
347+
skip, reason = workflow.should_skip_stage("architect_review", {})
348+
349+
assert skip is False

0 commit comments

Comments
 (0)