262 lines
11 KiB
Python
262 lines
11 KiB
Python
"""Test PostToolUse hook functionality."""
|
|
|
|
import tempfile
|
|
from unittest.mock import patch
|
|
|
|
from code_quality_guard import QualityConfig, posttooluse_hook
|
|
|
|
|
|
class TestPostToolUseHook:
|
|
"""Test PostToolUse hook behavior."""
|
|
|
|
def test_non_write_tool_allowed(self):
|
|
"""Test that non-write/edit tools are always allowed."""
|
|
config = QualityConfig()
|
|
hook_data = {
|
|
"tool_name": "Read",
|
|
"tool_output": {"status": "success"},
|
|
}
|
|
|
|
result = posttooluse_hook(hook_data, config)
|
|
assert result["decision"] == "allow"
|
|
|
|
def test_file_path_extraction_dict(self):
|
|
"""Test file path extraction from dict output."""
|
|
config = QualityConfig()
|
|
test_file = f"{tempfile.gettempdir()}/test.py"
|
|
|
|
# Test with file_path key
|
|
hook_data = {
|
|
"tool_name": "Write",
|
|
"tool_output": {"file_path": test_file},
|
|
}
|
|
|
|
with patch("pathlib.Path.exists", return_value=True):
|
|
with patch("pathlib.Path.read_text", return_value="def test(): pass"):
|
|
result = posttooluse_hook(hook_data, config)
|
|
assert result["decision"] == "allow"
|
|
|
|
# Test with path key
|
|
hook_data["tool_output"] = {"path": test_file}
|
|
|
|
with patch("pathlib.Path.exists", return_value=True):
|
|
with patch("pathlib.Path.read_text", return_value="def test(): pass"):
|
|
result = posttooluse_hook(hook_data, config)
|
|
assert result["decision"] == "allow"
|
|
|
|
def test_file_path_extraction_string(self):
|
|
"""Test file path extraction from string output."""
|
|
config = QualityConfig()
|
|
hook_data = {
|
|
"tool_name": "Write",
|
|
"tool_output": "File written successfully: /tmp/test.py",
|
|
}
|
|
|
|
with patch("pathlib.Path.exists", return_value=True):
|
|
with patch("pathlib.Path.read_text", return_value="def test(): pass"):
|
|
result = posttooluse_hook(hook_data, config)
|
|
assert result["decision"] == "allow"
|
|
|
|
def test_non_python_file_skipped(self):
|
|
"""Test that non-Python files are skipped."""
|
|
config = QualityConfig()
|
|
hook_data = {
|
|
"tool_name": "Write",
|
|
"tool_output": {"file_path": f"{tempfile.gettempdir()}/test.js"},
|
|
}
|
|
|
|
result = posttooluse_hook(hook_data, config)
|
|
assert result["decision"] == "allow"
|
|
|
|
def test_nonexistent_file_skipped(self):
|
|
"""Test that nonexistent files are skipped."""
|
|
config = QualityConfig()
|
|
hook_data = {
|
|
"tool_name": "Write",
|
|
"tool_output": {"file_path": f"{tempfile.gettempdir()}/nonexistent.py"},
|
|
}
|
|
|
|
with patch("pathlib.Path.exists", return_value=False):
|
|
result = posttooluse_hook(hook_data, config)
|
|
assert result["decision"] == "allow"
|
|
|
|
def test_state_tracking_degradation(self):
|
|
"""Test state tracking detects quality degradation."""
|
|
config = QualityConfig(state_tracking_enabled=True)
|
|
hook_data = {
|
|
"tool_name": "Write",
|
|
"tool_output": {"file_path": f"{tempfile.gettempdir()}/test.py"},
|
|
}
|
|
|
|
with patch("pathlib.Path.exists", return_value=True):
|
|
with patch("pathlib.Path.read_text", return_value="def test(): pass"):
|
|
with patch("code_quality_guard.check_state_changes") as mock_check:
|
|
mock_check.return_value = [
|
|
"⚠️ Reduced functions: 5 → 2",
|
|
"⚠️ File size increased significantly: 100 → 250 lines",
|
|
]
|
|
|
|
result = posttooluse_hook(hook_data, config)
|
|
assert result["decision"] == "allow"
|
|
assert "Post-write quality notes" in result["message"]
|
|
assert "Reduced functions" in result["message"]
|
|
|
|
def test_cross_file_duplicates(self):
|
|
"""Test cross-file duplicate detection."""
|
|
config = QualityConfig(cross_file_check_enabled=True)
|
|
hook_data = {
|
|
"tool_name": "Write",
|
|
"tool_output": {"file_path": f"{tempfile.gettempdir()}/test.py"},
|
|
}
|
|
|
|
with patch("pathlib.Path.exists", return_value=True):
|
|
with patch("pathlib.Path.read_text", return_value="def test(): pass"):
|
|
with patch(
|
|
"code_quality_guard.check_cross_file_duplicates",
|
|
) as mock_check:
|
|
mock_check.return_value = ["⚠️ Cross-file duplication detected"]
|
|
|
|
result = posttooluse_hook(hook_data, config)
|
|
assert result["decision"] == "allow"
|
|
assert "Cross-file duplication" in result["message"]
|
|
|
|
def test_naming_convention_violations(self, non_pep8_code):
|
|
"""Test naming convention verification."""
|
|
config = QualityConfig(verify_naming=True)
|
|
hook_data = {
|
|
"tool_name": "Write",
|
|
"tool_output": {"file_path": f"{tempfile.gettempdir()}/test.py"},
|
|
}
|
|
|
|
with patch("pathlib.Path.exists", return_value=True):
|
|
with patch("pathlib.Path.read_text", return_value=non_pep8_code):
|
|
result = posttooluse_hook(hook_data, config)
|
|
assert result["decision"] == "allow"
|
|
assert "Non-PEP8 function names" in result["message"]
|
|
assert "Non-PEP8 class names" in result["message"]
|
|
|
|
def test_show_success_message(self, clean_code):
|
|
"""Test success message when enabled."""
|
|
config = QualityConfig(show_success=True, verify_naming=False)
|
|
hook_data = {
|
|
"tool_name": "Write",
|
|
"tool_output": {"file_path": f"{tempfile.gettempdir()}/test.py"},
|
|
}
|
|
|
|
with patch("pathlib.Path.exists", return_value=True):
|
|
with patch("pathlib.Path.read_text", return_value=clean_code):
|
|
result = posttooluse_hook(hook_data, config)
|
|
assert result["decision"] == "allow"
|
|
assert "passed post-write verification" in result["message"]
|
|
|
|
def test_no_message_when_success_disabled(self, clean_code):
|
|
"""Test no message when show_success is disabled."""
|
|
config = QualityConfig(show_success=False, verify_naming=False)
|
|
hook_data = {
|
|
"tool_name": "Write",
|
|
"tool_output": {"file_path": f"{tempfile.gettempdir()}/test.py"},
|
|
}
|
|
|
|
with patch("pathlib.Path.exists", return_value=True):
|
|
with patch("pathlib.Path.read_text", return_value=clean_code):
|
|
result = posttooluse_hook(hook_data, config)
|
|
assert result["decision"] == "allow"
|
|
assert "message" not in result
|
|
|
|
def test_all_features_combined(self):
|
|
"""Test all PostToolUse features combined."""
|
|
config = QualityConfig(
|
|
state_tracking_enabled=True,
|
|
cross_file_check_enabled=True,
|
|
verify_naming=True,
|
|
show_success=False,
|
|
)
|
|
hook_data = {
|
|
"tool_name": "Write",
|
|
"tool_output": {"file_path": f"{tempfile.gettempdir()}/test.py"},
|
|
}
|
|
|
|
with patch("pathlib.Path.exists", return_value=True):
|
|
with patch("pathlib.Path.read_text", return_value="def test(): pass"):
|
|
with patch("code_quality_guard.check_state_changes") as mock_state:
|
|
with patch(
|
|
"code_quality_guard.check_cross_file_duplicates",
|
|
) as mock_cross:
|
|
with patch(
|
|
"code_quality_guard.verify_naming_conventions",
|
|
) as mock_naming:
|
|
mock_state.return_value = ["⚠️ Issue 1"]
|
|
mock_cross.return_value = ["⚠️ Issue 2"]
|
|
mock_naming.return_value = ["⚠️ Issue 3"]
|
|
|
|
result = posttooluse_hook(hook_data, config)
|
|
assert result["decision"] == "allow"
|
|
assert "Issue 1" in result["message"]
|
|
assert "Issue 2" in result["message"]
|
|
assert "Issue 3" in result["message"]
|
|
|
|
def test_edit_tool_output(self):
|
|
"""Test Edit tool output handling."""
|
|
config = QualityConfig(verify_naming=True)
|
|
hook_data = {
|
|
"tool_name": "Edit",
|
|
"tool_output": {
|
|
"file_path": f"{tempfile.gettempdir()}/test.py",
|
|
"status": "success",
|
|
},
|
|
}
|
|
|
|
with patch("pathlib.Path.exists", return_value=True):
|
|
with patch("pathlib.Path.read_text", return_value="def test(): pass"):
|
|
result = posttooluse_hook(hook_data, config)
|
|
assert result["decision"] == "allow"
|
|
|
|
def test_multiedit_tool_output(self):
|
|
"""Test MultiEdit tool output handling."""
|
|
config = QualityConfig(verify_naming=True)
|
|
hook_data = {
|
|
"tool_name": "MultiEdit",
|
|
"tool_output": {
|
|
"file_path": f"{tempfile.gettempdir()}/test.py",
|
|
"edits_applied": 3,
|
|
},
|
|
}
|
|
|
|
with patch("pathlib.Path.exists", return_value=True):
|
|
with patch("pathlib.Path.read_text", return_value="def test(): pass"):
|
|
result = posttooluse_hook(hook_data, config)
|
|
assert result["decision"] == "allow"
|
|
|
|
def test_features_disabled(self):
|
|
"""Test with all features disabled."""
|
|
config = QualityConfig(
|
|
state_tracking_enabled=False,
|
|
cross_file_check_enabled=False,
|
|
verify_naming=False,
|
|
show_success=False,
|
|
)
|
|
hook_data = {
|
|
"tool_name": "Write",
|
|
"tool_output": {"file_path": f"{tempfile.gettempdir()}/test.py"},
|
|
}
|
|
|
|
with patch("pathlib.Path.exists", return_value=True):
|
|
with patch("pathlib.Path.read_text", return_value="def test(): pass"):
|
|
# Should not call any check functions
|
|
with patch("code_quality_guard.check_state_changes") as mock_state:
|
|
with patch(
|
|
"code_quality_guard.check_cross_file_duplicates",
|
|
) as mock_cross:
|
|
with patch(
|
|
"code_quality_guard.verify_naming_conventions",
|
|
) as mock_naming:
|
|
result = posttooluse_hook(hook_data, config)
|
|
|
|
# Verify no checks were called
|
|
mock_state.assert_not_called()
|
|
mock_cross.assert_not_called()
|
|
mock_naming.assert_not_called()
|
|
|
|
assert result["decision"] == "allow"
|
|
assert "message" not in result
|