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,646 @@
"""
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

View File

@@ -0,0 +1,460 @@
"""
MCP and Context Engine E2E Workflow Tests.
Tests complete workflows involving MCP servers and the Context Engine
against real PostgreSQL. These tests verify:
- MCP server listing and tool discovery
- Context engine operations
- Admin-only MCP operations with proper authentication
- Error handling for MCP operations
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 TestMCPServerDiscovery:
"""Test MCP server listing and discovery workflows."""
async def test_list_mcp_servers(self, e2e_client):
"""Test listing MCP servers returns expected configuration."""
response = await e2e_client.get("/api/v1/mcp/servers")
assert response.status_code == 200, f"Failed: {response.text}"
data = response.json()
# Should have servers configured
assert "servers" in data
assert "total" in data
assert isinstance(data["servers"], list)
# Should have at least llm-gateway and knowledge-base
server_names = [s["name"] for s in data["servers"]]
assert "llm-gateway" in server_names
assert "knowledge-base" in server_names
async def test_list_all_mcp_tools(self, e2e_client):
"""Test listing all tools from all MCP servers."""
response = await e2e_client.get("/api/v1/mcp/tools")
assert response.status_code == 200, f"Failed: {response.text}"
data = response.json()
assert "tools" in data
assert "total" in data
assert isinstance(data["tools"], list)
async def test_mcp_health_check(self, e2e_client):
"""Test MCP health check returns server status."""
response = await e2e_client.get("/api/v1/mcp/health")
assert response.status_code == 200, f"Failed: {response.text}"
data = response.json()
assert "servers" in data
assert "healthy_count" in data
assert "unhealthy_count" in data
assert "total" in data
async def test_list_circuit_breakers(self, e2e_client):
"""Test listing circuit breaker status."""
response = await e2e_client.get("/api/v1/mcp/circuit-breakers")
assert response.status_code == 200, f"Failed: {response.text}"
data = response.json()
assert "circuit_breakers" in data
assert isinstance(data["circuit_breakers"], list)
class TestMCPServerTools:
"""Test MCP server tool listing."""
async def test_list_llm_gateway_tools(self, e2e_client):
"""Test listing tools from LLM Gateway server."""
response = await e2e_client.get("/api/v1/mcp/servers/llm-gateway/tools")
# May return 200 with tools or 404 if server not connected
assert response.status_code in [200, 404, 502]
if response.status_code == 200:
data = response.json()
assert "tools" in data
assert "total" in data
async def test_list_knowledge_base_tools(self, e2e_client):
"""Test listing tools from Knowledge Base server."""
response = await e2e_client.get("/api/v1/mcp/servers/knowledge-base/tools")
# May return 200 with tools or 404/502 if server not connected
assert response.status_code in [200, 404, 502]
if response.status_code == 200:
data = response.json()
assert "tools" in data
assert "total" in data
async def test_invalid_server_returns_404(self, e2e_client):
"""Test that invalid server name returns 404."""
response = await e2e_client.get("/api/v1/mcp/servers/nonexistent-server/tools")
assert response.status_code == 404
class TestContextEngineWorkflows:
"""Test Context Engine operations."""
async def test_context_engine_health(self, e2e_client):
"""Test context engine health endpoint."""
response = await e2e_client.get("/api/v1/context/health")
assert response.status_code == 200, f"Failed: {response.text}"
data = response.json()
assert data["status"] == "healthy"
assert "mcp_connected" in data
assert "cache_enabled" in data
async def test_get_token_budget_claude_sonnet(self, e2e_client):
"""Test getting token budget for Claude 3 Sonnet."""
response = await e2e_client.get("/api/v1/context/budget/claude-3-sonnet")
assert response.status_code == 200, f"Failed: {response.text}"
data = response.json()
assert data["model"] == "claude-3-sonnet"
assert "total_tokens" in data
assert "system_tokens" in data
assert "knowledge_tokens" in data
assert "conversation_tokens" in data
assert "tool_tokens" in data
assert "response_reserve" in data
# Verify budget allocation makes sense
assert data["total_tokens"] > 0
total_allocated = (
data["system_tokens"]
+ data["knowledge_tokens"]
+ data["conversation_tokens"]
+ data["tool_tokens"]
+ data["response_reserve"]
)
assert total_allocated <= data["total_tokens"]
async def test_get_token_budget_with_custom_max(self, e2e_client):
"""Test getting token budget with custom max tokens."""
response = await e2e_client.get(
"/api/v1/context/budget/claude-3-sonnet",
params={"max_tokens": 50000},
)
assert response.status_code == 200, f"Failed: {response.text}"
data = response.json()
assert data["model"] == "claude-3-sonnet"
# Custom max should be respected or capped
assert data["total_tokens"] <= 50000
async def test_count_tokens(self, e2e_client):
"""Test token counting endpoint."""
response = await e2e_client.post(
"/api/v1/context/count-tokens",
json={
"content": "Hello, this is a test message for token counting.",
"model": "claude-3-sonnet",
},
)
assert response.status_code == 200, f"Failed: {response.text}"
data = response.json()
assert "token_count" in data
assert data["token_count"] > 0
assert data["model"] == "claude-3-sonnet"
class TestAdminMCPOperations:
"""Test admin-only MCP operations require authentication."""
async def test_tool_call_requires_auth(self, e2e_client):
"""Test that tool execution requires authentication."""
response = await e2e_client.post(
"/api/v1/mcp/call",
json={
"server": "llm-gateway",
"tool": "count_tokens",
"arguments": {"text": "test"},
},
)
# Should require authentication
assert response.status_code in [401, 403]
async def test_circuit_reset_requires_auth(self, e2e_client):
"""Test that circuit breaker reset requires authentication."""
response = await e2e_client.post(
"/api/v1/mcp/circuit-breakers/llm-gateway/reset"
)
assert response.status_code in [401, 403]
async def test_server_reconnect_requires_auth(self, e2e_client):
"""Test that server reconnect requires authentication."""
response = await e2e_client.post("/api/v1/mcp/servers/llm-gateway/reconnect")
assert response.status_code in [401, 403]
async def test_context_stats_requires_auth(self, e2e_client):
"""Test that context stats requires authentication."""
response = await e2e_client.get("/api/v1/context/stats")
assert response.status_code in [401, 403]
async def test_context_assemble_requires_auth(self, e2e_client):
"""Test that context assembly requires authentication."""
response = await e2e_client.post(
"/api/v1/context/assemble",
json={
"project_id": "test-project",
"agent_id": "test-agent",
"query": "test query",
"model": "claude-3-sonnet",
},
)
assert response.status_code in [401, 403]
async def test_cache_invalidate_requires_auth(self, e2e_client):
"""Test that cache invalidation requires authentication."""
response = await e2e_client.post("/api/v1/context/cache/invalidate")
assert response.status_code in [401, 403]
class TestAdminMCPWithAuthentication:
"""Test admin MCP operations with superuser authentication."""
async def test_superuser_can_get_context_stats(self, e2e_client, e2e_superuser):
"""Test that superuser can get context engine stats."""
response = await e2e_client.get(
"/api/v1/context/stats",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
)
assert response.status_code == 200, f"Failed: {response.text}"
data = response.json()
assert "cache" in data
assert "settings" in data
@pytest.mark.skip(
reason="Requires MCP servers (llm-gateway, knowledge-base) to be running"
)
async def test_superuser_can_assemble_context(self, e2e_client, e2e_superuser):
"""Test that superuser can assemble context."""
response = await e2e_client.post(
"/api/v1/context/assemble",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={
"project_id": f"test-project-{uuid4().hex[:8]}",
"agent_id": f"test-agent-{uuid4().hex[:8]}",
"query": "What is the status of the project?",
"model": "claude-3-sonnet",
"system_prompt": "You are a helpful assistant.",
"compress": True,
"use_cache": False,
},
)
assert response.status_code == 200, f"Failed: {response.text}"
data = response.json()
assert "content" in data
assert "total_tokens" in data
assert "context_count" in data
assert "budget_used_percent" in data
assert "metadata" in data
async def test_superuser_can_invalidate_cache(self, e2e_client, e2e_superuser):
"""Test that superuser can invalidate cache."""
response = await e2e_client.post(
"/api/v1/context/cache/invalidate",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
params={"project_id": "test-project"},
)
assert response.status_code == 204
async def test_regular_user_cannot_access_admin_operations(self, e2e_client):
"""Test that regular (non-superuser) cannot access admin operations."""
email = f"regular-{uuid4().hex[:8]}@example.com"
password = "RegularUser123!"
# Register regular user
await e2e_client.post(
"/api/v1/auth/register",
json={
"email": email,
"password": password,
"first_name": "Regular",
"last_name": "User",
},
)
# Login
login_resp = await e2e_client.post(
"/api/v1/auth/login",
json={"email": email, "password": password},
)
tokens = login_resp.json()
# Try to access admin endpoint
response = await e2e_client.get(
"/api/v1/context/stats",
headers={"Authorization": f"Bearer {tokens['access_token']}"},
)
# Should be forbidden for non-superuser
assert response.status_code == 403
class TestMCPInputValidation:
"""Test input validation for MCP endpoints."""
async def test_server_name_max_length(self, e2e_client):
"""Test that server name has max length validation."""
long_name = "a" * 100 # Exceeds 64 char limit
response = await e2e_client.get(f"/api/v1/mcp/servers/{long_name}/tools")
assert response.status_code == 422
async def test_server_name_invalid_characters(self, e2e_client):
"""Test that server name rejects invalid characters."""
invalid_name = "server@name!invalid"
response = await e2e_client.get(f"/api/v1/mcp/servers/{invalid_name}/tools")
assert response.status_code == 422
async def test_token_count_empty_content(self, e2e_client):
"""Test token counting with empty content."""
response = await e2e_client.post(
"/api/v1/context/count-tokens",
json={"content": ""},
)
# Empty content is valid, should return 0 tokens
if response.status_code == 200:
data = response.json()
assert data["token_count"] == 0
else:
# Or it might be rejected as invalid
assert response.status_code == 422
class TestMCPWorkflowIntegration:
"""Test complete MCP workflows end-to-end."""
async def test_discovery_to_budget_workflow(self, e2e_client):
"""Test complete workflow: discover servers -> check budget -> ready for use."""
# 1. Discover available servers
servers_resp = await e2e_client.get("/api/v1/mcp/servers")
assert servers_resp.status_code == 200
servers = servers_resp.json()["servers"]
assert len(servers) > 0
# 2. Check context engine health
health_resp = await e2e_client.get("/api/v1/context/health")
assert health_resp.status_code == 200
health = health_resp.json()
assert health["status"] == "healthy"
# 3. Get token budget for a model
budget_resp = await e2e_client.get("/api/v1/context/budget/claude-3-sonnet")
assert budget_resp.status_code == 200
budget = budget_resp.json()
# 4. Verify system is ready for context assembly
assert budget["total_tokens"] > 0
assert health["mcp_connected"] is True
@pytest.mark.skip(
reason="Requires MCP servers (llm-gateway, knowledge-base) to be running"
)
async def test_full_context_assembly_workflow(self, e2e_client, e2e_superuser):
"""Test complete context assembly workflow with superuser."""
project_id = f"e2e-project-{uuid4().hex[:8]}"
agent_id = f"e2e-agent-{uuid4().hex[:8]}"
# 1. Check budget before assembly
budget_resp = await e2e_client.get("/api/v1/context/budget/claude-3-sonnet")
assert budget_resp.status_code == 200
_ = budget_resp.json() # Verify valid response
# 2. Count tokens in sample content
count_resp = await e2e_client.post(
"/api/v1/context/count-tokens",
json={"content": "This is a test message for context assembly."},
)
assert count_resp.status_code == 200
token_count = count_resp.json()["token_count"]
assert token_count > 0
# 3. Assemble context
assemble_resp = await e2e_client.post(
"/api/v1/context/assemble",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
json={
"project_id": project_id,
"agent_id": agent_id,
"query": "Summarize the current project status",
"model": "claude-3-sonnet",
"system_prompt": "You are a project management assistant.",
"task_description": "Generate a status report",
"conversation_history": [
{"role": "user", "content": "What's the project status?"},
{
"role": "assistant",
"content": "Let me check the current status.",
},
],
"compress": True,
"use_cache": False,
},
)
assert assemble_resp.status_code == 200
assembled = assemble_resp.json()
# 4. Verify assembly results
assert assembled["total_tokens"] > 0
assert assembled["context_count"] > 0
assert assembled["budget_used_percent"] > 0
assert assembled["budget_used_percent"] <= 100
# 5. Get stats to verify the operation was recorded
stats_resp = await e2e_client.get(
"/api/v1/context/stats",
headers={
"Authorization": f"Bearer {e2e_superuser['tokens']['access_token']}"
},
)
assert stats_resp.status_code == 200

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