Files
claude-scripts/tests/test_hook_integration.py
Travis Vasceannie 812378c0e1 Refactor and enhance code quality analysis framework
- Updated AGENTS.md to provide comprehensive guidance on the Claude-Scripts project, including project overview, development commands, and architecture.
- Added new utility functions in hooks/guards/utils.py to support code quality checks and enhance modularity.
- Introduced HookResponseRequired TypedDict for stricter type checking in hook responses.
- Enhanced quality guard functionality with additional checks and improved type annotations across various modules.
- Updated pyproject.toml and uv.lock to include mypy as a development dependency for better type checking.
- Improved type checking configurations in pyrightconfig.json to exclude unnecessary directories and suppress specific warnings.

This update significantly improves the structure and maintainability of the code quality analysis toolkit, ensuring better adherence to type safety and project guidelines.
2025-10-26 09:43:47 +00:00

249 lines
8.1 KiB
Python

"""Comprehensive integration tests for code quality hooks.
This test suite validates that the hooks properly block forbidden code patterns
and allow good code to pass through.
"""
import json
import re
import sys
import tempfile
from pathlib import Path
# Add hooks directory to path for imports
_HOOKS_DIR = Path(__file__).parent.parent / "hooks"
sys.path.insert(0, str(_HOOKS_DIR.parent))
sys.path.insert(0, str(_HOOKS_DIR))
from facade import Guards # pyright: ignore[reportMissingImports]
from models import HookResponse # pyright: ignore[reportMissingImports]
HOOKS_DIR = _HOOKS_DIR
# Type alias for test data
JsonObject = dict[str, object]
def _detect_any_usage(content: str) -> list[dict[str, object]]:
"""Detect typing.Any usage in code."""
issues: list[dict[str, object]] = []
patterns = [
r"\bfrom\s+typing\s+import\s+.*\bAny\b",
r"\btyping\.Any\b",
r"\b:\s*Any\b",
r"->\s*Any\b",
]
lines = content.split("\n")
for line_num, line in enumerate(lines, 1):
for pattern in patterns:
if re.search(pattern, line):
issues.append({"line": line_num, "context": line.strip()})
break
return issues
def _detect_old_typing_patterns(content: str) -> list[dict[str, object]]:
"""Detect old typing patterns like Union, Optional, List, Dict."""
issues: list[dict[str, object]] = []
old_patterns = [
(r"\bUnion\[", "Union"),
(r"\bOptional\[", "Optional"),
(r"\bList\[", "List"),
(r"\bDict\[", "Dict"),
(r"\bTuple\[", "Tuple"),
(r"\bSet\[", "Set"),
]
lines = content.split("\n")
for line_num, line in enumerate(lines, 1):
for pattern, name in old_patterns:
if re.search(pattern, line):
issues.append(
{"line": line_num, "pattern": name, "context": line.strip()},
)
return issues
def _detect_type_ignore_usage(content: str) -> list[dict[str, object]]:
"""Detect type: ignore comments."""
issues: list[dict[str, object]] = []
lines = content.split("\n")
for line_num, line in enumerate(lines, 1):
if re.search(r"#\s*type:\s*ignore", line):
issues.append({"line": line_num, "context": line.strip()})
return issues
class _MockConfig:
"""Mock config for backwards compatibility."""
enforcement_mode: str = "strict"
@classmethod
def from_env(cls) -> "_MockConfig":
"""Create config from environment (mock implementation)."""
return cls()
def pretooluse_hook(hook_data: JsonObject, config: object) -> HookResponse:
"""Wrapper for pretooluse using Guards facade."""
_ = config
guards = Guards()
return guards.handle_pretooluse(hook_data)
def posttooluse_hook(hook_data: JsonObject, config: object) -> HookResponse:
"""Wrapper for posttooluse using Guards facade."""
_ = config
guards = Guards()
return guards.handle_posttooluse(hook_data)
QualityConfig = _MockConfig
class TestHookIntegration:
"""Integration tests for the complete hook system."""
config: QualityConfig
def __init__(self) -> None:
super().__init__()
self.config = QualityConfig.from_env()
self.config.enforcement_mode = "strict"
def setup_method(self) -> None:
"""Set up test environment."""
self.config = QualityConfig.from_env()
self.config.enforcement_mode = "strict"
def test_any_usage_blocked(self) -> None:
"""Test that typing.Any usage is blocked."""
content = """from typing import Any
def bad_function(param: Any) -> Any:
return param"""
hook_data: JsonObject = {
"tool_name": "Write",
"tool_input": {
"file_path": "/src/production_code.py",
"content": content,
},
}
result = pretooluse_hook(hook_data, self.config)
decision = result.get("permissionDecision", "")
reason = result.get("reason", "")
assert decision == "deny" or "Any" in str(reason)
def test_good_code_allowed(self) -> None:
"""Test that good code is allowed through."""
content = """def good_function(param: str | int) -> list[dict[str, int]] | None:
\"\"\"A properly typed function.\"\"\"
if param == "empty":
return None
return [{"value": 1}]"""
hook_data: JsonObject = {
"tool_name": "Write",
"tool_input": {"file_path": "/src/production_code.py", "content": content},
}
result = pretooluse_hook(hook_data, self.config)
decision = result.get("permissionDecision", "allow")
assert decision == "allow"
def test_non_python_files_allowed(self) -> None:
"""Test that non-Python files are allowed through."""
hook_data: JsonObject = {
"tool_name": "Write",
"tool_input": {
"file_path": "/src/config.json",
"content": json.dumps({"any": "value", "type": "ignore"}),
},
}
result = pretooluse_hook(hook_data, self.config)
decision = result.get("permissionDecision", "allow")
assert decision == "allow"
def test_posttooluse_hook(self) -> None:
"""Test PostToolUse hook functionality."""
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
_ = f.write("from typing import Any\ndef bad(x: Any) -> Any: return x")
temp_path = f.name
try:
hook_data: JsonObject = {
"tool_name": "Write",
"tool_response": {"file_path": temp_path},
}
result = posttooluse_hook(hook_data, self.config)
# PostToolUse should detect issues in the written file
assert "decision" in result or "hookSpecificOutput" in result
finally:
Path(temp_path).unlink(missing_ok=True)
class TestDetectionFunctions:
"""Test the individual detection functions."""
def test_any_detection_comprehensive(self) -> None:
"""Test comprehensive Any detection scenarios."""
test_cases = [
("from typing import Any", True),
("import typing; x: typing.Any", True),
("def func(x: Any) -> Any:", True),
("collection: dict[str, Any]", True),
("# This has Any in comment", False),
("def func(x: str) -> int:", False),
("x = 'Any string'", False),
]
for content, should_detect in test_cases:
issues = _detect_any_usage(content)
has_issues = len(issues) > 0
assert has_issues == should_detect, f"Failed for: {content}"
def test_type_ignore_detection_comprehensive(self) -> None:
"""Test comprehensive type: ignore detection."""
test_cases = [
("x = call() # type: ignore", True),
("x = call() #type:ignore", True),
("x = call() # type: ignore[arg-type]", True),
("x = call() # TYPE: IGNORE", True),
("# This is just a comment about type ignore", False),
("x = call() # not a type ignore", False),
]
for content, should_detect in test_cases:
issues = _detect_type_ignore_usage(content)
has_issues = len(issues) > 0
assert has_issues == should_detect, f"Failed for: {content}"
def test_old_typing_patterns_comprehensive(self) -> None:
"""Test comprehensive old typing patterns detection."""
test_cases = [
("from typing import Union", True),
("from typing import Optional", True),
("from typing import List, Dict", True),
("Union[str, int]", True),
("Optional[str]", True),
("List[str]", True),
("Dict[str, int]", True),
("str | int", False),
("list[str]", False),
("dict[str, int]", False),
]
for content, should_detect in test_cases:
issues = _detect_old_typing_patterns(content)
has_issues = len(issues) > 0
assert has_issues == should_detect, f"Failed for: {content}"