Files
my-pal-mcp-server/tests/test_prompt_regression.py
Fahad 7ea790ef88 fix: Docker path translation for review_changes and code deduplication
- Fixed review_changes tool to properly translate host paths to container paths in Docker
- Prevents "No such file or directory" errors when running in Docker containers
- Added proper error handling with clear messages when paths are inaccessible

refactor: Centralized token limit validation across all tools
- Added _validate_token_limit method to BaseTool to eliminate code duplication
- Reduced ~25 lines of duplicated code across 5 tools (analyze, chat, debug_issue, review_code, think_deeper)
- Maintains exact same error messages and behavior

feat: Enhanced large prompt handling
- Added support for prompts >50K chars by requesting file-based input
- Preserves MCP's ~25K token capacity for responses
- All tools now check prompt size before processing

test: Added comprehensive Docker path integration tests
- Tests for path translation, security validation, and error handling
- Tests for review_changes tool specifically with Docker paths
- Fixed failing think_deeper test (updated default from "max" to "high")

chore: Code quality improvements
- Applied black formatting across all files
- Fixed import sorting with isort
- All tests passing (96 tests)
- Standardized error handling follows MCP TextContent format

The changes ensure consistent behavior across all environments while reducing code duplication and improving maintainability.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-10 07:20:24 +04:00

334 lines
13 KiB
Python

"""
Regression tests to ensure normal prompt handling still works after large prompt changes.
This test module verifies that all tools continue to work correctly with
normal-sized prompts after implementing the large prompt handling feature.
"""
import json
from unittest.mock import MagicMock, patch
import pytest
from tools.analyze import AnalyzeTool
from tools.chat import ChatTool
from tools.debug_issue import DebugIssueTool
from tools.review_changes import ReviewChanges
from tools.review_code import ReviewCodeTool
from tools.think_deeper import ThinkDeeperTool
class TestPromptRegression:
"""Regression test suite for normal prompt handling."""
@pytest.fixture
def mock_model_response(self):
"""Create a mock model response."""
def _create_response(text="Test response"):
mock_response = MagicMock()
mock_response.candidates = [
MagicMock(
content=MagicMock(parts=[MagicMock(text=text)]),
finish_reason="STOP",
)
]
return mock_response
return _create_response
@pytest.mark.asyncio
async def test_chat_normal_prompt(self, mock_model_response):
"""Test chat tool with normal prompt."""
tool = ChatTool()
with patch.object(tool, "create_model") as mock_create_model:
mock_model = MagicMock()
mock_model.generate_content.return_value = mock_model_response(
"This is a helpful response about Python."
)
mock_create_model.return_value = mock_model
result = await tool.execute({"prompt": "Explain Python decorators"})
assert len(result) == 1
output = json.loads(result[0].text)
assert output["status"] == "success"
assert "helpful response about Python" in output["content"]
# Verify model was called
mock_model.generate_content.assert_called_once()
@pytest.mark.asyncio
async def test_chat_with_files(self, mock_model_response):
"""Test chat tool with files parameter."""
tool = ChatTool()
with patch.object(tool, "create_model") as mock_create_model:
mock_model = MagicMock()
mock_model.generate_content.return_value = mock_model_response()
mock_create_model.return_value = mock_model
# Mock file reading
with patch("tools.chat.read_files") as mock_read_files:
mock_read_files.return_value = ("File content here", "Summary")
result = await tool.execute(
{"prompt": "Analyze this code", "files": ["/path/to/file.py"]}
)
assert len(result) == 1
output = json.loads(result[0].text)
assert output["status"] == "success"
mock_read_files.assert_called_once_with(["/path/to/file.py"])
@pytest.mark.asyncio
async def test_think_deeper_normal_analysis(self, mock_model_response):
"""Test think_deeper tool with normal analysis."""
tool = ThinkDeeperTool()
with patch.object(tool, "create_model") as mock_create_model:
mock_model = MagicMock()
mock_model.generate_content.return_value = mock_model_response(
"Here's a deeper analysis with edge cases..."
)
mock_create_model.return_value = mock_model
result = await tool.execute(
{
"current_analysis": "I think we should use a cache for performance",
"problem_context": "Building a high-traffic API",
"focus_areas": ["scalability", "reliability"],
}
)
assert len(result) == 1
output = json.loads(result[0].text)
assert output["status"] == "success"
assert "Extended Analysis by Gemini" in output["content"]
assert "deeper analysis" in output["content"]
@pytest.mark.asyncio
async def test_review_code_normal_review(self, mock_model_response):
"""Test review_code tool with normal inputs."""
tool = ReviewCodeTool()
with patch.object(tool, "create_model") as mock_create_model:
mock_model = MagicMock()
mock_model.generate_content.return_value = mock_model_response(
"Found 3 issues: 1) Missing error handling..."
)
mock_create_model.return_value = mock_model
# Mock file reading
with patch("tools.review_code.read_files") as mock_read_files:
mock_read_files.return_value = ("def main(): pass", "1 file")
result = await tool.execute(
{
"files": ["/path/to/code.py"],
"review_type": "security",
"focus_on": "Look for SQL injection vulnerabilities",
}
)
assert len(result) == 1
output = json.loads(result[0].text)
assert output["status"] == "success"
assert "Found 3 issues" in output["content"]
@pytest.mark.asyncio
async def test_review_changes_normal_request(self, mock_model_response):
"""Test review_changes tool with normal original_request."""
tool = ReviewChanges()
with patch.object(tool, "create_model") as mock_create_model:
mock_model = MagicMock()
mock_model.generate_content.return_value = mock_model_response(
"Changes look good, implementing feature as requested..."
)
mock_create_model.return_value = mock_model
# Mock git operations
with patch("tools.review_changes.find_git_repositories") as mock_find_repos:
with patch("tools.review_changes.get_git_status") as mock_git_status:
mock_find_repos.return_value = ["/path/to/repo"]
mock_git_status.return_value = {
"modified": ["file.py"],
"untracked": [],
}
result = await tool.execute(
{
"path": "/path/to/repo",
"original_request": "Add user authentication feature with JWT tokens",
}
)
assert len(result) == 1
output = json.loads(result[0].text)
assert output["status"] == "success"
@pytest.mark.asyncio
async def test_debug_issue_normal_error(self, mock_model_response):
"""Test debug_issue tool with normal error description."""
tool = DebugIssueTool()
with patch.object(tool, "create_model") as mock_create_model:
mock_model = MagicMock()
mock_model.generate_content.return_value = mock_model_response(
"Root cause: The variable is undefined. Fix: Initialize it..."
)
mock_create_model.return_value = mock_model
result = await tool.execute(
{
"error_description": "TypeError: Cannot read property 'name' of undefined",
"error_context": "at line 42 in user.js\n console.log(user.name)",
"runtime_info": "Node.js v16.14.0",
}
)
assert len(result) == 1
output = json.loads(result[0].text)
assert output["status"] == "success"
assert "Debug Analysis" in output["content"]
assert "Root cause" in output["content"]
@pytest.mark.asyncio
async def test_analyze_normal_question(self, mock_model_response):
"""Test analyze tool with normal question."""
tool = AnalyzeTool()
with patch.object(tool, "create_model") as mock_create_model:
mock_model = MagicMock()
mock_model.generate_content.return_value = mock_model_response(
"The code follows MVC pattern with clear separation..."
)
mock_create_model.return_value = mock_model
# Mock file reading
with patch("tools.analyze.read_files") as mock_read_files:
mock_read_files.return_value = ("class UserController: ...", "3 files")
result = await tool.execute(
{
"files": ["/path/to/project"],
"question": "What design patterns are used in this codebase?",
"analysis_type": "architecture",
}
)
assert len(result) == 1
output = json.loads(result[0].text)
assert output["status"] == "success"
assert "MVC pattern" in output["content"]
@pytest.mark.asyncio
async def test_empty_optional_fields(self, mock_model_response):
"""Test tools work with empty optional fields."""
tool = ChatTool()
with patch.object(tool, "create_model") as mock_create_model:
mock_model = MagicMock()
mock_model.generate_content.return_value = mock_model_response()
mock_create_model.return_value = mock_model
# Test with no files parameter
result = await tool.execute({"prompt": "Hello"})
assert len(result) == 1
output = json.loads(result[0].text)
assert output["status"] == "success"
@pytest.mark.asyncio
async def test_thinking_modes_work(self, mock_model_response):
"""Test that thinking modes are properly passed through."""
tool = ChatTool()
with patch.object(tool, "create_model") as mock_create_model:
mock_model = MagicMock()
mock_model.generate_content.return_value = mock_model_response()
mock_create_model.return_value = mock_model
result = await tool.execute(
{"prompt": "Test", "thinking_mode": "high", "temperature": 0.8}
)
assert len(result) == 1
output = json.loads(result[0].text)
assert output["status"] == "success"
# Verify create_model was called with correct parameters
mock_create_model.assert_called_once()
call_args = mock_create_model.call_args
assert call_args[0][2] == "high" # thinking_mode
assert call_args[0][1] == 0.8 # temperature
@pytest.mark.asyncio
async def test_special_characters_in_prompts(self, mock_model_response):
"""Test prompts with special characters work correctly."""
tool = ChatTool()
with patch.object(tool, "create_model") as mock_create_model:
mock_model = MagicMock()
mock_model.generate_content.return_value = mock_model_response()
mock_create_model.return_value = mock_model
special_prompt = 'Test with "quotes" and\nnewlines\tand tabs'
result = await tool.execute({"prompt": special_prompt})
assert len(result) == 1
output = json.loads(result[0].text)
assert output["status"] == "success"
@pytest.mark.asyncio
async def test_mixed_file_paths(self, mock_model_response):
"""Test handling of various file path formats."""
tool = AnalyzeTool()
with patch.object(tool, "create_model") as mock_create_model:
mock_model = MagicMock()
mock_model.generate_content.return_value = mock_model_response()
mock_create_model.return_value = mock_model
with patch("tools.analyze.read_files") as mock_read_files:
mock_read_files.return_value = ("Content", "Summary")
result = await tool.execute(
{
"files": [
"/absolute/path/file.py",
"/Users/name/project/src/",
"/home/user/code.js",
],
"question": "Analyze these files",
}
)
assert len(result) == 1
output = json.loads(result[0].text)
assert output["status"] == "success"
mock_read_files.assert_called_once()
@pytest.mark.asyncio
async def test_unicode_content(self, mock_model_response):
"""Test handling of unicode content in prompts."""
tool = ChatTool()
with patch.object(tool, "create_model") as mock_create_model:
mock_model = MagicMock()
mock_model.generate_content.return_value = mock_model_response()
mock_create_model.return_value = mock_model
unicode_prompt = "Explain this: 你好世界 مرحبا بالعالم"
result = await tool.execute({"prompt": unicode_prompt})
assert len(result) == 1
output = json.loads(result[0].text)
assert output["status"] == "success"
if __name__ == "__main__":
pytest.main([__file__, "-v"])