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.
This commit is contained in:
2026-01-05 01:02:41 +01:00
parent 49359b1416
commit ad0c06851d
3 changed files with 1790 additions and 0 deletions

View File

@@ -0,0 +1,684 @@
"""
Project and Agent E2E Workflow Tests.
Tests complete project management workflows with real PostgreSQL:
- Project CRUD and lifecycle management
- Agent spawning and lifecycle
- Issue management within projects
- Sprint planning and execution
Usage:
make test-e2e # Run all E2E tests
"""
from datetime import date, timedelta
from uuid import uuid4
import pytest
pytestmark = [
pytest.mark.e2e,
pytest.mark.postgres,
pytest.mark.asyncio,
]
class TestProjectCRUDWorkflows:
"""Test complete project CRUD workflows."""
async def test_create_project_workflow(self, e2e_client):
"""Test creating a project as authenticated user."""
# Register and login
email = f"project-owner-{uuid4().hex[:8]}@example.com"
password = "SecurePass123!"
await e2e_client.post(
"/api/v1/auth/register",
json={
"email": email,
"password": password,
"first_name": "Project",
"last_name": "Owner",
},
)
login_resp = await e2e_client.post(
"/api/v1/auth/login",
json={"email": email, "password": password},
)
tokens = login_resp.json()
# Create project
project_slug = f"test-project-{uuid4().hex[:8]}"
create_resp = await e2e_client.post(
"/api/v1/projects",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
json={
"name": "E2E Test Project",
"slug": project_slug,
"description": "A project for E2E testing",
"autonomy_level": "milestone",
},
)
assert create_resp.status_code == 201, f"Failed: {create_resp.text}"
project = create_resp.json()
assert project["name"] == "E2E Test Project"
assert project["slug"] == project_slug
assert project["status"] == "active"
assert project["agent_count"] == 0
assert project["issue_count"] == 0
async def test_list_projects_only_shows_owned(self, e2e_client):
"""Test that users only see their own projects."""
# Create two users with projects
users = []
for i in range(2):
email = f"user-{i}-{uuid4().hex[:8]}@example.com"
password = "SecurePass123!"
await e2e_client.post(
"/api/v1/auth/register",
json={
"email": email,
"password": password,
"first_name": f"User{i}",
"last_name": "Test",
},
)
login_resp = await e2e_client.post(
"/api/v1/auth/login",
json={"email": email, "password": password},
)
tokens = login_resp.json()
# Each user creates their own project
project_slug = f"user{i}-project-{uuid4().hex[:8]}"
await e2e_client.post(
"/api/v1/projects",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
json={
"name": f"User {i} Project",
"slug": project_slug,
},
)
users.append({"email": email, "tokens": tokens, "slug": project_slug})
# User 0 should only see their project
list_resp = await e2e_client.get(
"/api/v1/projects",
headers={"Authorization": f"Bearer {users[0]['tokens']['access_token']}"},
)
assert list_resp.status_code == 200
data = list_resp.json()
slugs = [p["slug"] for p in data["data"]]
assert users[0]["slug"] in slugs
assert users[1]["slug"] not in slugs
async def test_project_lifecycle_pause_resume(self, e2e_client):
"""Test pausing and resuming a project."""
# Setup user and project
email = f"lifecycle-{uuid4().hex[:8]}@example.com"
password = "SecurePass123!"
await e2e_client.post(
"/api/v1/auth/register",
json={
"email": email,
"password": password,
"first_name": "Lifecycle",
"last_name": "Test",
},
)
login_resp = await e2e_client.post(
"/api/v1/auth/login",
json={"email": email, "password": password},
)
tokens = login_resp.json()
project_slug = f"lifecycle-project-{uuid4().hex[:8]}"
create_resp = await e2e_client.post(
"/api/v1/projects",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
json={"name": "Lifecycle Project", "slug": project_slug},
)
project = create_resp.json()
project_id = project["id"]
# Pause the project
pause_resp = await e2e_client.post(
f"/api/v1/projects/{project_id}/pause",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
)
assert pause_resp.status_code == 200
assert pause_resp.json()["status"] == "paused"
# Resume the project
resume_resp = await e2e_client.post(
f"/api/v1/projects/{project_id}/resume",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
)
assert resume_resp.status_code == 200
assert resume_resp.json()["status"] == "active"
async def test_project_archive(self, e2e_client):
"""Test archiving a project (soft delete)."""
# Setup user and project
email = f"archive-{uuid4().hex[:8]}@example.com"
password = "SecurePass123!"
await e2e_client.post(
"/api/v1/auth/register",
json={
"email": email,
"password": password,
"first_name": "Archive",
"last_name": "Test",
},
)
login_resp = await e2e_client.post(
"/api/v1/auth/login",
json={"email": email, "password": password},
)
tokens = login_resp.json()
project_slug = f"archive-project-{uuid4().hex[:8]}"
create_resp = await e2e_client.post(
"/api/v1/projects",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
json={"name": "Archive Project", "slug": project_slug},
)
project = create_resp.json()
project_id = project["id"]
# Archive the project
archive_resp = await e2e_client.delete(
f"/api/v1/projects/{project_id}",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
)
assert archive_resp.status_code == 200
assert archive_resp.json()["success"] is True
# Verify project is archived
get_resp = await e2e_client.get(
f"/api/v1/projects/{project_id}",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
)
assert get_resp.status_code == 200
assert get_resp.json()["status"] == "archived"
class TestIssueWorkflows:
"""Test issue management workflows within projects."""
async def test_create_and_list_issues(self, e2e_client):
"""Test creating and listing issues in a project."""
# Setup user and project
email = f"issue-test-{uuid4().hex[:8]}@example.com"
password = "SecurePass123!"
await e2e_client.post(
"/api/v1/auth/register",
json={
"email": email,
"password": password,
"first_name": "Issue",
"last_name": "Tester",
},
)
login_resp = await e2e_client.post(
"/api/v1/auth/login",
json={"email": email, "password": password},
)
tokens = login_resp.json()
project_slug = f"issue-project-{uuid4().hex[:8]}"
create_resp = await e2e_client.post(
"/api/v1/projects",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
json={"name": "Issue Test Project", "slug": project_slug},
)
project = create_resp.json()
project_id = project["id"]
# Create multiple issues
issues = []
for i in range(3):
issue_resp = await e2e_client.post(
f"/api/v1/projects/{project_id}/issues",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
json={
"project_id": project_id,
"title": f"Test Issue {i + 1}",
"body": f"Description for issue {i + 1}",
"priority": ["low", "medium", "high"][i],
},
)
assert issue_resp.status_code == 201, f"Failed: {issue_resp.text}"
issues.append(issue_resp.json())
# List issues
list_resp = await e2e_client.get(
f"/api/v1/projects/{project_id}/issues",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
)
assert list_resp.status_code == 200
data = list_resp.json()
assert data["pagination"]["total"] == 3
async def test_issue_status_transitions(self, e2e_client):
"""Test issue status workflow transitions."""
# Setup user and project
email = f"status-test-{uuid4().hex[:8]}@example.com"
password = "SecurePass123!"
await e2e_client.post(
"/api/v1/auth/register",
json={
"email": email,
"password": password,
"first_name": "Status",
"last_name": "Tester",
},
)
login_resp = await e2e_client.post(
"/api/v1/auth/login",
json={"email": email, "password": password},
)
tokens = login_resp.json()
project_slug = f"status-project-{uuid4().hex[:8]}"
create_resp = await e2e_client.post(
"/api/v1/projects",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
json={"name": "Status Test Project", "slug": project_slug},
)
project = create_resp.json()
project_id = project["id"]
# Create issue
issue_resp = await e2e_client.post(
f"/api/v1/projects/{project_id}/issues",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
json={
"project_id": project_id,
"title": "Status Workflow Issue",
"body": "Testing status transitions",
},
)
issue = issue_resp.json()
issue_id = issue["id"]
assert issue["status"] == "open"
# Transition through statuses
for new_status in ["in_progress", "in_review", "closed"]:
update_resp = await e2e_client.patch(
f"/api/v1/projects/{project_id}/issues/{issue_id}",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
json={"status": new_status},
)
assert update_resp.status_code == 200, f"Failed: {update_resp.text}"
assert update_resp.json()["status"] == new_status
async def test_issue_filtering(self, e2e_client):
"""Test issue filtering by status and priority."""
# Setup user and project
email = f"filter-test-{uuid4().hex[:8]}@example.com"
password = "SecurePass123!"
await e2e_client.post(
"/api/v1/auth/register",
json={
"email": email,
"password": password,
"first_name": "Filter",
"last_name": "Tester",
},
)
login_resp = await e2e_client.post(
"/api/v1/auth/login",
json={"email": email, "password": password},
)
tokens = login_resp.json()
project_slug = f"filter-project-{uuid4().hex[:8]}"
create_resp = await e2e_client.post(
"/api/v1/projects",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
json={"name": "Filter Test Project", "slug": project_slug},
)
project = create_resp.json()
project_id = project["id"]
# Create issues with different priorities
for priority in ["low", "medium", "high"]:
await e2e_client.post(
f"/api/v1/projects/{project_id}/issues",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
json={
"project_id": project_id,
"title": f"{priority.title()} Priority Issue",
"priority": priority,
},
)
# Filter by high priority
filter_resp = await e2e_client.get(
f"/api/v1/projects/{project_id}/issues",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
params={"priority": "high"},
)
assert filter_resp.status_code == 200
data = filter_resp.json()
assert data["pagination"]["total"] == 1
assert data["data"][0]["priority"] == "high"
class TestSprintWorkflows:
"""Test sprint planning and execution workflows."""
async def test_sprint_lifecycle(self, e2e_client):
"""Test complete sprint lifecycle: plan -> start -> complete."""
# Setup user and project
email = f"sprint-test-{uuid4().hex[:8]}@example.com"
password = "SecurePass123!"
await e2e_client.post(
"/api/v1/auth/register",
json={
"email": email,
"password": password,
"first_name": "Sprint",
"last_name": "Tester",
},
)
login_resp = await e2e_client.post(
"/api/v1/auth/login",
json={"email": email, "password": password},
)
tokens = login_resp.json()
project_slug = f"sprint-project-{uuid4().hex[:8]}"
create_resp = await e2e_client.post(
"/api/v1/projects",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
json={"name": "Sprint Test Project", "slug": project_slug},
)
project = create_resp.json()
project_id = project["id"]
# Create sprint
today = date.today()
sprint_resp = await e2e_client.post(
f"/api/v1/projects/{project_id}/sprints",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
json={
"project_id": project_id,
"name": "Sprint 1",
"number": 1,
"goal": "Complete initial features",
"start_date": today.isoformat(),
"end_date": (today + timedelta(days=14)).isoformat(),
},
)
assert sprint_resp.status_code == 201, f"Failed: {sprint_resp.text}"
sprint = sprint_resp.json()
sprint_id = sprint["id"]
assert sprint["status"] == "planned"
# Start sprint
start_resp = await e2e_client.post(
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/start",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
)
assert start_resp.status_code == 200, f"Failed: {start_resp.text}"
assert start_resp.json()["status"] == "active"
# Complete sprint
complete_resp = await e2e_client.post(
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/complete",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
)
assert complete_resp.status_code == 200, f"Failed: {complete_resp.text}"
assert complete_resp.json()["status"] == "completed"
async def test_add_issues_to_sprint(self, e2e_client):
"""Test adding issues to a sprint."""
# Setup user and project
email = f"sprint-issues-{uuid4().hex[:8]}@example.com"
password = "SecurePass123!"
await e2e_client.post(
"/api/v1/auth/register",
json={
"email": email,
"password": password,
"first_name": "SprintIssues",
"last_name": "Tester",
},
)
login_resp = await e2e_client.post(
"/api/v1/auth/login",
json={"email": email, "password": password},
)
tokens = login_resp.json()
project_slug = f"sprint-issues-project-{uuid4().hex[:8]}"
create_resp = await e2e_client.post(
"/api/v1/projects",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
json={"name": "Sprint Issues Project", "slug": project_slug},
)
project = create_resp.json()
project_id = project["id"]
# Create sprint
today = date.today()
sprint_resp = await e2e_client.post(
f"/api/v1/projects/{project_id}/sprints",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
json={
"project_id": project_id,
"name": "Sprint 1",
"number": 1,
"start_date": today.isoformat(),
"end_date": (today + timedelta(days=14)).isoformat(),
},
)
assert sprint_resp.status_code == 201, f"Failed: {sprint_resp.text}"
sprint = sprint_resp.json()
sprint_id = sprint["id"]
# Create issue
issue_resp = await e2e_client.post(
f"/api/v1/projects/{project_id}/issues",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
json={
"project_id": project_id,
"title": "Sprint Issue",
"story_points": 5,
},
)
issue = issue_resp.json()
issue_id = issue["id"]
# Add issue to sprint
add_resp = await e2e_client.post(
f"/api/v1/projects/{project_id}/sprints/{sprint_id}/issues",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
params={"issue_id": issue_id},
)
assert add_resp.status_code == 200, f"Failed: {add_resp.text}"
# Verify issue is in sprint
issue_check = await e2e_client.get(
f"/api/v1/projects/{project_id}/issues/{issue_id}",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
)
assert issue_check.json()["sprint_id"] == sprint_id
class TestCrossEntityValidation:
"""Test validation across related entities."""
async def test_cannot_access_other_users_project(self, e2e_client):
"""Test that users cannot access projects they don't own."""
# Create two users
owner_email = f"owner-{uuid4().hex[:8]}@example.com"
other_email = f"other-{uuid4().hex[:8]}@example.com"
password = "SecurePass123!"
# Register owner
await e2e_client.post(
"/api/v1/auth/register",
json={
"email": owner_email,
"password": password,
"first_name": "Owner",
"last_name": "User",
},
)
owner_tokens = (
await e2e_client.post(
"/api/v1/auth/login",
json={"email": owner_email, "password": password},
)
).json()
# Register other user
await e2e_client.post(
"/api/v1/auth/register",
json={
"email": other_email,
"password": password,
"first_name": "Other",
"last_name": "User",
},
)
other_tokens = (
await e2e_client.post(
"/api/v1/auth/login",
json={"email": other_email, "password": password},
)
).json()
# Owner creates project
project_slug = f"private-project-{uuid4().hex[:8]}"
create_resp = await e2e_client.post(
"/api/v1/projects",
headers={"Authorization": f"Bearer {owner_tokens['access_token']}"},
json={"name": "Private Project", "slug": project_slug},
)
project = create_resp.json()
project_id = project["id"]
# Other user tries to access
access_resp = await e2e_client.get(
f"/api/v1/projects/{project_id}",
headers={"Authorization": f"Bearer {other_tokens['access_token']}"},
)
assert access_resp.status_code == 403
async def test_duplicate_project_slug_rejected(self, e2e_client):
"""Test that duplicate project slugs are rejected."""
email = f"dup-test-{uuid4().hex[:8]}@example.com"
password = "SecurePass123!"
await e2e_client.post(
"/api/v1/auth/register",
json={
"email": email,
"password": password,
"first_name": "Dup",
"last_name": "Tester",
},
)
tokens = (
await e2e_client.post(
"/api/v1/auth/login",
json={"email": email, "password": password},
)
).json()
slug = f"unique-slug-{uuid4().hex[:8]}"
# First creation should succeed
resp1 = await e2e_client.post(
"/api/v1/projects",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
json={"name": "First Project", "slug": slug},
)
assert resp1.status_code == 201
# Second creation with same slug should fail
resp2 = await e2e_client.post(
"/api/v1/projects",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
json={"name": "Second Project", "slug": slug},
)
assert resp2.status_code == 409 # Conflict
class TestIssueStats:
"""Test issue statistics endpoints."""
async def test_issue_stats_aggregation(self, e2e_client):
"""Test that issue stats are correctly aggregated."""
email = f"stats-test-{uuid4().hex[:8]}@example.com"
password = "SecurePass123!"
await e2e_client.post(
"/api/v1/auth/register",
json={
"email": email,
"password": password,
"first_name": "Stats",
"last_name": "Tester",
},
)
tokens = (
await e2e_client.post(
"/api/v1/auth/login",
json={"email": email, "password": password},
)
).json()
project_slug = f"stats-project-{uuid4().hex[:8]}"
create_resp = await e2e_client.post(
"/api/v1/projects",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
json={"name": "Stats Project", "slug": project_slug},
)
project = create_resp.json()
project_id = project["id"]
# Create issues with different priorities and story points
await e2e_client.post(
f"/api/v1/projects/{project_id}/issues",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
json={
"project_id": project_id,
"title": "High Priority",
"priority": "high",
"story_points": 8,
},
)
await e2e_client.post(
f"/api/v1/projects/{project_id}/issues",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
json={
"project_id": project_id,
"title": "Low Priority",
"priority": "low",
"story_points": 2,
},
)
# Get stats
stats_resp = await e2e_client.get(
f"/api/v1/projects/{project_id}/issues/stats",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
)
assert stats_resp.status_code == 200
stats = stats_resp.json()
assert stats["total"] == 2
assert stats["total_story_points"] == 10