"""
Phase 4: Regression Tests for Known Issues
Tests that prevent regressions of previously fixed bugs:
1. response_format only on supported tools
2. GitHub auth in execute_code
3. JSON error responses
4. Parameter validation
"""
import pytest
# Import the MCP server
import sys
from pathlib import Path
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root))
from src.github_mcp import tools as tools_module # noqa: E402
from src.github_mcp.deno_runtime import get_runtime # noqa: E402
def _fix_windows_encoding():
"""Fix Windows console encoding for Unicode output."""
import sys
if sys.platform == "win32":
try:
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
if hasattr(sys.stderr, "reconfigure"):
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
except (AttributeError, ValueError, OSError):
pass
class TestResponseFormatRegression:
"""Regression tests for response_format parameter issues."""
def test_response_format_only_on_supported_tools(self):
"""Regression: response_format was being added to all tools."""
# Verify write operations don't have response_format
write_tools = [
"github_create_release",
"github_update_file",
"github_delete_file",
"github_create_issue",
]
for tool_name in write_tools:
# Get the input model
import inspect
func = getattr(tools_module, tool_name, None)
if not func:
continue
sig = inspect.signature(func)
params = sig.parameters
if "params" in params:
param_type = params["params"].annotation
if inspect.isclass(param_type):
if hasattr(param_type, "model_fields"):
fields = param_type.model_fields
assert "response_format" not in fields, (
f"{tool_name}: Write operation should not have response_format parameter"
)
@pytest.mark.asyncio
async def test_github_get_file_content_no_response_format(self):
"""Regression: github_get_file_content was incorrectly getting response_format."""
_fix_windows_encoding()
runtime = get_runtime()
code = """
// This should work without response_format
const result = await callMCPTool("github_get_file_content", {
owner: "modelcontextprotocol",
repo: "servers",
path: "README.md"
// No response_format parameter
});
return {
success: true,
hasContent: !!result,
isString: typeof result === 'string'
};
"""
result = runtime.execute_code(code)
# Should execute without "Extra inputs" error
if isinstance(result, dict):
assert result.get("error") is False or "data" in result, (
f"Should not require response_format: {result.get('error', 'Unknown error')}"
)
else:
assert result is not None, "execute_code returned None"
class TestAuthRegression:
"""Regression tests for authentication issues."""
@pytest.mark.asyncio
async def test_github_auth_in_execute_code(self):
"""Regression: GitHub auth wasn't passed to execute_code subprocess."""
_fix_windows_encoding()
runtime = get_runtime()
# Note: health_check is no longer an MCP tool - it's internal only
# Test auth by calling a real GitHub tool instead
code = """
// Test that we can call GitHub tools (which requires auth)
const repoInfo = await callMCPTool("github_get_repo_info", {
owner: "facebook",
repo: "react"
});
// If auth works, we'll get repo info (or an error message)
// If auth fails, we'll get an auth error
return {
hasResult: !!repoInfo,
isError: typeof repoInfo === 'string' && repoInfo.toLowerCase().includes('error'),
authWorks: !(typeof repoInfo === 'string' && repoInfo.toLowerCase().includes('authentication'))
};
"""
result = runtime.execute_code(code)
# Should have result (either success or error, but not None)
if isinstance(result, dict):
assert result.get("error") is False or result.get("error") is True, (
f"Tool call failed: {result.get('message', 'Unknown error')}"
)
result_data = result.get("data", {})
# Verify we got a response (auth is working if we get any response)
assert "hasResult" in result_data, "Should have result from tool call"
else:
assert result is not None, "execute_code returned None"
class TestJsonErrorRegression:
"""Regression tests for JSON error responses."""
@pytest.mark.asyncio
async def test_json_error_responses(self):
"""Regression: Errors returned as strings when response_format='json'."""
_fix_windows_encoding()
runtime = get_runtime()
code = """
try {
// Force an error with response_format='json'
const result = await callMCPTool("github_search_code", {
query: "", // Invalid empty query
response_format: "json"
});
// Try to parse as JSON
const parsed = typeof result === 'string' ? JSON.parse(result) : result;
return {
isJson: typeof parsed === 'object',
hasError: !!parsed.error || !!parsed.message,
canParse: true
};
} catch (e) {
return {
isJson: false,
hasError: true,
canParse: false,
error: e.message
};
}
"""
result = runtime.execute_code(code)
# Should handle errors gracefully
assert result.get("error") is False or "error" in result, (
"Should handle JSON errors without crashing"
)
class TestParameterValidationRegression:
"""Regression tests for parameter validation."""
@pytest.mark.asyncio
async def test_extra_parameters_rejected(self):
"""Regression: Extra parameters should be rejected."""
_fix_windows_encoding()
runtime = get_runtime()
code = """
try {
// Try to add an unsupported parameter
await callMCPTool("github_get_file_content", {
owner: "test",
repo: "test",
path: "README.md",
response_format: "json" // This should be rejected
});
return { rejected: false };
} catch (error) {
return {
rejected: true,
errorMessage: error.message || String(error)
};
}
"""
result = runtime.execute_code(code)
# Should reject extra parameters
# Note: Pydantic validation happens server-side
# This test verifies the pattern exists
assert result.get("error") is False or "error" in result, (
"Should handle parameter validation"
)
if __name__ == "__main__":
pytest.main([__file__, "-v"])