Aider MCP Server
- tests
"""Tests for the Aider MCP Server."""
import json
import os
from pathlib import Path
import pytest
import tempfile
import asyncio
import contextlib
import contextvars
from unittest.mock import patch, MagicMock, AsyncMock
from aider_mcp.server import find_git_root, load_aider_config, load_dotenv_file, create_server
from mcp.types import TextContent
from mcp.shared.context import RequestContext
from mcp.shared.session import ServerSession
def test_find_git_root():
"""Test finding a git root directory."""
# Current directory is not a git root
assert find_git_root(os.getcwd()) is not None
def test_load_aider_config():
"""Test loading Aider configuration."""
# Create a temporary config file
with tempfile.NamedTemporaryFile(mode='w+', suffix='.yml', delete=False) as f:
f.write("model: gpt-4\ndark_mode: true\n")
config_file = f.name
try:
# Load the config
config = load_aider_config(config_file=config_file)
# Check that the config contains the expected values
assert "model" in config
assert config["model"] == "gpt-4"
assert "dark_mode" in config
assert config["dark_mode"] is True
finally:
# Clean up
os.unlink(config_file)
def test_load_dotenv_file():
"""Test loading environment variables from .env file."""
# Create a temporary .env file
with tempfile.NamedTemporaryFile(mode='w+', suffix='.env', delete=False) as f:
f.write("TEST_VAR=test_value\nOTHER_VAR=other_value\n")
env_file = f.name
try:
# Load the environment variables
env_vars = load_dotenv_file(env_file=env_file)
# Check that the environment variables were loaded correctly
assert "TEST_VAR" in env_vars
assert env_vars["TEST_VAR"] == "test_value"
assert "OTHER_VAR" in env_vars
assert env_vars["OTHER_VAR"] == "other_value"
finally:
# Clean up
os.unlink(env_file)
def test_create_server():
"""Test creating the MCP server."""
# Create the server
server = create_server()
# Check that the server has the expected attributes
assert server.name == "aider-mcp"
assert hasattr(server, "list_resources")
assert hasattr(server, "read_resource")
assert hasattr(server, "list_tools")
assert hasattr(server, "call_tool")
@contextlib.contextmanager
def mock_request_context(server):
"""Create a mock request context for testing."""
# Create a mock lifespan context
lifespan_context = MagicMock()
lifespan_context.aider_path = "aider"
# Create a mock session
session = ServerSession()
# Create a request context
context = RequestContext(
request_id="test-request-id",
request_meta={},
session=session,
lifespan_context=lifespan_context
)
# Set the context in the server
token = server._routes["request_ctx"].set(context)
try:
yield context
finally:
server._routes["request_ctx"].reset(token)
@pytest.mark.asyncio
async def test_list_tools():
"""Test that the server lists the expected tools."""
# Create the server
server = create_server()
# Use the mock context
with mock_request_context(server):
# Access the list_tools handler directly
handler = server._routes["list_tools"]
# Call the handler function
tools = await handler()
# Get the tool names
tool_names = [tool.name for tool in tools]
# Check that the expected tools are in the list
expected_tools = [
"edit_files",
"create_files",
"git_status",
"extract_code",
"aider_status",
"aider_config"
]
for tool in expected_tools:
assert tool in tool_names
@pytest.mark.asyncio
async def test_list_resources():
"""Test that the server lists the expected resources."""
# Create the server
server = create_server()
# Use the mock context
with mock_request_context(server):
# Access the list_resources handler directly
handler = server._routes["list_resources"]
# Call the handler function
resources = await handler()
# Check that the resources list is not empty
assert len(resources) > 0
# Check that the resources have the required attributes
for resource in resources:
assert hasattr(resource, "uri")
assert hasattr(resource, "name")
@pytest.mark.asyncio
async def test_read_resource_not_found():
"""Test reading a resource that doesn't exist."""
# Create the server
server = create_server()
# Use the mock context
with mock_request_context(server):
# Access the read_resource handler directly
handler = server._routes["read_resource"]
# Call the handler function with an invalid URI
content, content_type = await handler(uri="invalid:uri")
# Check that the response indicates the resource wasn't found
assert "not found" in content.lower()
assert content_type == "text/plain"
@pytest.mark.asyncio
async def test_call_tool_unknown():
"""Test calling an unknown tool."""
# Create the server
server = create_server()
# Use the mock context
with mock_request_context(server):
# Access the call_tool handler directly
handler = server._routes["call_tool"]
# Call the handler function with an unknown tool name
response = await handler(name="unknown_tool", arguments={})
# Check that the response indicates the tool is unknown
assert len(response) == 1
assert response[0].type == "text"
assert "unknown tool" in response[0].text.lower()
@pytest.mark.asyncio
async def test_extract_code_tool():
"""Test the extract_code tool."""
# Create the server
server = create_server()
# Test input with code blocks
test_input = """
Here is some Python code:
```python
def hello_world():
print("Hello, world!")
```
And here's some JavaScript:
```javascript
function greet() {
console.log("Hello!");
}
```
"""
# Use the mock context
with mock_request_context(server):
# Access the call_tool handler directly
handler = server._routes["call_tool"]
# Call the extract_code tool
response = await handler(name="extract_code", arguments={"text": test_input})
# Check the response
assert len(response) > 0
assert response[0].type == "text"
# Response should contain information about the extracted code blocks
assert "python" in response[0].text.lower()
assert "javascript" in response[0].text.lower()
@pytest.mark.asyncio
async def test_aider_config_tool():
"""Test the aider_config tool."""
# Create the server
server = create_server()
# Create a temporary config file
with tempfile.NamedTemporaryFile(mode='w+', suffix='.aider.conf.yml', delete=False) as f:
f.write("model: gpt-4\ndark_mode: true\n")
temp_dir = os.path.dirname(f.name)
try:
# Use the mock context
with mock_request_context(server):
# Mock the load_aider_config function to return our test config
with patch('aider_mcp.server.load_aider_config') as mock_load_config:
mock_load_config.return_value = {"model": "gpt-4", "dark_mode": True}
# Access the call_tool handler directly
handler = server._routes["call_tool"]
# Call the aider_config tool
response = await handler(name="aider_config", arguments={"directory": temp_dir})
# Check the response
assert len(response) > 0
assert response[0].type == "text"
# Parse the JSON response
result = json.loads(response[0].text)
# Verify that the config is in the response
assert "config" in result
assert result["config"]["model"] == "gpt-4"
assert result["config"]["dark_mode"] is True
finally:
# Clean up
os.unlink(f.name)
@pytest.mark.asyncio
async def test_git_status_tool():
"""Test the git_status tool."""
# Create the server
server = create_server()
# Use the mock context
with mock_request_context(server):
# Mock the run_command function to return a git status output
with patch('aider_mcp.server.run_command') as mock_run_command:
mock_run_command.return_value = (
"On branch main\nYour branch is up to date with 'origin/main'.\n\n"
"Changes not staged for commit:\n"
" (use \"git add <file>...\" to update what will be committed)\n"
" (use \"git restore <file>...\" to discard changes in working directory)\n"
" modified: README.md\n\n"
"Untracked files:\n"
" (use \"git add <file>...\" to include in what will be committed)\n"
" new_file.txt\n\n",
""
)
# Access the call_tool handler directly
handler = server._routes["call_tool"]
# Call the git_status tool
response = await handler(name="git_status", arguments={"directory": os.getcwd()})
# Check the response
assert len(response) > 0
assert response[0].type == "text"
# Response should contain information about the git status
assert "branch main" in response[0].text.lower()
assert "modified" in response[0].text.lower()
assert "untracked" in response[0].text.lower()
@pytest.mark.asyncio
async def test_create_files_tool():
"""Test the create_files tool."""
# Create the server
server = create_server()
# Create a temporary directory
with tempfile.TemporaryDirectory() as temp_dir:
# Use the mock context
with mock_request_context(server):
# Mock the run_command function to simulate successful git operations
with patch('aider_mcp.server.run_command') as mock_run_command:
mock_run_command.return_value = ("", "")
# Files to create
files = {
"test_file.py": "print('Hello, world!')",
"another_file.txt": "This is a test file."
}
# Access the call_tool handler directly
handler = server._routes["call_tool"]
# Call the create_files tool
response = await handler(
name="create_files",
arguments={
"directory": temp_dir,
"files": files,
"message": "Add test files",
"git_commit": True
}
)
# Check the response
assert len(response) > 0
assert response[0].type == "text"
# Response should indicate success
assert "created" in response[0].text.lower()
# Check that the files were actually created
for filename, content in files.items():
file_path = os.path.join(temp_dir, filename)
assert os.path.exists(file_path)
with open(file_path, 'r') as f:
assert f.read() == content
@pytest.mark.asyncio
async def test_aider_status_tool():
"""Test the aider_status tool."""
# Create the server
server = create_server()
# Use the mock context
with mock_request_context(server):
# Mock the run_command function to return a version string
with patch('aider_mcp.server.run_command') as mock_run_command:
mock_run_command.return_value = ("aider 0.25.0\n", "")
# Mock the load_aider_config function
with patch('aider_mcp.server.load_aider_config') as mock_load_config:
mock_load_config.return_value = {"model": "gpt-4", "dark_mode": True}
# Mock the load_dotenv_file function
with patch('aider_mcp.server.load_dotenv_file') as mock_load_env:
mock_load_env.return_value = {"OPENAI_API_KEY": "sk-..."}
# Access the call_tool handler directly
handler = server._routes["call_tool"]
# Call the aider_status tool
response = await handler(
name="aider_status",
arguments={
"directory": os.getcwd(),
"check_environment": True
}
)
# Check the response
assert len(response) > 0
assert response[0].type == "text"
# Parse the JSON response
result = json.loads(response[0].text)
# Verify the response contains expected information
assert "aider_version" in result
assert result["aider_version"] == "aider 0.25.0"
assert "config" in result
assert "environment" in result
assert "api_keys" in result["environment"]
@pytest.mark.asyncio
async def test_edit_files_tool():
"""Test the edit_files tool."""
# Create the server
server = create_server()
# Create a temporary directory with a test file
with tempfile.TemporaryDirectory() as temp_dir:
# Create a test file to edit
test_file = os.path.join(temp_dir, "test_file.py")
with open(test_file, 'w') as f:
f.write("def hello():\n print('Hello')\n")
# Use the mock context
with mock_request_context(server):
# Define the expected output from running aider
aider_output = (
"Aider: I'll help you update the hello function to include a world parameter.\n\n"
"I've made the following changes to test_file.py:\n\n"
"```diff\n"
"- def hello():\n"
"- print('Hello')\n"
"+ def hello(world='world'):\n"
"+ print(f'Hello, {world}!')\n"
"```\n\n"
"Committed as: Updated hello function with world parameter\n",
""
)
# Mock the run_command function to return the expected output
with patch('aider_mcp.server.run_command', new_callable=AsyncMock) as mock_run:
mock_run.return_value = aider_output
# Access the call_tool handler directly
handler = server._routes["call_tool"]
# Call the edit_files tool
response = await handler(
name="edit_files",
arguments={
"directory": temp_dir,
"message": "Update the hello function to include a world parameter"
}
)
# Check the response format
assert len(response) > 0
assert response[0].type == "text"
# Verify the aider output is included in the response
response_text = response[0].text
assert "updated hello function" in response_text.lower()
# Check that mock_run was called with the expected arguments
mock_run.assert_called_once()
# We expect the command to include 'aider' and the message
args = mock_run.call_args[0][0]
assert 'aider' in args[0].lower() or args[0].endswith('aider')
message_file_content = ""
# The message should be written to a file which is passed to aider
with open(args[-1], 'r') as f:
message_file_content = f.read()
assert "world parameter" in message_file_content