🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
255 lines
8.9 KiB
Python
255 lines
8.9 KiB
Python
"""
|
|
Tests for the review_pending_changes tool
|
|
"""
|
|
|
|
import json
|
|
from unittest.mock import MagicMock, Mock, patch
|
|
|
|
import pytest
|
|
|
|
from tools.review_pending_changes import (
|
|
ReviewPendingChanges,
|
|
ReviewPendingChangesRequest,
|
|
)
|
|
|
|
|
|
class TestReviewPendingChangesTool:
|
|
"""Test the review_pending_changes tool"""
|
|
|
|
@pytest.fixture
|
|
def tool(self):
|
|
"""Create tool instance"""
|
|
return ReviewPendingChanges()
|
|
|
|
def test_tool_metadata(self, tool):
|
|
"""Test tool metadata"""
|
|
assert tool.get_name() == "review_pending_changes"
|
|
assert "REVIEW PENDING GIT CHANGES" in tool.get_description()
|
|
assert "pre-commit review" in tool.get_description()
|
|
|
|
# Check schema
|
|
schema = tool.get_input_schema()
|
|
assert schema["type"] == "object"
|
|
assert "path" in schema["properties"]
|
|
assert "original_request" in schema["properties"]
|
|
assert "compare_to" in schema["properties"]
|
|
assert "review_type" in schema["properties"]
|
|
|
|
def test_request_model_defaults(self):
|
|
"""Test request model default values"""
|
|
request = ReviewPendingChangesRequest(path="/some/absolute/path")
|
|
assert request.path == "/some/absolute/path"
|
|
assert request.original_request is None
|
|
assert request.compare_to is None
|
|
assert request.include_staged is True
|
|
assert request.include_unstaged is True
|
|
assert request.review_type == "full"
|
|
assert request.severity_filter == "all"
|
|
assert request.max_depth == 5
|
|
|
|
def test_sanitize_filename(self, tool):
|
|
"""Test filename sanitization"""
|
|
# Test path separators
|
|
assert tool._sanitize_filename("src/main.py") == "src_main.py"
|
|
assert tool._sanitize_filename("src\\main.py") == "src_main.py"
|
|
|
|
# Test spaces
|
|
assert tool._sanitize_filename("my file.py") == "my_file.py"
|
|
|
|
# Test special characters
|
|
assert tool._sanitize_filename("file@#$.py") == "file.py"
|
|
|
|
# Test length limit
|
|
long_name = "a" * 150
|
|
sanitized = tool._sanitize_filename(long_name)
|
|
assert len(sanitized) == 100
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_relative_path_rejected(self, tool):
|
|
"""Test that relative paths are rejected"""
|
|
result = await tool.execute(
|
|
{"path": "./relative/path", "original_request": "Test"}
|
|
)
|
|
assert len(result) == 1
|
|
response = json.loads(result[0].text)
|
|
assert response["status"] == "error"
|
|
assert "must be absolute" in response["content"]
|
|
assert "./relative/path" in response["content"]
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("tools.review_pending_changes.find_git_repositories")
|
|
async def test_no_repositories_found(self, mock_find_repos, tool):
|
|
"""Test when no git repositories are found"""
|
|
mock_find_repos.return_value = []
|
|
|
|
request = ReviewPendingChangesRequest(path="/absolute/path/no-git")
|
|
result = await tool.prepare_prompt(request)
|
|
|
|
assert result == "No git repositories found in the specified path."
|
|
mock_find_repos.assert_called_once_with("/absolute/path/no-git", 5)
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("tools.review_pending_changes.find_git_repositories")
|
|
@patch("tools.review_pending_changes.get_git_status")
|
|
@patch("tools.review_pending_changes.run_git_command")
|
|
async def test_no_changes_found(
|
|
self, mock_run_git, mock_status, mock_find_repos, tool
|
|
):
|
|
"""Test when repositories have no changes"""
|
|
mock_find_repos.return_value = ["/test/repo"]
|
|
mock_status.return_value = {
|
|
"branch": "main",
|
|
"ahead": 0,
|
|
"behind": 0,
|
|
"staged_files": [],
|
|
"unstaged_files": [],
|
|
"untracked_files": [],
|
|
}
|
|
|
|
# No staged or unstaged files
|
|
mock_run_git.side_effect = [
|
|
(True, ""), # staged files (empty)
|
|
(True, ""), # unstaged files (empty)
|
|
]
|
|
|
|
request = ReviewPendingChangesRequest(path="/absolute/repo/path")
|
|
result = await tool.prepare_prompt(request)
|
|
|
|
assert result == "No pending changes found in any of the git repositories."
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("tools.review_pending_changes.find_git_repositories")
|
|
@patch("tools.review_pending_changes.get_git_status")
|
|
@patch("tools.review_pending_changes.run_git_command")
|
|
async def test_staged_changes_review(
|
|
self,
|
|
mock_run_git,
|
|
mock_status,
|
|
mock_find_repos,
|
|
tool,
|
|
):
|
|
"""Test reviewing staged changes"""
|
|
mock_find_repos.return_value = ["/test/repo"]
|
|
mock_status.return_value = {
|
|
"branch": "feature",
|
|
"ahead": 1,
|
|
"behind": 0,
|
|
"staged_files": ["main.py"],
|
|
"unstaged_files": [],
|
|
"untracked_files": [],
|
|
}
|
|
|
|
# Mock git commands
|
|
mock_run_git.side_effect = [
|
|
(True, "main.py\n"), # staged files
|
|
(
|
|
True,
|
|
"diff --git a/main.py b/main.py\n+print('hello')",
|
|
), # diff for main.py
|
|
(True, ""), # unstaged files (empty)
|
|
]
|
|
|
|
request = ReviewPendingChangesRequest(
|
|
path="/absolute/repo/path",
|
|
original_request="Add hello message",
|
|
review_type="security",
|
|
)
|
|
result = await tool.prepare_prompt(request)
|
|
|
|
# Verify result structure
|
|
assert "## Original Request/Ticket" in result
|
|
assert "Add hello message" in result
|
|
assert "## Review Parameters" in result
|
|
assert "Review Type: security" in result
|
|
assert "## Repository Changes Summary" in result
|
|
assert "Branch: feature" in result
|
|
assert "## Git Diffs" in result
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("tools.review_pending_changes.find_git_repositories")
|
|
@patch("tools.review_pending_changes.get_git_status")
|
|
@patch("tools.review_pending_changes.run_git_command")
|
|
async def test_compare_to_invalid_ref(
|
|
self, mock_run_git, mock_status, mock_find_repos, tool
|
|
):
|
|
"""Test comparing to an invalid git ref"""
|
|
mock_find_repos.return_value = ["/test/repo"]
|
|
mock_status.return_value = {"branch": "main"}
|
|
|
|
# Mock git commands - ref validation fails
|
|
mock_run_git.side_effect = [
|
|
(False, "fatal: not a valid ref"), # rev-parse fails
|
|
]
|
|
|
|
request = ReviewPendingChangesRequest(
|
|
path="/absolute/repo/path", compare_to="invalid-branch"
|
|
)
|
|
result = await tool.prepare_prompt(request)
|
|
|
|
# When all repos have errors and no changes, we get this message
|
|
assert "No pending changes found in any of the git repositories." in result
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("tools.review_pending_changes.ReviewPendingChanges.execute")
|
|
async def test_execute_integration(self, mock_execute, tool):
|
|
"""Test execute method integration"""
|
|
# Mock the execute to return a standardized response
|
|
mock_execute.return_value = [
|
|
Mock(
|
|
text='{"status": "success", "content": "Review complete", "content_type": "text"}'
|
|
)
|
|
]
|
|
|
|
result = await tool.execute({"path": ".", "review_type": "full"})
|
|
|
|
assert len(result) == 1
|
|
mock_execute.assert_called_once()
|
|
|
|
def test_default_temperature(self, tool):
|
|
"""Test default temperature setting"""
|
|
from config import TEMPERATURE_ANALYTICAL
|
|
|
|
assert tool.get_default_temperature() == TEMPERATURE_ANALYTICAL
|
|
|
|
@pytest.mark.asyncio
|
|
@patch("tools.review_pending_changes.find_git_repositories")
|
|
@patch("tools.review_pending_changes.get_git_status")
|
|
@patch("tools.review_pending_changes.run_git_command")
|
|
async def test_mixed_staged_unstaged_changes(
|
|
self,
|
|
mock_run_git,
|
|
mock_status,
|
|
mock_find_repos,
|
|
tool,
|
|
):
|
|
"""Test reviewing both staged and unstaged changes"""
|
|
mock_find_repos.return_value = ["/test/repo"]
|
|
mock_status.return_value = {
|
|
"branch": "develop",
|
|
"ahead": 2,
|
|
"behind": 1,
|
|
"staged_files": ["file1.py"],
|
|
"unstaged_files": ["file2.py"],
|
|
}
|
|
|
|
# Mock git commands
|
|
mock_run_git.side_effect = [
|
|
(True, "file1.py\n"), # staged files
|
|
(True, "diff --git a/file1.py..."), # diff for file1.py
|
|
(True, "file2.py\n"), # unstaged files
|
|
(True, "diff --git a/file2.py..."), # diff for file2.py
|
|
]
|
|
|
|
request = ReviewPendingChangesRequest(
|
|
path="/absolute/repo/path",
|
|
focus_on="error handling",
|
|
severity_filter="high",
|
|
)
|
|
result = await tool.prepare_prompt(request)
|
|
|
|
# Verify all sections are present
|
|
assert "Review Type: full" in result
|
|
assert "Severity Filter: high" in result
|
|
assert "Focus Areas: error handling" in result
|
|
assert "Reviewing: staged and unstaged changes" in result
|