| """ |
| Agent API load test scenarios. |
| |
| This module defines load test tasks for agent CRUD operations. |
| The AgentCRUDTasks mixin can be imported and mixed into Locust user classes. |
| |
| Weight Distribution: |
| - Read operations (list, get) have higher weights (5:3) - realistic pattern |
| - Write operations (create, update, delete) have lower weights (1:1:1) - less frequent |
| - This reflects typical API usage where users read more than they write |
| |
| Reference: Phase 209 Plan 02 - Extended Agent CRUD Operations |
| """ |
|
|
| import random |
| import logging |
| from locust import task |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| class AgentCRUDTasks: |
| """ |
| Mixin class for agent CRUD load test tasks. |
| |
| This mixin provides task methods for testing agent create, read, update, |
| and delete operations. It can be mixed into any HttpUser class. |
| |
| Tasks: |
| - list_agents: High-frequency read operation (weight 5) |
| - get_agent: Medium-frequency read operation (weight 3) |
| - create_agent: Low-frequency write operation (weight 1) |
| - update_agent: Low-frequency write operation (weight 1) |
| - delete_agent: Low-frequency write operation (weight 1) |
| |
| Requires the HttpUser class to have: |
| - self.client: Locust HTTP client |
| - self.token: Authentication token (optional, will skip if None) |
| """ |
|
|
| @task(5) |
| def list_agents(self): |
| """ |
| List agents with pagination (high frequency). |
| |
| Simulates loading the agents list page, which is one of the |
| most common operations in the UI. Uses pagination parameters |
| to simulate realistic browsing behavior. |
| |
| Weight: 5 (most common - every page load) |
| Endpoint: GET /api/v1/agents |
| Target: <50ms (from Phase 208 benchmarks) |
| |
| Query Parameters: |
| - skip: Pagination offset (0-100) |
| - limit: Results per page (10-50) |
| |
| Success Criteria: |
| - 200: Valid response with agents list |
| - 401: Acceptable (auth expired during test) |
| - 500: Failure (server error) |
| """ |
| if not hasattr(self, 'token') or not self.token: |
| return |
|
|
| headers = self._get_auth_headers() |
|
|
| |
| params = { |
| "skip": random.randint(0, 100), |
| "limit": random.choice([10, 20, 50]) |
| } |
|
|
| with self.client.get( |
| "/api/v1/agents", |
| headers=headers, |
| params=params, |
| catch_response=True |
| ) as response: |
| if response.status_code == 200: |
| data = response.json() |
| |
| if "agents" in data or isinstance(data, list): |
| response.success() |
| else: |
| response.failure("Invalid response structure - missing 'agents' key") |
| elif response.status_code == 401: |
| |
| response.success() |
| elif response.status_code == 404: |
| |
| response.success() |
| elif response.status_code >= 500: |
| response.failure(f"Server error: {response.status_code}") |
| else: |
| response.failure(f"Unexpected status: {response.status_code}") |
|
|
| @task(3) |
| def get_agent(self): |
| """ |
| Get specific agent by ID (medium frequency). |
| |
| Simulates viewing agent details. Uses random agent IDs |
| to test caching and database query performance. |
| |
| Weight: 3 (medium frequency - detail views) |
| Endpoint: GET /api/v1/agents/{id} |
| Target: <50ms (from Phase 208 benchmarks) |
| |
| Success Criteria: |
| - 200: Agent found and returned |
| - 404: Acceptable (agent doesn't exist) |
| - 401: Acceptable (auth expired) |
| - 500: Failure (server error) |
| """ |
| if not hasattr(self, 'token') or not self.token: |
| return |
|
|
| agent_id = f"agent_{random.randint(1, 100)}" |
| headers = self._get_auth_headers() |
|
|
| with self.client.get( |
| f"/api/v1/agents/{agent_id}", |
| headers=headers, |
| catch_response=True |
| ) as response: |
| if response.status_code == 200: |
| data = response.json() |
| |
| if "id" in data or "agent_id" in data: |
| response.success() |
| else: |
| response.failure("Invalid response structure - missing agent ID") |
| elif response.status_code in [401, 404]: |
| |
| response.success() |
| elif response.status_code >= 500: |
| response.failure(f"Server error: {response.status_code}") |
| else: |
| response.failure(f"Unexpected status: {response.status_code}") |
|
|
| @task(1) |
| def create_agent(self): |
| """ |
| Create new agent (low frequency). |
| |
| Simulates user creating a new agent. Generates realistic |
| agent data with random values to test unique constraints |
| and database insert performance. |
| |
| Weight: 1 (low frequency - user actions) |
| Endpoint: POST /api/v1/agents |
| Target: <100ms (from Phase 208 benchmarks) |
| |
| Success Criteria: |
| - 201: Agent created successfully |
| - 400: Acceptable (validation error) |
| - 401: Acceptable (auth expired) |
| - 500: Failure (server error) |
| |
| Note: Uses AUTONOMOUS maturity level to avoid governance |
| overhead in load tests (we test governance separately). |
| """ |
| if not hasattr(self, 'token') or not self.token: |
| return |
|
|
| headers = self._get_auth_headers() |
|
|
| |
| payload = { |
| "name": f"Load Test Agent {random.randint(1, 10000)}", |
| "category": random.choice(["automation", "analytics", "integration", "monitoring"]), |
| "module_path": "test.module", |
| "class_name": "TestAgent", |
| "maturity": "AUTONOMOUS", |
| "confidence_score": round(random.uniform(0.7, 0.95), 2) |
| } |
|
|
| with self.client.post( |
| "/api/v1/agents", |
| json=payload, |
| headers=headers, |
| catch_response=True |
| ) as response: |
| if response.status_code in [200, 201]: |
| response.success() |
| elif response.status_code in [400, 401, 422]: |
| |
| response.success() |
| elif response.status_code >= 500: |
| response.failure(f"Server error: {response.status_code}") |
| else: |
| response.failure(f"Unexpected status: {response.status_code}") |
|
|
| @task(1) |
| def update_agent(self): |
| """ |
| Update existing agent (low frequency). |
| |
| Simulates updating agent status or configuration. Tests |
| database update performance and optimistic locking. |
| |
| Weight: 1 (low frequency - user actions) |
| Endpoint: PUT /api/v1/agents/{id} |
| Target: <100ms (from Phase 208 benchmarks) |
| |
| Success Criteria: |
| - 200: Agent updated successfully |
| - 404: Acceptable (agent doesn't exist) |
| - 401: Acceptable (auth expired) |
| - 500: Failure (server error) |
| """ |
| if not hasattr(self, 'token') or not self.token: |
| return |
|
|
| agent_id = f"agent_{random.randint(1, 100)}" |
| headers = self._get_auth_headers() |
|
|
| |
| payload = { |
| "status": random.choice(["active", "inactive", "paused"]), |
| "confidence_score": round(random.uniform(0.7, 0.95), 2) |
| } |
|
|
| with self.client.put( |
| f"/api/v1/agents/{agent_id}", |
| json=payload, |
| headers=headers, |
| catch_response=True |
| ) as response: |
| if response.status_code == 200: |
| response.success() |
| elif response.status_code in [401, 404]: |
| |
| response.success() |
| elif response.status_code >= 500: |
| response.failure(f"Server error: {response.status_code}") |
| else: |
| response.failure(f"Unexpected status: {response.status_code}") |
|
|
| @task(1) |
| def delete_agent(self): |
| """ |
| Delete test agent (low frequency). |
| |
| Simulates deleting agents. Only deletes agents with |
| "load_test" prefix to avoid deleting real data. |
| |
| Weight: 1 (low frequency - user actions) |
| Endpoint: DELETE /api/v1/agents/{id} |
| Target: <100ms (from Phase 208 benchmarks) |
| |
| Success Criteria: |
| - 200/204: Agent deleted successfully |
| - 404: Acceptable (agent doesn't exist) |
| - 401: Acceptable (auth expired) |
| - 500: Failure (server error) |
| |
| Note: Uses "load_test_" prefix to ensure we only delete |
| test agents created during load testing. |
| """ |
| if not hasattr(self, 'token') or not self.token: |
| return |
|
|
| |
| agent_id = f"load_test_agent_{random.randint(1, 1000)}" |
| headers = self._get_auth_headers() |
|
|
| with self.client.delete( |
| f"/api/v1/agents/{agent_id}", |
| headers=headers, |
| catch_response=True |
| ) as response: |
| if response.status_code in [200, 204]: |
| response.success() |
| elif response.status_code in [401, 404]: |
| |
| response.success() |
| elif response.status_code >= 500: |
| response.failure(f"Server error: {response.status_code}") |
| else: |
| response.failure(f"Unexpected status: {response.status_code}") |
|
|
| def _get_auth_headers(self): |
| """ |
| Build authentication headers. |
| |
| Returns Authorization header if token exists, |
| otherwise returns empty dict. |
| |
| Returns: |
| dict: Headers dict with Authorization if token available |
| """ |
| if hasattr(self, 'token') and self.token: |
| return {"Authorization": f"Bearer {self.token}"} |
| return {} |
|
|