# tests/api/routes/syndarix/test_agents.py """Tests for agent instance management endpoints. Tests cover: - Agent instance CRUD operations - Agent lifecycle management (pause, resume) - Agent status filtering - Agent metrics - Authorization and access control """ import uuid import pytest import pytest_asyncio from starlette import status @pytest_asyncio.fixture async def test_project(client, user_token): """Create a test project for agent tests.""" response = await client.post( "/api/v1/projects", json={ "name": "Agent Test Project", "slug": "agent-test-project", "autonomy_level": "milestone", }, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_201_CREATED return response.json() @pytest_asyncio.fixture async def test_agent_type(client, superuser_token): """Create a test agent type for spawning agents.""" import uuid as uuid_mod unique_slug = f"test-developer-agent-{uuid_mod.uuid4().hex[:8]}" response = await client.post( "/api/v1/agent-types", json={ "name": "Test Developer Agent", "slug": unique_slug, "expertise": ["python", "testing"], "primary_model": "claude-3-opus", "personality_prompt": "You are a helpful developer agent for testing.", "description": "A test developer agent", }, headers={"Authorization": f"Bearer {superuser_token}"}, ) assert response.status_code == status.HTTP_201_CREATED, f"Failed: {response.json()}" return response.json() @pytest.mark.asyncio class TestSpawnAgent: """Tests for POST /api/v1/projects/{project_id}/agents endpoint.""" async def test_spawn_agent_success( self, client, user_token, test_project, test_agent_type ): """Test successfully spawning a new agent.""" project_id = test_project["id"] agent_type_id = test_agent_type["id"] response = await client.post( f"/api/v1/projects/{project_id}/agents", json={ "project_id": project_id, "agent_type_id": agent_type_id, "name": "My Developer Agent", }, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_201_CREATED data = response.json() assert data["name"] == "My Developer Agent" assert data["status"] == "idle" assert data["project_id"] == project_id async def test_spawn_agent_with_initial_memory( self, client, user_token, test_project, test_agent_type ): """Test spawning agent with initial short-term memory.""" project_id = test_project["id"] agent_type_id = test_agent_type["id"] response = await client.post( f"/api/v1/projects/{project_id}/agents", json={ "project_id": project_id, "agent_type_id": agent_type_id, "name": "Memory Agent", "short_term_memory": {"context": "test setup"}, }, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_201_CREATED data = response.json() assert data["short_term_memory"]["context"] == "test setup" async def test_spawn_agent_nonexistent_project( self, client, user_token, test_agent_type ): """Test spawning agent in nonexistent project.""" fake_project_id = str(uuid.uuid4()) agent_type_id = test_agent_type["id"] response = await client.post( f"/api/v1/projects/{fake_project_id}/agents", json={ "project_id": fake_project_id, "agent_type_id": agent_type_id, "name": "Orphan Agent", }, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_404_NOT_FOUND async def test_spawn_agent_nonexistent_type( self, client, user_token, test_project ): """Test spawning agent with nonexistent agent type.""" project_id = test_project["id"] fake_type_id = str(uuid.uuid4()) response = await client.post( f"/api/v1/projects/{project_id}/agents", json={ "project_id": project_id, "agent_type_id": fake_type_id, "name": "Invalid Type Agent", }, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_404_NOT_FOUND async def test_spawn_agent_mismatched_project_id( self, client, user_token, test_project, test_agent_type ): """Test spawning agent with mismatched project_id in body.""" project_id = test_project["id"] agent_type_id = test_agent_type["id"] different_project_id = str(uuid.uuid4()) response = await client.post( f"/api/v1/projects/{project_id}/agents", json={ "project_id": different_project_id, "agent_type_id": agent_type_id, "name": "Mismatched Agent", }, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY @pytest.mark.asyncio class TestListAgents: """Tests for GET /api/v1/projects/{project_id}/agents endpoint.""" async def test_list_agents_empty(self, client, user_token, test_project): """Test listing agents when none exist.""" project_id = test_project["id"] response = await client.get( f"/api/v1/projects/{project_id}/agents", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert data["data"] == [] assert data["pagination"]["total"] == 0 async def test_list_agents_with_data( self, client, user_token, test_project, test_agent_type ): """Test listing agents with data.""" project_id = test_project["id"] agent_type_id = test_agent_type["id"] # Create agents await client.post( f"/api/v1/projects/{project_id}/agents", json={ "project_id": project_id, "agent_type_id": agent_type_id, "name": "Agent One", }, headers={"Authorization": f"Bearer {user_token}"}, ) await client.post( f"/api/v1/projects/{project_id}/agents", json={ "project_id": project_id, "agent_type_id": agent_type_id, "name": "Agent Two", }, headers={"Authorization": f"Bearer {user_token}"}, ) response = await client.get( f"/api/v1/projects/{project_id}/agents", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert len(data["data"]) == 2 assert data["pagination"]["total"] == 2 async def test_list_agents_filter_by_status( self, client, user_token, test_project, test_agent_type ): """Test filtering agents by status.""" project_id = test_project["id"] agent_type_id = test_agent_type["id"] # Create agent await client.post( f"/api/v1/projects/{project_id}/agents", json={ "project_id": project_id, "agent_type_id": agent_type_id, "name": "Idle Agent", }, headers={"Authorization": f"Bearer {user_token}"}, ) # Filter by idle status response = await client.get( f"/api/v1/projects/{project_id}/agents?status=idle", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert all(agent["status"] == "idle" for agent in data["data"]) @pytest.mark.asyncio class TestGetAgent: """Tests for GET /api/v1/projects/{project_id}/agents/{agent_id} endpoint.""" async def test_get_agent_success( self, client, user_token, test_project, test_agent_type ): """Test getting agent by ID.""" project_id = test_project["id"] agent_type_id = test_agent_type["id"] # Create agent create_response = await client.post( f"/api/v1/projects/{project_id}/agents", json={ "project_id": project_id, "agent_type_id": agent_type_id, "name": "Get Test Agent", }, headers={"Authorization": f"Bearer {user_token}"}, ) agent_id = create_response.json()["id"] # Get agent response = await client.get( f"/api/v1/projects/{project_id}/agents/{agent_id}", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert data["id"] == agent_id assert data["name"] == "Get Test Agent" async def test_get_agent_not_found(self, client, user_token, test_project): """Test getting a nonexistent agent.""" project_id = test_project["id"] fake_agent_id = str(uuid.uuid4()) response = await client.get( f"/api/v1/projects/{project_id}/agents/{fake_agent_id}", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_404_NOT_FOUND @pytest.mark.asyncio class TestUpdateAgent: """Tests for PATCH /api/v1/projects/{project_id}/agents/{agent_id} endpoint.""" async def test_update_agent_current_task( self, client, user_token, test_project, test_agent_type ): """Test updating agent current_task.""" project_id = test_project["id"] agent_type_id = test_agent_type["id"] # Create agent create_response = await client.post( f"/api/v1/projects/{project_id}/agents", json={ "project_id": project_id, "agent_type_id": agent_type_id, "name": "Task Update Agent", }, headers={"Authorization": f"Bearer {user_token}"}, ) agent_id = create_response.json()["id"] # Update current_task response = await client.patch( f"/api/v1/projects/{project_id}/agents/{agent_id}", json={"current_task": "Working on feature #123"}, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert data["current_task"] == "Working on feature #123" async def test_update_agent_memory( self, client, user_token, test_project, test_agent_type ): """Test updating agent short-term memory.""" project_id = test_project["id"] agent_type_id = test_agent_type["id"] # Create agent create_response = await client.post( f"/api/v1/projects/{project_id}/agents", json={ "project_id": project_id, "agent_type_id": agent_type_id, "name": "Memory Update Agent", }, headers={"Authorization": f"Bearer {user_token}"}, ) agent_id = create_response.json()["id"] # Update memory response = await client.patch( f"/api/v1/projects/{project_id}/agents/{agent_id}", json={"short_term_memory": {"last_context": "updated", "step": 2}}, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert data["short_term_memory"]["last_context"] == "updated" assert data["short_term_memory"]["step"] == 2 async def test_update_agent_not_found(self, client, user_token, test_project): """Test updating a nonexistent agent.""" project_id = test_project["id"] fake_agent_id = str(uuid.uuid4()) response = await client.patch( f"/api/v1/projects/{project_id}/agents/{fake_agent_id}", json={"current_task": "Some task"}, headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_404_NOT_FOUND @pytest.mark.asyncio class TestAgentLifecycle: """Tests for agent lifecycle management endpoints.""" async def test_pause_agent( self, client, user_token, test_project, test_agent_type ): """Test pausing an agent.""" project_id = test_project["id"] agent_type_id = test_agent_type["id"] # Create agent create_response = await client.post( f"/api/v1/projects/{project_id}/agents", json={ "project_id": project_id, "agent_type_id": agent_type_id, "name": "Pause Test Agent", }, headers={"Authorization": f"Bearer {user_token}"}, ) agent_id = create_response.json()["id"] # Pause agent response = await client.post( f"/api/v1/projects/{project_id}/agents/{agent_id}/pause", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert data["status"] == "paused" async def test_resume_paused_agent( self, client, user_token, test_project, test_agent_type ): """Test resuming a paused agent.""" project_id = test_project["id"] agent_type_id = test_agent_type["id"] # Create and pause agent create_response = await client.post( f"/api/v1/projects/{project_id}/agents", json={ "project_id": project_id, "agent_type_id": agent_type_id, "name": "Resume Test Agent", }, headers={"Authorization": f"Bearer {user_token}"}, ) agent_id = create_response.json()["id"] # Pause first await client.post( f"/api/v1/projects/{project_id}/agents/{agent_id}/pause", headers={"Authorization": f"Bearer {user_token}"}, ) # Resume agent response = await client.post( f"/api/v1/projects/{project_id}/agents/{agent_id}/resume", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() assert data["status"] == "idle" async def test_pause_nonexistent_agent(self, client, user_token, test_project): """Test pausing a nonexistent agent.""" project_id = test_project["id"] fake_agent_id = str(uuid.uuid4()) response = await client.post( f"/api/v1/projects/{project_id}/agents/{fake_agent_id}/pause", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_404_NOT_FOUND @pytest.mark.asyncio class TestDeleteAgent: """Tests for DELETE /api/v1/projects/{project_id}/agents/{agent_id} endpoint.""" async def test_delete_agent_success( self, client, user_token, test_project, test_agent_type ): """Test deleting an agent.""" project_id = test_project["id"] agent_type_id = test_agent_type["id"] # Create agent create_response = await client.post( f"/api/v1/projects/{project_id}/agents", json={ "project_id": project_id, "agent_type_id": agent_type_id, "name": "Delete Test Agent", }, headers={"Authorization": f"Bearer {user_token}"}, ) agent_id = create_response.json()["id"] # Delete agent response = await client.delete( f"/api/v1/projects/{project_id}/agents/{agent_id}", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK assert response.json()["success"] is True async def test_delete_agent_not_found(self, client, user_token, test_project): """Test deleting a nonexistent agent.""" project_id = test_project["id"] fake_agent_id = str(uuid.uuid4()) response = await client.delete( f"/api/v1/projects/{project_id}/agents/{fake_agent_id}", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_404_NOT_FOUND @pytest.mark.asyncio class TestAgentMetrics: """Tests for agent metrics endpoints.""" async def test_get_agent_metrics( self, client, user_token, test_project, test_agent_type ): """Test getting metrics for a single agent.""" project_id = test_project["id"] agent_type_id = test_agent_type["id"] # Create agent create_response = await client.post( f"/api/v1/projects/{project_id}/agents", json={ "project_id": project_id, "agent_type_id": agent_type_id, "name": "Metrics Test Agent", }, headers={"Authorization": f"Bearer {user_token}"}, ) agent_id = create_response.json()["id"] # Get metrics response = await client.get( f"/api/v1/projects/{project_id}/agents/{agent_id}/metrics", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK data = response.json() # AgentInstanceMetrics schema assert "total_instances" in data assert "total_tasks_completed" in data assert "total_tokens_used" in data assert "total_cost_incurred" in data async def test_get_project_agents_metrics( self, client, user_token, test_project, test_agent_type ): """Test getting metrics for all agents in a project.""" project_id = test_project["id"] agent_type_id = test_agent_type["id"] # Create agents await client.post( f"/api/v1/projects/{project_id}/agents", json={ "project_id": project_id, "agent_type_id": agent_type_id, "name": "Metrics Agent 1", }, headers={"Authorization": f"Bearer {user_token}"}, ) await client.post( f"/api/v1/projects/{project_id}/agents", json={ "project_id": project_id, "agent_type_id": agent_type_id, "name": "Metrics Agent 2", }, headers={"Authorization": f"Bearer {user_token}"}, ) # Get project-wide metrics response = await client.get( f"/api/v1/projects/{project_id}/agents/metrics", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_200_OK @pytest.mark.asyncio class TestAgentAuthorization: """Tests for agent authorization.""" async def test_superuser_can_manage_any_project_agents( self, client, user_token, superuser_token, test_project, test_agent_type ): """Test that superuser can manage agents in any project.""" project_id = test_project["id"] agent_type_id = test_agent_type["id"] # Create agent as superuser in user's project response = await client.post( f"/api/v1/projects/{project_id}/agents", json={ "project_id": project_id, "agent_type_id": agent_type_id, "name": "Superuser Created Agent", }, headers={"Authorization": f"Bearer {superuser_token}"}, ) assert response.status_code == status.HTTP_201_CREATED async def test_user_cannot_access_other_project_agents( self, client, user_token, superuser_token, test_agent_type ): """Test that user cannot access agents in another user's project.""" # Create a project as superuser (not owned by regular user) project_response = await client.post( "/api/v1/projects", json={ "name": "Other User Project", "slug": f"other-user-project-{uuid.uuid4().hex[:8]}", }, headers={"Authorization": f"Bearer {superuser_token}"}, ) other_project_id = project_response.json()["id"] # Regular user tries to list agents - should fail response = await client.get( f"/api/v1/projects/{other_project_id}/agents", headers={"Authorization": f"Bearer {user_token}"}, ) assert response.status_code == status.HTTP_403_FORBIDDEN