Files
syndarix/backend/tests/e2e/test_agent_workflows.py
Felipe Cardoso ad0c06851d feat(tests): add comprehensive E2E tests for MCP and Agent workflows
- Introduced end-to-end tests for MCP workflows, including server discovery, authentication, context engine operations, error handling, and input validation.
- Added full lifecycle tests for agent workflows, covering type management, instance spawning, status transitions, and admin-only operations.
- Enhanced test coverage for real-world MCP and Agent scenarios across PostgreSQL and async environments.
2026-01-05 01:02:41 +01:00

647 lines
22 KiB
Python

"""
Agent E2E Workflow Tests.
Tests complete workflows for AI agents including:
- Agent type management (admin-only)
- Agent instance spawning and lifecycle
- Agent status transitions (pause/resume/terminate)
- Authorization and access control
Usage:
make test-e2e # Run all E2E tests
"""
from uuid import uuid4
import pytest
pytestmark = [
pytest.mark.e2e,
pytest.mark.postgres,
pytest.mark.asyncio,
]
class TestAgentTypesAdminWorkflows:
"""Test agent type management (admin-only operations)."""
async def test_create_agent_type_requires_superuser(self, e2e_client):
"""Test that creating agent types requires superuser privileges."""
# Register regular user
email = f"regular-{uuid4().hex[:8]}@example.com"
password = "RegularPass123!"
await e2e_client.post(
"/api/v1/auth/register",
json={
"email": email,
"password": password,
"first_name": "Regular",
"last_name": "User",
},
)
login_resp = await e2e_client.post(
"/api/v1/auth/login",
json={"email": email, "password": password},
)
tokens = login_resp.json()
# Try to create agent type
response = await e2e_client.post(
"/api/v1/agent-types",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
json={
"name": "Test Agent",
"slug": f"test-agent-{uuid4().hex[:8]}",
"personality_prompt": "You are a helpful assistant.",
"primary_model": "claude-3-sonnet",
},
)
assert response.status_code == 403
async def test_superuser_can_create_agent_type(self, e2e_client, e2e_superuser):
"""Test that superuser can create and manage agent types."""
slug = f"test-type-{uuid4().hex[:8]}"
# Create agent type
create_resp = await e2e_client.post(
"/api/v1/agent-types",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={
"name": "Product Owner Agent",
"slug": slug,
"description": "A product owner agent for requirements gathering",
"expertise": ["requirements", "user_stories", "prioritization"],
"personality_prompt": "You are a product owner focused on delivering value.",
"primary_model": "claude-3-opus",
"fallback_models": ["claude-3-sonnet"],
"model_params": {"temperature": 0.7, "max_tokens": 4000},
"mcp_servers": ["knowledge-base"],
"is_active": True,
},
)
assert create_resp.status_code == 201, f"Failed: {create_resp.text}"
agent_type = create_resp.json()
assert agent_type["name"] == "Product Owner Agent"
assert agent_type["slug"] == slug
assert agent_type["primary_model"] == "claude-3-opus"
assert agent_type["is_active"] is True
assert "requirements" in agent_type["expertise"]
async def test_list_agent_types_public(self, e2e_client, e2e_superuser):
"""Test that any authenticated user can list agent types."""
# First create an agent type as superuser
slug = f"list-test-{uuid4().hex[:8]}"
await e2e_client.post(
"/api/v1/agent-types",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={
"name": f"List Test Agent {slug}",
"slug": slug,
"personality_prompt": "Test agent.",
"primary_model": "claude-3-sonnet",
},
)
# Register regular user
email = f"lister-{uuid4().hex[:8]}@example.com"
password = "ListerPass123!"
await e2e_client.post(
"/api/v1/auth/register",
json={
"email": email,
"password": password,
"first_name": "List",
"last_name": "User",
},
)
login_resp = await e2e_client.post(
"/api/v1/auth/login",
json={"email": email, "password": password},
)
tokens = login_resp.json()
# List agent types as regular user
list_resp = await e2e_client.get(
"/api/v1/agent-types",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
)
assert list_resp.status_code == 200
data = list_resp.json()
assert "data" in data
assert "pagination" in data
assert data["pagination"]["total"] >= 1
async def test_get_agent_type_by_slug(self, e2e_client, e2e_superuser):
"""Test getting agent type by slug."""
slug = f"slug-test-{uuid4().hex[:8]}"
# Create agent type
await e2e_client.post(
"/api/v1/agent-types",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={
"name": f"Slug Test {slug}",
"slug": slug,
"personality_prompt": "Test agent.",
"primary_model": "claude-3-sonnet",
},
)
# Get by slug (route is /slug/{slug}, not /by-slug/{slug})
get_resp = await e2e_client.get(
f"/api/v1/agent-types/slug/{slug}",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
)
assert get_resp.status_code == 200
data = get_resp.json()
assert data["slug"] == slug
async def test_update_agent_type(self, e2e_client, e2e_superuser):
"""Test updating an agent type."""
slug = f"update-test-{uuid4().hex[:8]}"
# Create agent type
create_resp = await e2e_client.post(
"/api/v1/agent-types",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={
"name": "Original Name",
"slug": slug,
"personality_prompt": "Original prompt.",
"primary_model": "claude-3-sonnet",
},
)
agent_type_id = create_resp.json()["id"]
# Update agent type
update_resp = await e2e_client.patch(
f"/api/v1/agent-types/{agent_type_id}",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={
"name": "Updated Name",
"description": "Added description",
},
)
assert update_resp.status_code == 200
updated = update_resp.json()
assert updated["name"] == "Updated Name"
assert updated["description"] == "Added description"
assert updated["personality_prompt"] == "Original prompt." # Unchanged
class TestAgentInstanceWorkflows:
"""Test agent instance spawning and lifecycle."""
async def test_spawn_agent_workflow(self, e2e_client, e2e_superuser):
"""Test complete workflow: create type -> create project -> spawn agent."""
# 1. Create agent type as superuser
type_slug = f"spawn-test-type-{uuid4().hex[:8]}"
type_resp = await e2e_client.post(
"/api/v1/agent-types",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={
"name": "Spawn Test Agent",
"slug": type_slug,
"personality_prompt": "You are a helpful agent.",
"primary_model": "claude-3-sonnet",
},
)
assert type_resp.status_code == 201
agent_type = type_resp.json()
agent_type_id = agent_type["id"]
# 2. Create a project (superuser can create projects too)
project_slug = f"spawn-test-project-{uuid4().hex[:8]}"
project_resp = await e2e_client.post(
"/api/v1/projects",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={"name": "Spawn Test Project", "slug": project_slug},
)
assert project_resp.status_code == 201
project = project_resp.json()
project_id = project["id"]
# 3. Spawn agent instance
spawn_resp = await e2e_client.post(
f"/api/v1/projects/{project_id}/agents",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={
"agent_type_id": agent_type_id,
"project_id": project_id,
"name": "My PO Agent",
},
)
assert spawn_resp.status_code == 201, f"Failed: {spawn_resp.text}"
agent = spawn_resp.json()
assert agent["name"] == "My PO Agent"
assert agent["status"] == "idle"
assert agent["project_id"] == project_id
assert agent["agent_type_id"] == agent_type_id
async def test_list_project_agents(self, e2e_client, e2e_superuser):
"""Test listing agents in a project."""
# Setup: Create agent type and project
type_slug = f"list-agents-type-{uuid4().hex[:8]}"
type_resp = await e2e_client.post(
"/api/v1/agent-types",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={
"name": "List Agents Type",
"slug": type_slug,
"personality_prompt": "Test agent.",
"primary_model": "claude-3-sonnet",
},
)
agent_type_id = type_resp.json()["id"]
project_slug = f"list-agents-project-{uuid4().hex[:8]}"
project_resp = await e2e_client.post(
"/api/v1/projects",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={"name": "List Agents Project", "slug": project_slug},
)
project_id = project_resp.json()["id"]
# Spawn multiple agents
for i in range(3):
await e2e_client.post(
f"/api/v1/projects/{project_id}/agents",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={
"agent_type_id": agent_type_id,
"project_id": project_id,
"name": f"Agent {i + 1}",
},
)
# List agents
list_resp = await e2e_client.get(
f"/api/v1/projects/{project_id}/agents",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
)
assert list_resp.status_code == 200
data = list_resp.json()
assert data["pagination"]["total"] == 3
assert len(data["data"]) == 3
class TestAgentLifecycle:
"""Test agent lifecycle operations (pause/resume/terminate)."""
async def test_agent_pause_and_resume(self, e2e_client, e2e_superuser):
"""Test pausing and resuming an agent."""
# Setup: Create agent type, project, and agent
type_slug = f"pause-test-type-{uuid4().hex[:8]}"
type_resp = await e2e_client.post(
"/api/v1/agent-types",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={
"name": "Pause Test Type",
"slug": type_slug,
"personality_prompt": "Test agent.",
"primary_model": "claude-3-sonnet",
},
)
agent_type_id = type_resp.json()["id"]
project_slug = f"pause-test-project-{uuid4().hex[:8]}"
project_resp = await e2e_client.post(
"/api/v1/projects",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={"name": "Pause Test Project", "slug": project_slug},
)
project_id = project_resp.json()["id"]
spawn_resp = await e2e_client.post(
f"/api/v1/projects/{project_id}/agents",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={
"agent_type_id": agent_type_id,
"project_id": project_id,
"name": "Pausable Agent",
},
)
agent_id = spawn_resp.json()["id"]
assert spawn_resp.json()["status"] == "idle"
# Pause agent
pause_resp = await e2e_client.post(
f"/api/v1/projects/{project_id}/agents/{agent_id}/pause",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
)
assert pause_resp.status_code == 200, f"Failed: {pause_resp.text}"
assert pause_resp.json()["status"] == "paused"
# Resume agent
resume_resp = await e2e_client.post(
f"/api/v1/projects/{project_id}/agents/{agent_id}/resume",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
)
assert resume_resp.status_code == 200, f"Failed: {resume_resp.text}"
assert resume_resp.json()["status"] == "idle"
async def test_agent_terminate(self, e2e_client, e2e_superuser):
"""Test terminating an agent."""
# Setup
type_slug = f"terminate-type-{uuid4().hex[:8]}"
type_resp = await e2e_client.post(
"/api/v1/agent-types",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={
"name": "Terminate Type",
"slug": type_slug,
"personality_prompt": "Test agent.",
"primary_model": "claude-3-sonnet",
},
)
agent_type_id = type_resp.json()["id"]
project_slug = f"terminate-project-{uuid4().hex[:8]}"
project_resp = await e2e_client.post(
"/api/v1/projects",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={"name": "Terminate Project", "slug": project_slug},
)
project_id = project_resp.json()["id"]
spawn_resp = await e2e_client.post(
f"/api/v1/projects/{project_id}/agents",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={
"agent_type_id": agent_type_id,
"project_id": project_id,
"name": "To Be Terminated",
},
)
agent_id = spawn_resp.json()["id"]
# Terminate agent (returns MessageResponse, not agent status)
terminate_resp = await e2e_client.delete(
f"/api/v1/projects/{project_id}/agents/{agent_id}",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
)
assert terminate_resp.status_code == 200
assert "message" in terminate_resp.json()
# Verify terminated agent cannot be resumed (returns 400 or 422)
resume_resp = await e2e_client.post(
f"/api/v1/projects/{project_id}/agents/{agent_id}/resume",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
)
assert resume_resp.status_code in [400, 422] # Invalid transition
class TestAgentAccessControl:
"""Test agent access control and authorization."""
async def test_user_cannot_access_other_project_agents(
self, e2e_client, e2e_superuser
):
"""Test that users cannot access agents in projects they don't own."""
# Superuser creates agent type
type_slug = f"access-type-{uuid4().hex[:8]}"
type_resp = await e2e_client.post(
"/api/v1/agent-types",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={
"name": "Access Type",
"slug": type_slug,
"personality_prompt": "Test agent.",
"primary_model": "claude-3-sonnet",
},
)
agent_type_id = type_resp.json()["id"]
# Superuser creates project and spawns agent
project_slug = f"protected-project-{uuid4().hex[:8]}"
project_resp = await e2e_client.post(
"/api/v1/projects",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={"name": "Protected Project", "slug": project_slug},
)
project_id = project_resp.json()["id"]
spawn_resp = await e2e_client.post(
f"/api/v1/projects/{project_id}/agents",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={
"agent_type_id": agent_type_id,
"project_id": project_id,
"name": "Protected Agent",
},
)
agent_id = spawn_resp.json()["id"]
# Create a different user
email = f"other-user-{uuid4().hex[:8]}@example.com"
password = "OtherPass123!"
await e2e_client.post(
"/api/v1/auth/register",
json={
"email": email,
"password": password,
"first_name": "Other",
"last_name": "User",
},
)
login_resp = await e2e_client.post(
"/api/v1/auth/login",
json={"email": email, "password": password},
)
other_tokens = login_resp.json()
# Other user tries to access the agent
get_resp = await e2e_client.get(
f"/api/v1/projects/{project_id}/agents/{agent_id}",
headers={"Authorization": f"Bearer {other_tokens['access_token']}"},
)
# Should be forbidden or not found
assert get_resp.status_code in [403, 404]
async def test_cannot_spawn_with_inactive_agent_type(
self, e2e_client, e2e_superuser
):
"""Test that agents cannot be spawned from inactive agent types."""
# Create agent type
type_slug = f"inactive-type-{uuid4().hex[:8]}"
type_resp = await e2e_client.post(
"/api/v1/agent-types",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={
"name": "Inactive Type",
"slug": type_slug,
"personality_prompt": "Test agent.",
"primary_model": "claude-3-sonnet",
"is_active": True,
},
)
agent_type_id = type_resp.json()["id"]
# Deactivate the agent type
await e2e_client.patch(
f"/api/v1/agent-types/{agent_type_id}",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={"is_active": False},
)
# Create project
project_slug = f"inactive-spawn-project-{uuid4().hex[:8]}"
project_resp = await e2e_client.post(
"/api/v1/projects",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={"name": "Inactive Spawn Project", "slug": project_slug},
)
project_id = project_resp.json()["id"]
# Try to spawn agent with inactive type
spawn_resp = await e2e_client.post(
f"/api/v1/projects/{project_id}/agents",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={
"agent_type_id": agent_type_id,
"project_id": project_id,
"name": "Should Fail",
},
)
# 422 is correct for validation errors per REST conventions
assert spawn_resp.status_code == 422
class TestAgentMetrics:
"""Test agent metrics endpoint."""
async def test_get_agent_metrics(self, e2e_client, e2e_superuser):
"""Test retrieving agent metrics."""
# Setup
type_slug = f"metrics-type-{uuid4().hex[:8]}"
type_resp = await e2e_client.post(
"/api/v1/agent-types",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={
"name": "Metrics Type",
"slug": type_slug,
"personality_prompt": "Test agent.",
"primary_model": "claude-3-sonnet",
},
)
agent_type_id = type_resp.json()["id"]
project_slug = f"metrics-project-{uuid4().hex[:8]}"
project_resp = await e2e_client.post(
"/api/v1/projects",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={"name": "Metrics Project", "slug": project_slug},
)
project_id = project_resp.json()["id"]
spawn_resp = await e2e_client.post(
f"/api/v1/projects/{project_id}/agents",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={
"agent_type_id": agent_type_id,
"project_id": project_id,
"name": "Metrics Agent",
},
)
agent_id = spawn_resp.json()["id"]
# Get metrics
metrics_resp = await e2e_client.get(
f"/api/v1/projects/{project_id}/agents/{agent_id}/metrics",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
)
assert metrics_resp.status_code == 200
metrics = metrics_resp.json()
# Verify AgentInstanceMetrics structure
assert "total_instances" in metrics
assert "active_instances" in metrics
assert "idle_instances" in metrics
assert "total_tasks_completed" in metrics
assert "total_tokens_used" in metrics
assert "total_cost_incurred" in metrics