Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ watchdog>=6.0.0

# Type Stubs
types-requests>=2.31.0
types-redis>=4.6.0.20241004
types-redis>=4.6.0.20241004
locust>=2.31.0
58 changes: 58 additions & 0 deletions tests/load/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# TENET AI Load Testing

## Overview

This directory contains load testing scripts for TENET AI APIs using Locust.

## Tested Endpoints

- GET /health
- GET /v1/stats
- GET /v1/events
- POST /v1/events/llm

## Installation

```bash
pip install locust
```

## Running Tests

```bash
locust -f tests/load/locustfile.py
```

Then open:

http://localhost:8089
Comment on lines +20 to +28

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Document required host configuration and API key setup.

The running instructions omit critical setup details: users must specify the target host URL via --host and ensure a valid API key is configured. Without these, the load tests will fail.

📝 Suggested documentation addition
 ## Running Tests
 
+### Prerequisites
+
+1. Ensure the TENET AI ingest service is running
+2. Configure a valid API key with `ingest` and `read` permissions (see Authentication section below)
+
+### Execute
+
 ```bash
-locust -f tests/load/locustfile.py
+locust -f tests/load/locustfile.py --host=http://localhost:8000

Then open:

http://localhost:8089
+
+### Authentication
+
+Update the API_KEY constant in locustfile.py with a valid test API key that has:
+- ingest permission for POST /v1/events/llm
+- read permission for GET /v1/stats and GET /v1/events

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @tests/load/README.md around lines 20 - 28, Update the tests/load/README.md
to include required host and auth setup: instruct users to pass --host (example:
--host=http://localhost:8000) when running locust with tests/load/locustfile.py
and add an "Authentication" section telling them to set the API_KEY constant in
locustfile.py to a valid test API key with the needed permissions (ingest for
POST /v1/events/llm and read for GET /v1/stats and GET /v1/events); reference
locustfile.py and the API_KEY constant so it's clear where to configure these
values.


</details>

<!-- fingerprinting:phantom:poseidon:puma -->

<!-- cr-comment:v1:fc7fe9e7eef0cf670bb72c46 -->

<!-- This is an auto-generated comment by CodeRabbit -->


## Test Scenarios

### Sustained Load Test

- Target: 100 requests/second
- Duration: 10 minutes

### Spike Load Test

- Simulate sudden bursts of traffic
- Increase users rapidly

### Concurrent Connection Test

- Run multiple concurrent users
- Observe latency and error rates

## Metrics to Measure

- P50 latency
- P95 latency
- P99 latency
- Throughput
- Error rate

## Acceptance Criteria

- P95 latency < 500 ms under sustained load
- Error rate < 0.1% under normal load
56 changes: 56 additions & 0 deletions tests/load/locustfile.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
from locust import HttpUser, task, between

API_KEY = "test-api-key"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Replace hardcoded API key with environment variable.

The hardcoded test-api-key constant poses security and configuration risks: it may be committed to version control and cannot be changed per environment without modifying code. From the relevant code snippets (services/ingest/app.py), the API requires valid authenticated keys with specific permissions (ingest for POST /v1/events/llm, read for GET endpoints).

🔐 Proposed fix using environment variable
+import os
 from locust import HttpUser, task, between
 
-API_KEY = "test-api-key"
+API_KEY = os.getenv("TENET_LOAD_TEST_API_KEY", "test-api-key")

Then document in README.md:

export TENET_LOAD_TEST_API_KEY="your-valid-test-key"
locust -f tests/load/locustfile.py --host=http://localhost:8000
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
API_KEY = "test-api-key"
import os
from locust import HttpUser, task, between
API_KEY = os.getenv("TENET_LOAD_TEST_API_KEY", "test-api-key")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/load/locustfile.py` at line 3, Replace the hardcoded API_KEY constant
in tests/load/locustfile.py with a value read from an environment variable
(e.g., TENET_LOAD_TEST_API_KEY); update the code that references API_KEY so it
raises a clear error or exits if the env var is missing, and ensure any locust
task functions (where API_KEY is used) continue to reference the same symbol
(API_KEY) so no other changes are required; also add a brief README note or
comment showing how to export TENET_LOAD_TEST_API_KEY before running locust.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Hardcoding test-api-key here makes the load test brittle and likely unauthorized under the project’s expected API-key configuration, so the script may only measure 401/403 responses instead of actual ingest performance.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At tests/load/locustfile.py, line 3:

<comment>Hardcoding `test-api-key` here makes the load test brittle and likely unauthorized under the project’s expected API-key configuration, so the script may only measure 401/403 responses instead of actual ingest performance.</comment>

<file context>
@@ -0,0 +1,56 @@
+from locust import HttpUser, task, between
+
+API_KEY = "test-api-key"
+
+
</file context>



class TENETLoadUser(HttpUser):
wait_time = between(1, 3)

headers = {
"x-api-key": API_KEY,
"Content-Type": "application/json",
}
Comment on lines +9 to +12

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Convert mutable class attribute to property or instance attribute.

Ruff correctly flags that defining headers as a mutable dict at class level means all instances share the same dict object. If any task modifies self.headers, it affects all concurrent users.

🛡️ Recommended fix using on_start
-    headers = {
-        "x-api-key": API_KEY,
-        "Content-Type": "application/json",
-    }
+
+    def on_start(self):
+        """Initialize headers per user instance."""
+        self.headers = {
+            "x-api-key": API_KEY,
+            "Content-Type": "application/json",
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
headers = {
"x-api-key": API_KEY,
"Content-Type": "application/json",
}
def on_start(self):
"""Initialize headers per user instance."""
self.headers = {
"x-api-key": API_KEY,
"Content-Type": "application/json",
}
🧰 Tools
🪛 Ruff (0.15.15)

[warning] 9-12: Mutable default value for class attribute

(RUF012)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/load/locustfile.py` around lines 9 - 12, The class-level mutable dict
headers should be converted to an instance attribute or property to avoid
sharing it across Locust users; update the Locust user class (where headers is
declared) to initialize self.headers in an on_start method (or implement a
`@property` that returns a fresh dict) using API_KEY and "Content-Type":
"application/json", and update any references from headers to self.headers so
each user gets its own dict instance and concurrent modifications won't affect
other users.

Source: Linters/SAST tools


@task(3)
def health_check(self):
self.client.get(
"/health",
headers=self.headers,
name="GET /health"
)

@task(2)
def get_stats(self):
self.client.get(
"/v1/stats",
headers=self.headers,
name="GET /v1/stats"
)

@task(2)
def list_events(self):
self.client.get(
"/v1/events",
headers=self.headers,
name="GET /v1/events"
)
Comment on lines +22 to +36

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add response validation and error handling to authenticated endpoints.

The get_stats and list_events tasks don't validate response status codes or catch exceptions. For load testing, tracking success/failure rates is essential to meet the acceptance criteria (error rate < 0.1%).

✅ Proposed enhancement with response validation
     `@task`(2)
     def get_stats(self):
-        self.client.get(
+        with self.client.get(
             "/v1/stats",
             headers=self.headers,
-            name="GET /v1/stats"
-        )
+            name="GET /v1/stats",
+            catch_response=True
+        ) as response:
+            if response.status_code != 200:
+                response.failure(f"Got status code {response.status_code}")
+            elif "total_events" not in response.json():
+                response.failure("Missing expected field: total_events")
+            else:
+                response.success()
 
     `@task`(2)
     def list_events(self):
-        self.client.get(
+        with self.client.get(
             "/v1/events",
             headers=self.headers,
-            name="GET /v1/events"
-        )
+            name="GET /v1/events",
+            catch_response=True
+        ) as response:
+            if response.status_code != 200:
+                response.failure(f"Got status code {response.status_code}")
+            elif "events" not in response.json():
+                response.failure("Missing expected field: events")
+            else:
+                response.success()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/load/locustfile.py` around lines 22 - 36, Wrap the GET calls in
get_stats and list_events with client.get(..., catch_response=True) and validate
the response inside that context: check resp.status_code (expect 200) and call
resp.success() on valid responses or resp.failure(...) with an informative
message on non-200 or unexpected body; also catch exceptions around the request
and call resp.failure(...) (or mark failure) so Locust records errors for those
authenticated endpoints (functions get_stats and list_events, and the
self.client.get calls using self.headers).


@task(1)
def submit_llm_event(self):
payload = {
"source_type": "chat",
"source_id": "load-test-user",
"model": "gpt-4",
"prompt": "This is a load testing request",
"system_prompt": "You are a helpful assistant",
"metadata": {
"environment": "load-test"
}
}

self.client.post(
"/v1/events/llm",
json=payload,
headers=self.headers,
name="POST /v1/events/llm"
)
Loading