mirror of
https://github.com/coleam00/Archon.git
synced 2025-12-24 02:39:17 -05:00
style: Apply linting fixes and formatting
Applied automated linting and formatting: - Fixed missing newlines at end of files - Adjusted line wrapping for better readability - Fixed multi-line string formatting in tests - No functional changes, only style improvements All 43 tests still passing after formatting changes.
This commit is contained in:
@@ -157,9 +157,7 @@ def register_document_tools(mcp: FastMCP):
|
||||
return MCPErrorFormatter.from_http_error(response, "list documents")
|
||||
|
||||
except httpx.RequestError as e:
|
||||
return MCPErrorFormatter.from_exception(
|
||||
e, "list documents", {"project_id": project_id}
|
||||
)
|
||||
return MCPErrorFormatter.from_exception(e, "list documents", {"project_id": project_id})
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing documents: {e}", exc_info=True)
|
||||
return MCPErrorFormatter.from_exception(e, "list documents")
|
||||
|
||||
@@ -81,7 +81,7 @@ def register_version_tools(mcp: FastMCP):
|
||||
change_summary="Updated user guide"
|
||||
)
|
||||
|
||||
# Version features
|
||||
# Version features
|
||||
create_version(
|
||||
project_id="550e8400-e29b-41d4-a716-446655440000",
|
||||
field_name="features",
|
||||
@@ -113,7 +113,7 @@ def register_version_tools(mcp: FastMCP):
|
||||
"success": True,
|
||||
"version": result.get("version"),
|
||||
"version_number": version_num,
|
||||
"message": f"Version {version_num} created successfully for {field_name} field"
|
||||
"message": f"Version {version_num} created successfully for {field_name} field",
|
||||
})
|
||||
elif response.status_code == 400:
|
||||
error_text = response.text.lower()
|
||||
@@ -122,14 +122,14 @@ def register_version_tools(mcp: FastMCP):
|
||||
error_type="validation_error",
|
||||
message=f"Invalid field_name '{field_name}'. Must be one of: docs, features, data, or prd",
|
||||
suggestion="Use one of the valid field names: docs, features, data, or prd",
|
||||
http_status=400
|
||||
http_status=400,
|
||||
)
|
||||
elif "content" in error_text and "required" in error_text:
|
||||
return MCPErrorFormatter.format_error(
|
||||
error_type="validation_error",
|
||||
message="Content is required and cannot be empty. Provide the complete data to version.",
|
||||
suggestion="Provide the complete data to version",
|
||||
http_status=400
|
||||
http_status=400,
|
||||
)
|
||||
elif "format" in error_text or "type" in error_text:
|
||||
if field_name == "docs":
|
||||
@@ -137,14 +137,14 @@ def register_version_tools(mcp: FastMCP):
|
||||
error_type="validation_error",
|
||||
message=f"For field_name='docs', content must be an array. Example: [{{'id': 'doc1', 'title': 'Guide', 'content': {{...}}}}]",
|
||||
suggestion="Ensure content is an array of document objects",
|
||||
http_status=400
|
||||
http_status=400,
|
||||
)
|
||||
else:
|
||||
return MCPErrorFormatter.format_error(
|
||||
error_type="validation_error",
|
||||
message=f"For field_name='{field_name}', content must be a dictionary/object. Example: {{'key': 'value'}}",
|
||||
suggestion="Ensure content is a dictionary/object",
|
||||
http_status=400
|
||||
http_status=400,
|
||||
)
|
||||
return MCPErrorFormatter.format_error(
|
||||
error_type="validation_error",
|
||||
@@ -171,11 +171,7 @@ def register_version_tools(mcp: FastMCP):
|
||||
return MCPErrorFormatter.from_exception(e, "create version")
|
||||
|
||||
@mcp.tool()
|
||||
async def list_versions(
|
||||
ctx: Context,
|
||||
project_id: str,
|
||||
field_name: Optional[str] = None
|
||||
) -> str:
|
||||
async def list_versions(ctx: Context, project_id: str, field_name: Optional[str] = None) -> str:
|
||||
"""
|
||||
List version history for a project.
|
||||
|
||||
@@ -199,8 +195,7 @@ def register_version_tools(mcp: FastMCP):
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
response = await client.get(
|
||||
urljoin(api_url, f"/api/projects/{project_id}/versions"),
|
||||
params=params
|
||||
urljoin(api_url, f"/api/projects/{project_id}/versions"), params=params
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
@@ -208,7 +203,7 @@ def register_version_tools(mcp: FastMCP):
|
||||
return json.dumps({
|
||||
"success": True,
|
||||
"versions": result.get("versions", []),
|
||||
"count": len(result.get("versions", []))
|
||||
"count": len(result.get("versions", [])),
|
||||
})
|
||||
else:
|
||||
return MCPErrorFormatter.from_http_error(response, "list versions")
|
||||
@@ -223,10 +218,7 @@ def register_version_tools(mcp: FastMCP):
|
||||
|
||||
@mcp.tool()
|
||||
async def get_version(
|
||||
ctx: Context,
|
||||
project_id: str,
|
||||
field_name: str,
|
||||
version_number: int
|
||||
ctx: Context, project_id: str, field_name: str, version_number: int
|
||||
) -> str:
|
||||
"""
|
||||
Get detailed information about a specific version.
|
||||
@@ -248,7 +240,10 @@ def register_version_tools(mcp: FastMCP):
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
response = await client.get(
|
||||
urljoin(api_url, f"/api/projects/{project_id}/versions/{field_name}/{version_number}")
|
||||
urljoin(
|
||||
api_url,
|
||||
f"/api/projects/{project_id}/versions/{field_name}/{version_number}",
|
||||
)
|
||||
)
|
||||
|
||||
if response.status_code == 200:
|
||||
@@ -256,7 +251,7 @@ def register_version_tools(mcp: FastMCP):
|
||||
return json.dumps({
|
||||
"success": True,
|
||||
"version": result.get("version"),
|
||||
"content": result.get("content")
|
||||
"content": result.get("content"),
|
||||
})
|
||||
elif response.status_code == 404:
|
||||
return MCPErrorFormatter.format_error(
|
||||
@@ -270,8 +265,13 @@ def register_version_tools(mcp: FastMCP):
|
||||
|
||||
except httpx.RequestError as e:
|
||||
return MCPErrorFormatter.from_exception(
|
||||
e, "get version",
|
||||
{"project_id": project_id, "field_name": field_name, "version_number": version_number}
|
||||
e,
|
||||
"get version",
|
||||
{
|
||||
"project_id": project_id,
|
||||
"field_name": field_name,
|
||||
"version_number": version_number,
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting version: {e}", exc_info=True)
|
||||
@@ -290,7 +290,7 @@ def register_version_tools(mcp: FastMCP):
|
||||
|
||||
Args:
|
||||
project_id: Project UUID (required)
|
||||
field_name: Field name - "docs", "features", "data", "prd" (required)
|
||||
field_name: Field name - "docs", "features", "data", "prd" (required)
|
||||
version_number: Version number to restore (required)
|
||||
restored_by: Identifier of who is restoring (optional, defaults to "system")
|
||||
|
||||
@@ -306,7 +306,10 @@ def register_version_tools(mcp: FastMCP):
|
||||
|
||||
async with httpx.AsyncClient(timeout=timeout) as client:
|
||||
response = await client.post(
|
||||
urljoin(api_url, f"/api/projects/{project_id}/versions/{field_name}/{version_number}/restore"),
|
||||
urljoin(
|
||||
api_url,
|
||||
f"/api/projects/{project_id}/versions/{field_name}/{version_number}/restore",
|
||||
),
|
||||
json={"restored_by": restored_by},
|
||||
)
|
||||
|
||||
@@ -314,7 +317,9 @@ def register_version_tools(mcp: FastMCP):
|
||||
result = response.json()
|
||||
return json.dumps({
|
||||
"success": True,
|
||||
"message": result.get("message", f"Version {version_number} restored successfully")
|
||||
"message": result.get(
|
||||
"message", f"Version {version_number} restored successfully"
|
||||
),
|
||||
})
|
||||
elif response.status_code == 404:
|
||||
return MCPErrorFormatter.format_error(
|
||||
@@ -328,9 +333,14 @@ def register_version_tools(mcp: FastMCP):
|
||||
|
||||
except httpx.RequestError as e:
|
||||
return MCPErrorFormatter.from_exception(
|
||||
e, "restore version",
|
||||
{"project_id": project_id, "field_name": field_name, "version_number": version_number}
|
||||
e,
|
||||
"restore version",
|
||||
{
|
||||
"project_id": project_id,
|
||||
"field_name": field_name,
|
||||
"version_number": version_number,
|
||||
},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error restoring version: {e}", exc_info=True)
|
||||
return MCPErrorFormatter.from_exception(e, "restore version")
|
||||
return MCPErrorFormatter.from_exception(e, "restore version")
|
||||
|
||||
@@ -88,18 +88,22 @@ def register_project_tools(mcp: FastMCP):
|
||||
# Poll for completion with proper error handling and backoff
|
||||
max_attempts = get_max_polling_attempts()
|
||||
polling_timeout = get_polling_timeout()
|
||||
|
||||
|
||||
for attempt in range(max_attempts):
|
||||
try:
|
||||
# Exponential backoff
|
||||
sleep_interval = get_polling_interval(attempt)
|
||||
await asyncio.sleep(sleep_interval)
|
||||
|
||||
|
||||
# Create new client with polling timeout
|
||||
async with httpx.AsyncClient(timeout=polling_timeout) as poll_client:
|
||||
list_response = await poll_client.get(urljoin(api_url, "/api/projects"))
|
||||
async with httpx.AsyncClient(
|
||||
timeout=polling_timeout
|
||||
) as poll_client:
|
||||
list_response = await poll_client.get(
|
||||
urljoin(api_url, "/api/projects")
|
||||
)
|
||||
list_response.raise_for_status() # Raise on HTTP errors
|
||||
|
||||
|
||||
projects = list_response.json()
|
||||
# Find project with matching title created recently
|
||||
for proj in projects:
|
||||
@@ -110,9 +114,11 @@ def register_project_tools(mcp: FastMCP):
|
||||
"project_id": proj["id"],
|
||||
"message": f"Project created successfully with ID: {proj['id']}",
|
||||
})
|
||||
|
||||
|
||||
except httpx.RequestError as poll_error:
|
||||
logger.warning(f"Polling attempt {attempt + 1}/{max_attempts} failed: {poll_error}")
|
||||
logger.warning(
|
||||
f"Polling attempt {attempt + 1}/{max_attempts} failed: {poll_error}"
|
||||
)
|
||||
if attempt == max_attempts - 1: # Last attempt
|
||||
return MCPErrorFormatter.format_error(
|
||||
error_type="polling_timeout",
|
||||
@@ -125,7 +131,9 @@ def register_project_tools(mcp: FastMCP):
|
||||
suggestion="The project may still be creating. Use list_projects to check status",
|
||||
)
|
||||
except Exception as poll_error:
|
||||
logger.warning(f"Unexpected error during polling attempt {attempt + 1}: {poll_error}")
|
||||
logger.warning(
|
||||
f"Unexpected error during polling attempt {attempt + 1}: {poll_error}"
|
||||
)
|
||||
|
||||
# If we couldn't find it after polling
|
||||
return json.dumps({
|
||||
|
||||
@@ -22,6 +22,7 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class TaskUpdateFields(TypedDict, total=False):
|
||||
"""Valid fields that can be updated on a task."""
|
||||
|
||||
title: str
|
||||
description: str
|
||||
status: str # "todo" | "doing" | "review" | "done"
|
||||
@@ -263,7 +264,9 @@ def register_task_tools(mcp: FastMCP):
|
||||
})
|
||||
|
||||
except httpx.RequestError as e:
|
||||
return MCPErrorFormatter.from_exception(e, "list tasks", {"filter_by": filter_by, "filter_value": filter_value})
|
||||
return MCPErrorFormatter.from_exception(
|
||||
e, "list tasks", {"filter_by": filter_by, "filter_value": filter_value}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing tasks: {e}", exc_info=True)
|
||||
return MCPErrorFormatter.from_exception(e, "list tasks")
|
||||
@@ -393,8 +396,8 @@ def register_task_tools(mcp: FastMCP):
|
||||
})
|
||||
elif response.status_code == 404:
|
||||
return json.dumps({
|
||||
"success": False,
|
||||
"error": f"Task {task_id} not found. Use list_tasks to find valid task IDs."
|
||||
"success": False,
|
||||
"error": f"Task {task_id} not found. Use list_tasks to find valid task IDs.",
|
||||
})
|
||||
elif response.status_code == 400:
|
||||
# More specific error for bad requests
|
||||
@@ -420,4 +423,3 @@ def register_task_tools(mcp: FastMCP):
|
||||
except Exception as e:
|
||||
logger.error(f"Error deleting task: {e}", exc_info=True)
|
||||
return MCPErrorFormatter.from_exception(e, "delete task")
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
"""MCP server tests."""
|
||||
"""MCP server tests."""
|
||||
|
||||
@@ -1 +1 @@
|
||||
"""MCP server features tests."""
|
||||
"""MCP server features tests."""
|
||||
|
||||
@@ -1 +1 @@
|
||||
"""Document and version tools tests."""
|
||||
"""Document and version tools tests."""
|
||||
|
||||
@@ -15,13 +15,14 @@ def mock_mcp():
|
||||
mock = MagicMock()
|
||||
# Store registered tools
|
||||
mock._tools = {}
|
||||
|
||||
|
||||
def tool_decorator():
|
||||
def decorator(func):
|
||||
mock._tools[func.__name__] = func
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
mock.tool = tool_decorator
|
||||
return mock
|
||||
|
||||
@@ -37,33 +38,33 @@ async def test_create_document_success(mock_mcp, mock_context):
|
||||
"""Test successful document creation."""
|
||||
# Register tools with mock MCP
|
||||
register_document_tools(mock_mcp)
|
||||
|
||||
|
||||
# Get the create_document function from registered tools
|
||||
create_document = mock_mcp._tools.get('create_document')
|
||||
create_document = mock_mcp._tools.get("create_document")
|
||||
assert create_document is not None, "create_document tool not registered"
|
||||
|
||||
|
||||
# Mock HTTP response
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"document": {"id": "doc-123", "title": "Test Doc"},
|
||||
"message": "Document created successfully"
|
||||
"message": "Document created successfully",
|
||||
}
|
||||
|
||||
with patch('src.mcp_server.features.documents.document_tools.httpx.AsyncClient') as mock_client:
|
||||
|
||||
with patch("src.mcp_server.features.documents.document_tools.httpx.AsyncClient") as mock_client:
|
||||
mock_async_client = AsyncMock()
|
||||
mock_async_client.post.return_value = mock_response
|
||||
mock_client.return_value.__aenter__.return_value = mock_async_client
|
||||
|
||||
|
||||
# Test the function
|
||||
result = await create_document(
|
||||
mock_context,
|
||||
project_id="project-123",
|
||||
title="Test Document",
|
||||
document_type="spec",
|
||||
content={"test": "content"}
|
||||
content={"test": "content"},
|
||||
)
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is True
|
||||
assert result_data["document_id"] == "doc-123"
|
||||
@@ -74,28 +75,28 @@ async def test_create_document_success(mock_mcp, mock_context):
|
||||
async def test_list_documents_success(mock_mcp, mock_context):
|
||||
"""Test successful document listing."""
|
||||
register_document_tools(mock_mcp)
|
||||
|
||||
|
||||
# Get the list_documents function from registered tools
|
||||
list_documents = mock_mcp._tools.get('list_documents')
|
||||
list_documents = mock_mcp._tools.get("list_documents")
|
||||
assert list_documents is not None, "list_documents tool not registered"
|
||||
|
||||
|
||||
# Mock HTTP response
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"documents": [
|
||||
{"id": "doc-1", "title": "Doc 1", "document_type": "spec"},
|
||||
{"id": "doc-2", "title": "Doc 2", "document_type": "design"}
|
||||
{"id": "doc-2", "title": "Doc 2", "document_type": "design"},
|
||||
]
|
||||
}
|
||||
|
||||
with patch('src.mcp_server.features.documents.document_tools.httpx.AsyncClient') as mock_client:
|
||||
|
||||
with patch("src.mcp_server.features.documents.document_tools.httpx.AsyncClient") as mock_client:
|
||||
mock_async_client = AsyncMock()
|
||||
mock_async_client.get.return_value = mock_response
|
||||
mock_client.return_value.__aenter__.return_value = mock_async_client
|
||||
|
||||
|
||||
result = await list_documents(mock_context, project_id="project-123")
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is True
|
||||
assert len(result_data["documents"]) == 2
|
||||
@@ -106,36 +107,33 @@ async def test_list_documents_success(mock_mcp, mock_context):
|
||||
async def test_update_document_partial_update(mock_mcp, mock_context):
|
||||
"""Test partial document update."""
|
||||
register_document_tools(mock_mcp)
|
||||
|
||||
|
||||
# Get the update_document function from registered tools
|
||||
update_document = mock_mcp._tools.get('update_document')
|
||||
update_document = mock_mcp._tools.get("update_document")
|
||||
assert update_document is not None, "update_document tool not registered"
|
||||
|
||||
|
||||
# Mock HTTP response
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"doc": {"id": "doc-123", "title": "Updated Title"},
|
||||
"message": "Document updated successfully"
|
||||
"message": "Document updated successfully",
|
||||
}
|
||||
|
||||
with patch('src.mcp_server.features.documents.document_tools.httpx.AsyncClient') as mock_client:
|
||||
|
||||
with patch("src.mcp_server.features.documents.document_tools.httpx.AsyncClient") as mock_client:
|
||||
mock_async_client = AsyncMock()
|
||||
mock_async_client.put.return_value = mock_response
|
||||
mock_client.return_value.__aenter__.return_value = mock_async_client
|
||||
|
||||
|
||||
# Update only title
|
||||
result = await update_document(
|
||||
mock_context,
|
||||
project_id="project-123",
|
||||
doc_id="doc-123",
|
||||
title="Updated Title"
|
||||
mock_context, project_id="project-123", doc_id="doc-123", title="Updated Title"
|
||||
)
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is True
|
||||
assert "Document updated successfully" in result_data["message"]
|
||||
|
||||
|
||||
# Verify only title was sent in update
|
||||
call_args = mock_async_client.put.call_args
|
||||
sent_data = call_args[1]["json"]
|
||||
@@ -146,31 +144,31 @@ async def test_update_document_partial_update(mock_mcp, mock_context):
|
||||
async def test_delete_document_not_found(mock_mcp, mock_context):
|
||||
"""Test deleting a non-existent document."""
|
||||
register_document_tools(mock_mcp)
|
||||
|
||||
|
||||
# Get the delete_document function from registered tools
|
||||
delete_document = mock_mcp._tools.get('delete_document')
|
||||
delete_document = mock_mcp._tools.get("delete_document")
|
||||
assert delete_document is not None, "delete_document tool not registered"
|
||||
|
||||
|
||||
# Mock 404 response
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 404
|
||||
mock_response.text = "Document not found"
|
||||
|
||||
with patch('src.mcp_server.features.documents.document_tools.httpx.AsyncClient') as mock_client:
|
||||
|
||||
with patch("src.mcp_server.features.documents.document_tools.httpx.AsyncClient") as mock_client:
|
||||
mock_async_client = AsyncMock()
|
||||
mock_async_client.delete.return_value = mock_response
|
||||
mock_client.return_value.__aenter__.return_value = mock_async_client
|
||||
|
||||
|
||||
result = await delete_document(
|
||||
mock_context,
|
||||
project_id="project-123",
|
||||
doc_id="non-existent"
|
||||
mock_context, project_id="project-123", doc_id="non-existent"
|
||||
)
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is False
|
||||
# Error must be structured format (dict), not string
|
||||
assert "error" in result_data
|
||||
assert isinstance(result_data["error"], dict), "Error should be structured format, not string"
|
||||
assert isinstance(result_data["error"], dict), (
|
||||
"Error should be structured format, not string"
|
||||
)
|
||||
assert result_data["error"]["type"] == "not_found"
|
||||
assert "not found" in result_data["error"]["message"].lower()
|
||||
assert "not found" in result_data["error"]["message"].lower()
|
||||
|
||||
@@ -15,13 +15,14 @@ def mock_mcp():
|
||||
mock = MagicMock()
|
||||
# Store registered tools
|
||||
mock._tools = {}
|
||||
|
||||
|
||||
def tool_decorator():
|
||||
def decorator(func):
|
||||
mock._tools[func.__name__] = func
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
mock.tool = tool_decorator
|
||||
return mock
|
||||
|
||||
@@ -36,33 +37,33 @@ def mock_context():
|
||||
async def test_create_version_success(mock_mcp, mock_context):
|
||||
"""Test successful version creation."""
|
||||
register_version_tools(mock_mcp)
|
||||
|
||||
|
||||
# Get the create_version function
|
||||
create_version = mock_mcp._tools.get('create_version')
|
||||
|
||||
create_version = mock_mcp._tools.get("create_version")
|
||||
|
||||
assert create_version is not None, "create_version tool not registered"
|
||||
|
||||
|
||||
# Mock HTTP response
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"version": {"version_number": 3, "field_name": "docs"},
|
||||
"message": "Version created successfully"
|
||||
"message": "Version created successfully",
|
||||
}
|
||||
|
||||
with patch('src.mcp_server.features.documents.version_tools.httpx.AsyncClient') as mock_client:
|
||||
|
||||
with patch("src.mcp_server.features.documents.version_tools.httpx.AsyncClient") as mock_client:
|
||||
mock_async_client = AsyncMock()
|
||||
mock_async_client.post.return_value = mock_response
|
||||
mock_client.return_value.__aenter__.return_value = mock_async_client
|
||||
|
||||
|
||||
result = await create_version(
|
||||
mock_context,
|
||||
project_id="project-123",
|
||||
field_name="docs",
|
||||
content=[{"id": "doc-1", "title": "Test Doc"}],
|
||||
change_summary="Added test document"
|
||||
change_summary="Added test document",
|
||||
)
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is True
|
||||
assert result_data["version_number"] == 3
|
||||
@@ -73,31 +74,30 @@ async def test_create_version_success(mock_mcp, mock_context):
|
||||
async def test_create_version_invalid_field(mock_mcp, mock_context):
|
||||
"""Test version creation with invalid field name."""
|
||||
register_version_tools(mock_mcp)
|
||||
|
||||
create_version = mock_mcp._tools.get('create_version')
|
||||
|
||||
|
||||
create_version = mock_mcp._tools.get("create_version")
|
||||
|
||||
# Mock 400 response for invalid field
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 400
|
||||
mock_response.text = "invalid field_name"
|
||||
|
||||
with patch('src.mcp_server.features.documents.version_tools.httpx.AsyncClient') as mock_client:
|
||||
|
||||
with patch("src.mcp_server.features.documents.version_tools.httpx.AsyncClient") as mock_client:
|
||||
mock_async_client = AsyncMock()
|
||||
mock_async_client.post.return_value = mock_response
|
||||
mock_client.return_value.__aenter__.return_value = mock_async_client
|
||||
|
||||
|
||||
result = await create_version(
|
||||
mock_context,
|
||||
project_id="project-123",
|
||||
field_name="invalid",
|
||||
content={"test": "data"}
|
||||
mock_context, project_id="project-123", field_name="invalid", content={"test": "data"}
|
||||
)
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is False
|
||||
# Error must be structured format (dict), not string
|
||||
assert "error" in result_data
|
||||
assert isinstance(result_data["error"], dict), "Error should be structured format, not string"
|
||||
assert isinstance(result_data["error"], dict), (
|
||||
"Error should be structured format, not string"
|
||||
)
|
||||
assert result_data["error"]["type"] == "validation_error"
|
||||
|
||||
|
||||
@@ -105,32 +105,30 @@ async def test_create_version_invalid_field(mock_mcp, mock_context):
|
||||
async def test_restore_version_success(mock_mcp, mock_context):
|
||||
"""Test successful version restoration."""
|
||||
register_version_tools(mock_mcp)
|
||||
|
||||
|
||||
# Get the restore_version function
|
||||
restore_version = mock_mcp._tools.get('restore_version')
|
||||
|
||||
restore_version = mock_mcp._tools.get("restore_version")
|
||||
|
||||
assert restore_version is not None, "restore_version tool not registered"
|
||||
|
||||
|
||||
# Mock HTTP response
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"message": "Version 2 restored successfully"
|
||||
}
|
||||
|
||||
with patch('src.mcp_server.features.documents.version_tools.httpx.AsyncClient') as mock_client:
|
||||
mock_response.json.return_value = {"message": "Version 2 restored successfully"}
|
||||
|
||||
with patch("src.mcp_server.features.documents.version_tools.httpx.AsyncClient") as mock_client:
|
||||
mock_async_client = AsyncMock()
|
||||
mock_async_client.post.return_value = mock_response
|
||||
mock_client.return_value.__aenter__.return_value = mock_async_client
|
||||
|
||||
|
||||
result = await restore_version(
|
||||
mock_context,
|
||||
project_id="project-123",
|
||||
field_name="docs",
|
||||
version_number=2,
|
||||
restored_by="test-user"
|
||||
restored_by="test-user",
|
||||
)
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is True
|
||||
assert "Version 2 restored successfully" in result_data["message"]
|
||||
@@ -140,38 +138,34 @@ async def test_restore_version_success(mock_mcp, mock_context):
|
||||
async def test_list_versions_with_filter(mock_mcp, mock_context):
|
||||
"""Test listing versions with field name filter."""
|
||||
register_version_tools(mock_mcp)
|
||||
|
||||
|
||||
# Get the list_versions function
|
||||
list_versions = mock_mcp._tools.get('list_versions')
|
||||
|
||||
list_versions = mock_mcp._tools.get("list_versions")
|
||||
|
||||
assert list_versions is not None, "list_versions tool not registered"
|
||||
|
||||
|
||||
# Mock HTTP response
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"versions": [
|
||||
{"version_number": 1, "field_name": "docs", "change_summary": "Initial"},
|
||||
{"version_number": 2, "field_name": "docs", "change_summary": "Updated"}
|
||||
{"version_number": 2, "field_name": "docs", "change_summary": "Updated"},
|
||||
]
|
||||
}
|
||||
|
||||
with patch('src.mcp_server.features.documents.version_tools.httpx.AsyncClient') as mock_client:
|
||||
|
||||
with patch("src.mcp_server.features.documents.version_tools.httpx.AsyncClient") as mock_client:
|
||||
mock_async_client = AsyncMock()
|
||||
mock_async_client.get.return_value = mock_response
|
||||
mock_client.return_value.__aenter__.return_value = mock_async_client
|
||||
|
||||
result = await list_versions(
|
||||
mock_context,
|
||||
project_id="project-123",
|
||||
field_name="docs"
|
||||
)
|
||||
|
||||
|
||||
result = await list_versions(mock_context, project_id="project-123", field_name="docs")
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is True
|
||||
assert result_data["count"] == 2
|
||||
assert len(result_data["versions"]) == 2
|
||||
|
||||
|
||||
# Verify filter was passed
|
||||
call_args = mock_async_client.get.call_args
|
||||
assert call_args[1]["params"]["field_name"] == "docs"
|
||||
assert call_args[1]["params"]["field_name"] == "docs"
|
||||
|
||||
@@ -1 +1 @@
|
||||
"""Project tools tests."""
|
||||
"""Project tools tests."""
|
||||
|
||||
@@ -16,13 +16,14 @@ def mock_mcp():
|
||||
mock = MagicMock()
|
||||
# Store registered tools
|
||||
mock._tools = {}
|
||||
|
||||
|
||||
def tool_decorator():
|
||||
def decorator(func):
|
||||
mock._tools[func.__name__] = func
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
mock.tool = tool_decorator
|
||||
return mock
|
||||
|
||||
@@ -37,47 +38,43 @@ def mock_context():
|
||||
async def test_create_project_success(mock_mcp, mock_context):
|
||||
"""Test successful project creation with polling."""
|
||||
register_project_tools(mock_mcp)
|
||||
|
||||
|
||||
# Get the create_project function
|
||||
create_project = mock_mcp._tools.get('create_project')
|
||||
|
||||
create_project = mock_mcp._tools.get("create_project")
|
||||
|
||||
assert create_project is not None, "create_project tool not registered"
|
||||
|
||||
|
||||
# Mock initial creation response with progress_id
|
||||
mock_create_response = MagicMock()
|
||||
mock_create_response.status_code = 200
|
||||
mock_create_response.json.return_value = {
|
||||
"progress_id": "progress-123",
|
||||
"message": "Project creation started"
|
||||
"message": "Project creation started",
|
||||
}
|
||||
|
||||
|
||||
# Mock list projects response for polling
|
||||
mock_list_response = MagicMock()
|
||||
mock_list_response.status_code = 200
|
||||
mock_list_response.json.return_value = [
|
||||
{
|
||||
"id": "project-123",
|
||||
"title": "Test Project",
|
||||
"created_at": "2024-01-01"
|
||||
}
|
||||
{"id": "project-123", "title": "Test Project", "created_at": "2024-01-01"}
|
||||
]
|
||||
|
||||
with patch('src.mcp_server.features.projects.project_tools.httpx.AsyncClient') as mock_client:
|
||||
|
||||
with patch("src.mcp_server.features.projects.project_tools.httpx.AsyncClient") as mock_client:
|
||||
mock_async_client = AsyncMock()
|
||||
# First call creates project, subsequent calls list projects
|
||||
mock_async_client.post.return_value = mock_create_response
|
||||
mock_async_client.get.return_value = mock_list_response
|
||||
mock_client.return_value.__aenter__.return_value = mock_async_client
|
||||
|
||||
|
||||
# Mock sleep to speed up test
|
||||
with patch('asyncio.sleep', new_callable=AsyncMock):
|
||||
with patch("asyncio.sleep", new_callable=AsyncMock):
|
||||
result = await create_project(
|
||||
mock_context,
|
||||
title="Test Project",
|
||||
description="A test project",
|
||||
github_repo="https://github.com/test/repo"
|
||||
github_repo="https://github.com/test/repo",
|
||||
)
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is True
|
||||
assert result_data["project"]["id"] == "project-123"
|
||||
@@ -89,27 +86,24 @@ async def test_create_project_success(mock_mcp, mock_context):
|
||||
async def test_create_project_direct_response(mock_mcp, mock_context):
|
||||
"""Test project creation with direct response (no polling)."""
|
||||
register_project_tools(mock_mcp)
|
||||
|
||||
create_project = mock_mcp._tools.get('create_project')
|
||||
|
||||
|
||||
create_project = mock_mcp._tools.get("create_project")
|
||||
|
||||
# Mock direct creation response (no progress_id)
|
||||
mock_create_response = MagicMock()
|
||||
mock_create_response.status_code = 200
|
||||
mock_create_response.json.return_value = {
|
||||
"project": {"id": "project-123", "title": "Test Project"},
|
||||
"message": "Project created immediately"
|
||||
"message": "Project created immediately",
|
||||
}
|
||||
|
||||
with patch('src.mcp_server.features.projects.project_tools.httpx.AsyncClient') as mock_client:
|
||||
|
||||
with patch("src.mcp_server.features.projects.project_tools.httpx.AsyncClient") as mock_client:
|
||||
mock_async_client = AsyncMock()
|
||||
mock_async_client.post.return_value = mock_create_response
|
||||
mock_client.return_value.__aenter__.return_value = mock_async_client
|
||||
|
||||
result = await create_project(
|
||||
mock_context,
|
||||
title="Test Project"
|
||||
)
|
||||
|
||||
|
||||
result = await create_project(mock_context, title="Test Project")
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is True
|
||||
# Direct response returns the project directly
|
||||
@@ -120,27 +114,27 @@ async def test_create_project_direct_response(mock_mcp, mock_context):
|
||||
async def test_list_projects_success(mock_mcp, mock_context):
|
||||
"""Test listing projects."""
|
||||
register_project_tools(mock_mcp)
|
||||
|
||||
|
||||
# Get the list_projects function
|
||||
list_projects = mock_mcp._tools.get('list_projects')
|
||||
|
||||
list_projects = mock_mcp._tools.get("list_projects")
|
||||
|
||||
assert list_projects is not None, "list_projects tool not registered"
|
||||
|
||||
|
||||
# Mock HTTP response - API returns a list directly
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = [
|
||||
{"id": "proj-1", "title": "Project 1", "created_at": "2024-01-01"},
|
||||
{"id": "proj-2", "title": "Project 2", "created_at": "2024-01-02"}
|
||||
{"id": "proj-2", "title": "Project 2", "created_at": "2024-01-02"},
|
||||
]
|
||||
|
||||
with patch('src.mcp_server.features.projects.project_tools.httpx.AsyncClient') as mock_client:
|
||||
|
||||
with patch("src.mcp_server.features.projects.project_tools.httpx.AsyncClient") as mock_client:
|
||||
mock_async_client = AsyncMock()
|
||||
mock_async_client.get.return_value = mock_response
|
||||
mock_client.return_value.__aenter__.return_value = mock_async_client
|
||||
|
||||
|
||||
result = await list_projects(mock_context)
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is True
|
||||
assert len(result_data["projects"]) == 2
|
||||
@@ -151,28 +145,30 @@ async def test_list_projects_success(mock_mcp, mock_context):
|
||||
async def test_get_project_not_found(mock_mcp, mock_context):
|
||||
"""Test getting a non-existent project."""
|
||||
register_project_tools(mock_mcp)
|
||||
|
||||
|
||||
# Get the get_project function
|
||||
get_project = mock_mcp._tools.get('get_project')
|
||||
|
||||
get_project = mock_mcp._tools.get("get_project")
|
||||
|
||||
assert get_project is not None, "get_project tool not registered"
|
||||
|
||||
|
||||
# Mock 404 response
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 404
|
||||
mock_response.text = "Project not found"
|
||||
|
||||
with patch('src.mcp_server.features.projects.project_tools.httpx.AsyncClient') as mock_client:
|
||||
|
||||
with patch("src.mcp_server.features.projects.project_tools.httpx.AsyncClient") as mock_client:
|
||||
mock_async_client = AsyncMock()
|
||||
mock_async_client.get.return_value = mock_response
|
||||
mock_client.return_value.__aenter__.return_value = mock_async_client
|
||||
|
||||
|
||||
result = await get_project(mock_context, project_id="non-existent")
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is False
|
||||
# Error must be structured format (dict), not string
|
||||
assert "error" in result_data
|
||||
assert isinstance(result_data["error"], dict), "Error should be structured format, not string"
|
||||
assert isinstance(result_data["error"], dict), (
|
||||
"Error should be structured format, not string"
|
||||
)
|
||||
assert result_data["error"]["type"] == "not_found"
|
||||
assert "not found" in result_data["error"]["message"].lower()
|
||||
assert "not found" in result_data["error"]["message"].lower()
|
||||
|
||||
@@ -1 +1 @@
|
||||
"""Task tools tests."""
|
||||
"""Task tools tests."""
|
||||
|
||||
@@ -15,13 +15,14 @@ def mock_mcp():
|
||||
mock = MagicMock()
|
||||
# Store registered tools
|
||||
mock._tools = {}
|
||||
|
||||
|
||||
def tool_decorator():
|
||||
def decorator(func):
|
||||
mock._tools[func.__name__] = func
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
mock.tool = tool_decorator
|
||||
return mock
|
||||
|
||||
@@ -36,25 +37,25 @@ def mock_context():
|
||||
async def test_create_task_with_sources(mock_mcp, mock_context):
|
||||
"""Test creating a task with sources and code examples."""
|
||||
register_task_tools(mock_mcp)
|
||||
|
||||
|
||||
# Get the create_task function
|
||||
create_task = mock_mcp._tools.get('create_task')
|
||||
|
||||
create_task = mock_mcp._tools.get("create_task")
|
||||
|
||||
assert create_task is not None, "create_task tool not registered"
|
||||
|
||||
|
||||
# Mock HTTP response
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"task": {"id": "task-123", "title": "Test Task"},
|
||||
"message": "Task created successfully"
|
||||
"message": "Task created successfully",
|
||||
}
|
||||
|
||||
with patch('src.mcp_server.features.tasks.task_tools.httpx.AsyncClient') as mock_client:
|
||||
|
||||
with patch("src.mcp_server.features.tasks.task_tools.httpx.AsyncClient") as mock_client:
|
||||
mock_async_client = AsyncMock()
|
||||
mock_async_client.post.return_value = mock_response
|
||||
mock_client.return_value.__aenter__.return_value = mock_async_client
|
||||
|
||||
|
||||
result = await create_task(
|
||||
mock_context,
|
||||
project_id="project-123",
|
||||
@@ -62,13 +63,13 @@ async def test_create_task_with_sources(mock_mcp, mock_context):
|
||||
description="Add OAuth2 authentication",
|
||||
assignee="AI IDE Agent",
|
||||
sources=[{"url": "https://oauth.net", "type": "doc", "relevance": "OAuth spec"}],
|
||||
code_examples=[{"file": "auth.py", "function": "authenticate", "purpose": "Example"}]
|
||||
code_examples=[{"file": "auth.py", "function": "authenticate", "purpose": "Example"}],
|
||||
)
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is True
|
||||
assert result_data["task_id"] == "task-123"
|
||||
|
||||
|
||||
# Verify sources and examples were sent
|
||||
call_args = mock_async_client.post.call_args
|
||||
sent_data = call_args[1]["json"]
|
||||
@@ -80,37 +81,33 @@ async def test_create_task_with_sources(mock_mcp, mock_context):
|
||||
async def test_list_tasks_with_project_filter(mock_mcp, mock_context):
|
||||
"""Test listing tasks with project-specific endpoint."""
|
||||
register_task_tools(mock_mcp)
|
||||
|
||||
|
||||
# Get the list_tasks function
|
||||
list_tasks = mock_mcp._tools.get('list_tasks')
|
||||
|
||||
list_tasks = mock_mcp._tools.get("list_tasks")
|
||||
|
||||
assert list_tasks is not None, "list_tasks tool not registered"
|
||||
|
||||
|
||||
# Mock HTTP response
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"tasks": [
|
||||
{"id": "task-1", "title": "Task 1", "status": "todo"},
|
||||
{"id": "task-2", "title": "Task 2", "status": "doing"}
|
||||
{"id": "task-2", "title": "Task 2", "status": "doing"},
|
||||
]
|
||||
}
|
||||
|
||||
with patch('src.mcp_server.features.tasks.task_tools.httpx.AsyncClient') as mock_client:
|
||||
|
||||
with patch("src.mcp_server.features.tasks.task_tools.httpx.AsyncClient") as mock_client:
|
||||
mock_async_client = AsyncMock()
|
||||
mock_async_client.get.return_value = mock_response
|
||||
mock_client.return_value.__aenter__.return_value = mock_async_client
|
||||
|
||||
result = await list_tasks(
|
||||
mock_context,
|
||||
filter_by="project",
|
||||
filter_value="project-123"
|
||||
)
|
||||
|
||||
|
||||
result = await list_tasks(mock_context, filter_by="project", filter_value="project-123")
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is True
|
||||
assert len(result_data["tasks"]) == 2
|
||||
|
||||
|
||||
# Verify project-specific endpoint was used
|
||||
call_args = mock_async_client.get.call_args
|
||||
assert "/api/projects/project-123/tasks" in call_args[0][0]
|
||||
@@ -120,31 +117,26 @@ async def test_list_tasks_with_project_filter(mock_mcp, mock_context):
|
||||
async def test_list_tasks_with_status_filter(mock_mcp, mock_context):
|
||||
"""Test listing tasks with status filter uses generic endpoint."""
|
||||
register_task_tools(mock_mcp)
|
||||
|
||||
list_tasks = mock_mcp._tools.get('list_tasks')
|
||||
|
||||
|
||||
list_tasks = mock_mcp._tools.get("list_tasks")
|
||||
|
||||
# Mock HTTP response
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = [
|
||||
{"id": "task-1", "title": "Task 1", "status": "todo"}
|
||||
]
|
||||
|
||||
with patch('src.mcp_server.features.tasks.task_tools.httpx.AsyncClient') as mock_client:
|
||||
mock_response.json.return_value = [{"id": "task-1", "title": "Task 1", "status": "todo"}]
|
||||
|
||||
with patch("src.mcp_server.features.tasks.task_tools.httpx.AsyncClient") as mock_client:
|
||||
mock_async_client = AsyncMock()
|
||||
mock_async_client.get.return_value = mock_response
|
||||
mock_client.return_value.__aenter__.return_value = mock_async_client
|
||||
|
||||
|
||||
result = await list_tasks(
|
||||
mock_context,
|
||||
filter_by="status",
|
||||
filter_value="todo",
|
||||
project_id="project-123"
|
||||
mock_context, filter_by="status", filter_value="todo", project_id="project-123"
|
||||
)
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is True
|
||||
|
||||
|
||||
# Verify generic endpoint with status param was used
|
||||
call_args = mock_async_client.get.call_args
|
||||
assert "/api/tasks" in call_args[0][0]
|
||||
@@ -156,31 +148,29 @@ async def test_list_tasks_with_status_filter(mock_mcp, mock_context):
|
||||
async def test_update_task_status(mock_mcp, mock_context):
|
||||
"""Test updating task status."""
|
||||
register_task_tools(mock_mcp)
|
||||
|
||||
|
||||
# Get the update_task function
|
||||
update_task = mock_mcp._tools.get('update_task')
|
||||
|
||||
update_task = mock_mcp._tools.get("update_task")
|
||||
|
||||
assert update_task is not None, "update_task tool not registered"
|
||||
|
||||
|
||||
# Mock HTTP response
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {
|
||||
"task": {"id": "task-123", "status": "doing"},
|
||||
"message": "Task updated successfully"
|
||||
"message": "Task updated successfully",
|
||||
}
|
||||
|
||||
with patch('src.mcp_server.features.tasks.task_tools.httpx.AsyncClient') as mock_client:
|
||||
|
||||
with patch("src.mcp_server.features.tasks.task_tools.httpx.AsyncClient") as mock_client:
|
||||
mock_async_client = AsyncMock()
|
||||
mock_async_client.put.return_value = mock_response
|
||||
mock_client.return_value.__aenter__.return_value = mock_async_client
|
||||
|
||||
|
||||
result = await update_task(
|
||||
mock_context,
|
||||
task_id="task-123",
|
||||
update_fields={"status": "doing", "assignee": "User"}
|
||||
mock_context, task_id="task-123", update_fields={"status": "doing", "assignee": "User"}
|
||||
)
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is True
|
||||
assert "Task updated successfully" in result_data["message"]
|
||||
@@ -190,28 +180,30 @@ async def test_update_task_status(mock_mcp, mock_context):
|
||||
async def test_delete_task_already_archived(mock_mcp, mock_context):
|
||||
"""Test deleting an already archived task."""
|
||||
register_task_tools(mock_mcp)
|
||||
|
||||
|
||||
# Get the delete_task function
|
||||
delete_task = mock_mcp._tools.get('delete_task')
|
||||
|
||||
delete_task = mock_mcp._tools.get("delete_task")
|
||||
|
||||
assert delete_task is not None, "delete_task tool not registered"
|
||||
|
||||
|
||||
# Mock 400 response for already archived
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 400
|
||||
mock_response.text = "Task already archived"
|
||||
|
||||
with patch('src.mcp_server.features.tasks.task_tools.httpx.AsyncClient') as mock_client:
|
||||
|
||||
with patch("src.mcp_server.features.tasks.task_tools.httpx.AsyncClient") as mock_client:
|
||||
mock_async_client = AsyncMock()
|
||||
mock_async_client.delete.return_value = mock_response
|
||||
mock_client.return_value.__aenter__.return_value = mock_async_client
|
||||
|
||||
|
||||
result = await delete_task(mock_context, task_id="task-123")
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is False
|
||||
# Error must be structured format (dict), not string
|
||||
assert "error" in result_data
|
||||
assert isinstance(result_data["error"], dict), "Error should be structured format, not string"
|
||||
assert isinstance(result_data["error"], dict), (
|
||||
"Error should be structured format, not string"
|
||||
)
|
||||
assert result_data["error"]["type"] == "already_archived"
|
||||
assert "already archived" in result_data["error"]["message"].lower()
|
||||
assert "already archived" in result_data["error"]["message"].lower()
|
||||
|
||||
@@ -15,13 +15,14 @@ def mock_mcp():
|
||||
mock = MagicMock()
|
||||
# Store registered tools
|
||||
mock._tools = {}
|
||||
|
||||
|
||||
def tool_decorator():
|
||||
def decorator(func):
|
||||
mock._tools[func.__name__] = func
|
||||
return func
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
mock.tool = tool_decorator
|
||||
return mock
|
||||
|
||||
@@ -36,12 +37,12 @@ def mock_context():
|
||||
async def test_get_project_features_success(mock_mcp, mock_context):
|
||||
"""Test successful retrieval of project features."""
|
||||
register_feature_tools(mock_mcp)
|
||||
|
||||
|
||||
# Get the get_project_features function
|
||||
get_project_features = mock_mcp._tools.get('get_project_features')
|
||||
|
||||
get_project_features = mock_mcp._tools.get("get_project_features")
|
||||
|
||||
assert get_project_features is not None, "get_project_features tool not registered"
|
||||
|
||||
|
||||
# Mock HTTP response with various feature structures
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
@@ -50,22 +51,22 @@ async def test_get_project_features_success(mock_mcp, mock_context):
|
||||
{"name": "authentication", "status": "completed", "components": ["oauth", "jwt"]},
|
||||
{"name": "api", "status": "in_progress", "endpoints_done": 12, "endpoints_total": 20},
|
||||
{"name": "database", "status": "planned"},
|
||||
{"name": "payments", "provider": "stripe", "version": "2.0", "enabled": True}
|
||||
{"name": "payments", "provider": "stripe", "version": "2.0", "enabled": True},
|
||||
]
|
||||
}
|
||||
|
||||
with patch('src.mcp_server.features.feature_tools.httpx.AsyncClient') as mock_client:
|
||||
|
||||
with patch("src.mcp_server.features.feature_tools.httpx.AsyncClient") as mock_client:
|
||||
mock_async_client = AsyncMock()
|
||||
mock_async_client.get.return_value = mock_response
|
||||
mock_client.return_value.__aenter__.return_value = mock_async_client
|
||||
|
||||
|
||||
result = await get_project_features(mock_context, project_id="project-123")
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is True
|
||||
assert result_data["count"] == 4
|
||||
assert len(result_data["features"]) == 4
|
||||
|
||||
|
||||
# Verify different feature structures are preserved
|
||||
features = result_data["features"]
|
||||
assert features[0]["components"] == ["oauth", "jwt"]
|
||||
@@ -78,21 +79,21 @@ async def test_get_project_features_success(mock_mcp, mock_context):
|
||||
async def test_get_project_features_empty(mock_mcp, mock_context):
|
||||
"""Test getting features for a project with no features defined."""
|
||||
register_feature_tools(mock_mcp)
|
||||
|
||||
get_project_features = mock_mcp._tools.get('get_project_features')
|
||||
|
||||
|
||||
get_project_features = mock_mcp._tools.get("get_project_features")
|
||||
|
||||
# Mock response with empty features
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 200
|
||||
mock_response.json.return_value = {"features": []}
|
||||
|
||||
with patch('src.mcp_server.features.feature_tools.httpx.AsyncClient') as mock_client:
|
||||
|
||||
with patch("src.mcp_server.features.feature_tools.httpx.AsyncClient") as mock_client:
|
||||
mock_async_client = AsyncMock()
|
||||
mock_async_client.get.return_value = mock_response
|
||||
mock_client.return_value.__aenter__.return_value = mock_async_client
|
||||
|
||||
|
||||
result = await get_project_features(mock_context, project_id="project-123")
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is True
|
||||
assert result_data["count"] == 0
|
||||
@@ -103,25 +104,27 @@ async def test_get_project_features_empty(mock_mcp, mock_context):
|
||||
async def test_get_project_features_not_found(mock_mcp, mock_context):
|
||||
"""Test getting features for a non-existent project."""
|
||||
register_feature_tools(mock_mcp)
|
||||
|
||||
get_project_features = mock_mcp._tools.get('get_project_features')
|
||||
|
||||
|
||||
get_project_features = mock_mcp._tools.get("get_project_features")
|
||||
|
||||
# Mock 404 response
|
||||
mock_response = MagicMock()
|
||||
mock_response.status_code = 404
|
||||
mock_response.text = "Project not found"
|
||||
|
||||
with patch('src.mcp_server.features.feature_tools.httpx.AsyncClient') as mock_client:
|
||||
|
||||
with patch("src.mcp_server.features.feature_tools.httpx.AsyncClient") as mock_client:
|
||||
mock_async_client = AsyncMock()
|
||||
mock_async_client.get.return_value = mock_response
|
||||
mock_client.return_value.__aenter__.return_value = mock_async_client
|
||||
|
||||
|
||||
result = await get_project_features(mock_context, project_id="non-existent")
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is False
|
||||
# Error must be structured format (dict), not string
|
||||
assert "error" in result_data
|
||||
assert isinstance(result_data["error"], dict), "Error should be structured format, not string"
|
||||
assert isinstance(result_data["error"], dict), (
|
||||
"Error should be structured format, not string"
|
||||
)
|
||||
assert result_data["error"]["type"] == "not_found"
|
||||
assert "not found" in result_data["error"]["message"].lower()
|
||||
assert "not found" in result_data["error"]["message"].lower()
|
||||
|
||||
@@ -1 +1 @@
|
||||
"""Tests for MCP server utility modules."""
|
||||
"""Tests for MCP server utility modules."""
|
||||
|
||||
@@ -15,7 +15,7 @@ def test_format_error_basic():
|
||||
error_type="validation_error",
|
||||
message="Invalid input",
|
||||
)
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is False
|
||||
assert result_data["error"]["type"] == "validation_error"
|
||||
@@ -33,7 +33,7 @@ def test_format_error_with_all_fields():
|
||||
suggestion="Check network connectivity",
|
||||
http_status=504,
|
||||
)
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is False
|
||||
assert result_data["error"]["type"] == "connection_timeout"
|
||||
@@ -51,9 +51,9 @@ def test_from_http_error_with_json_body():
|
||||
"detail": {"error": "Field is required"},
|
||||
"message": "Validation failed",
|
||||
}
|
||||
|
||||
|
||||
result = MCPErrorFormatter.from_http_error(mock_response, "create item")
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is False
|
||||
# When JSON body has error details, it returns api_error, not http_error
|
||||
@@ -68,9 +68,9 @@ def test_from_http_error_with_text_body():
|
||||
mock_response.status_code = 404
|
||||
mock_response.json.side_effect = json.JSONDecodeError("msg", "doc", 0)
|
||||
mock_response.text = "Resource not found"
|
||||
|
||||
|
||||
result = MCPErrorFormatter.from_http_error(mock_response, "get item")
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is False
|
||||
assert result_data["error"]["type"] == "http_error"
|
||||
@@ -83,11 +83,11 @@ def test_from_exception_timeout():
|
||||
"""Test formatting from timeout exception."""
|
||||
# httpx.TimeoutException is a subclass of httpx.RequestError
|
||||
exception = httpx.TimeoutException("Request timed out after 30s")
|
||||
|
||||
|
||||
result = MCPErrorFormatter.from_exception(
|
||||
exception, "fetch data", {"url": "http://api.example.com"}
|
||||
)
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is False
|
||||
# TimeoutException is categorized as request_error since it's a RequestError subclass
|
||||
@@ -100,9 +100,9 @@ def test_from_exception_timeout():
|
||||
def test_from_exception_connection():
|
||||
"""Test formatting from connection exception."""
|
||||
exception = httpx.ConnectError("Failed to connect to host")
|
||||
|
||||
|
||||
result = MCPErrorFormatter.from_exception(exception, "connect to API")
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is False
|
||||
assert result_data["error"]["type"] == "connection_error"
|
||||
@@ -114,9 +114,9 @@ def test_from_exception_connection():
|
||||
def test_from_exception_request_error():
|
||||
"""Test formatting from generic request error."""
|
||||
exception = httpx.RequestError("Network error")
|
||||
|
||||
|
||||
result = MCPErrorFormatter.from_exception(exception, "make request")
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is False
|
||||
assert result_data["error"]["type"] == "request_error"
|
||||
@@ -127,9 +127,9 @@ def test_from_exception_request_error():
|
||||
def test_from_exception_generic():
|
||||
"""Test formatting from generic exception."""
|
||||
exception = ValueError("Invalid value")
|
||||
|
||||
|
||||
result = MCPErrorFormatter.from_exception(exception, "process data")
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is False
|
||||
# ValueError is specifically categorized as validation_error
|
||||
@@ -141,9 +141,9 @@ def test_from_exception_generic():
|
||||
def test_from_exception_connect_timeout():
|
||||
"""Test formatting from connect timeout exception."""
|
||||
exception = httpx.ConnectTimeout("Connection timed out")
|
||||
|
||||
|
||||
result = MCPErrorFormatter.from_exception(exception, "connect to API")
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is False
|
||||
assert result_data["error"]["type"] == "connection_timeout"
|
||||
@@ -154,11 +154,11 @@ def test_from_exception_connect_timeout():
|
||||
def test_from_exception_read_timeout():
|
||||
"""Test formatting from read timeout exception."""
|
||||
exception = httpx.ReadTimeout("Read timed out")
|
||||
|
||||
|
||||
result = MCPErrorFormatter.from_exception(exception, "read data")
|
||||
|
||||
|
||||
result_data = json.loads(result)
|
||||
assert result_data["success"] is False
|
||||
assert result_data["error"]["type"] == "read_timeout"
|
||||
assert "Read timed out" in result_data["error"]["message"]
|
||||
assert "taking longer than expected" in result_data["error"]["suggestion"].lower()
|
||||
assert "taking longer than expected" in result_data["error"]["suggestion"].lower()
|
||||
|
||||
@@ -18,7 +18,7 @@ def test_get_default_timeout_defaults():
|
||||
"""Test default timeout values when no environment variables are set."""
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
timeout = get_default_timeout()
|
||||
|
||||
|
||||
assert isinstance(timeout, httpx.Timeout)
|
||||
# httpx.Timeout uses 'total' for the overall timeout
|
||||
# We need to check the actual timeout values
|
||||
@@ -33,10 +33,10 @@ def test_get_default_timeout_from_env():
|
||||
"MCP_READ_TIMEOUT": "40.0",
|
||||
"MCP_WRITE_TIMEOUT": "20.0",
|
||||
}
|
||||
|
||||
|
||||
with patch.dict(os.environ, env_vars):
|
||||
timeout = get_default_timeout()
|
||||
|
||||
|
||||
assert isinstance(timeout, httpx.Timeout)
|
||||
# Just verify it's created with the env values
|
||||
|
||||
@@ -45,7 +45,7 @@ def test_get_polling_timeout_defaults():
|
||||
"""Test default polling timeout values."""
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
timeout = get_polling_timeout()
|
||||
|
||||
|
||||
assert isinstance(timeout, httpx.Timeout)
|
||||
# Default polling timeout is 60.0, not 10.0
|
||||
|
||||
@@ -56,10 +56,10 @@ def test_get_polling_timeout_from_env():
|
||||
"MCP_POLLING_TIMEOUT": "15.0",
|
||||
"MCP_CONNECT_TIMEOUT": "3.0", # Uses MCP_CONNECT_TIMEOUT, not MCP_POLLING_CONNECT_TIMEOUT
|
||||
}
|
||||
|
||||
|
||||
with patch.dict(os.environ, env_vars):
|
||||
timeout = get_polling_timeout()
|
||||
|
||||
|
||||
assert isinstance(timeout, httpx.Timeout)
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ def test_get_max_polling_attempts_default():
|
||||
"""Test default max polling attempts."""
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
attempts = get_max_polling_attempts()
|
||||
|
||||
|
||||
assert attempts == 30
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ def test_get_max_polling_attempts_from_env():
|
||||
"""Test max polling attempts from environment variable."""
|
||||
with patch.dict(os.environ, {"MCP_MAX_POLLING_ATTEMPTS": "50"}):
|
||||
attempts = get_max_polling_attempts()
|
||||
|
||||
|
||||
assert attempts == 50
|
||||
|
||||
|
||||
@@ -83,7 +83,7 @@ def test_get_max_polling_attempts_invalid_env():
|
||||
"""Test max polling attempts with invalid environment variable."""
|
||||
with patch.dict(os.environ, {"MCP_MAX_POLLING_ATTEMPTS": "not_a_number"}):
|
||||
attempts = get_max_polling_attempts()
|
||||
|
||||
|
||||
# Should fall back to default after ValueError handling
|
||||
assert attempts == 30
|
||||
|
||||
@@ -92,7 +92,7 @@ def test_get_polling_interval_base():
|
||||
"""Test base polling interval (attempt 0)."""
|
||||
with patch.dict(os.environ, {}, clear=True):
|
||||
interval = get_polling_interval(0)
|
||||
|
||||
|
||||
assert interval == 1.0
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ def test_get_polling_interval_exponential_backoff():
|
||||
assert get_polling_interval(0) == 1.0
|
||||
assert get_polling_interval(1) == 2.0
|
||||
assert get_polling_interval(2) == 4.0
|
||||
|
||||
|
||||
# Test max cap at 5 seconds (default max_interval)
|
||||
assert get_polling_interval(3) == 5.0 # Would be 8.0 but capped at 5.0
|
||||
assert get_polling_interval(4) == 5.0
|
||||
@@ -135,7 +135,7 @@ def test_get_polling_interval_all_custom():
|
||||
"MCP_POLLING_BASE_INTERVAL": "0.5",
|
||||
"MCP_POLLING_MAX_INTERVAL": "3.0",
|
||||
}
|
||||
|
||||
|
||||
with patch.dict(os.environ, env_vars):
|
||||
assert get_polling_interval(0) == 0.5
|
||||
assert get_polling_interval(1) == 1.0
|
||||
@@ -152,10 +152,10 @@ def test_timeout_values_are_floats():
|
||||
"MCP_POLLING_BASE_INTERVAL": "1",
|
||||
"MCP_POLLING_MAX_INTERVAL": "10",
|
||||
}
|
||||
|
||||
|
||||
with patch.dict(os.environ, env_vars):
|
||||
timeout = get_default_timeout()
|
||||
assert isinstance(timeout, httpx.Timeout)
|
||||
|
||||
|
||||
interval = get_polling_interval(0)
|
||||
assert isinstance(interval, float)
|
||||
assert isinstance(interval, float)
|
||||
|
||||
Reference in New Issue
Block a user