Skip to content

Commit a7ad400

Browse files
committed
test: Add concurrency isolation tests verifying request sandboxing
1 parent 69b793b commit a7ad400

1 file changed

Lines changed: 76 additions & 0 deletions

File tree

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import pytest
2+
import asyncio
3+
import re
4+
from src.api.routes.generation import content_generator
5+
6+
@pytest.mark.anyio
7+
async def test_concurrent_followup_generations(async_client, mock_litellm_completion):
8+
# Dynamic mock side_effect to generate output corresponding to the client name
9+
async def mock_acompletion(messages, **kwargs):
10+
# Extract client name from user message content
11+
user_msg = messages[-1]["content"]
12+
match = re.search(r"- Client: ([^\n]+)", user_msg)
13+
client_name = match.group(1).strip() if match else "Unknown"
14+
15+
content = f"Subject: Overdue Notice: {client_name}\nBody:\nHello {client_name}, this is your custom invoice reminder."
16+
return mock_litellm_completion.create_response(content=content)
17+
18+
mock_litellm_completion.side_effect = mock_acompletion
19+
20+
headers = {"X-Service-Key": "test-service-key"}
21+
22+
async def fire_request(i):
23+
payload = {
24+
"invoice_id": f"inv_{i}",
25+
"invoice_no": f"INV-{i}",
26+
"client_name": f"Client_{i}",
27+
"contact_email": f"billing{i}@client.com",
28+
"invoice_amount": "100.00",
29+
"currency": "USD",
30+
"due_date": "2026-07-20",
31+
"days_overdue": 5,
32+
"urgency_tier": "stage_1_warm",
33+
"channel": "email"
34+
}
35+
response = await async_client.post("/followup", json=payload, headers=headers)
36+
assert response.status_code == 200
37+
data = response.json()
38+
# Assert that the output returned corresponds to the correct client name, ensuring isolation
39+
assert data["invoice_id"] == f"inv_{i}"
40+
assert data["content"]["subject"] == f"Overdue Notice: Client_{i}"
41+
assert f"Hello Client_{i}" in data["content"]["plain_body"]
42+
43+
# Fire 10 requests concurrently
44+
tasks = [fire_request(i) for i in range(10)]
45+
await asyncio.gather(*tasks)
46+
47+
@pytest.mark.anyio
48+
async def test_concurrent_risk_scoring(async_client):
49+
headers = {"X-Service-Key": "test-service-key"}
50+
51+
async def fire_risk_request(i):
52+
# We vary the amount so the score calculated differs
53+
# rule-based: overdue (5/60*0.4=0.033) + amount (i*1000/100000*0.2=0.002*i) + followup (0)
54+
# expected score = 0.033 + 0.002 * i
55+
payload = {
56+
"invoice_id": f"inv_{i}",
57+
"features": {
58+
"days_overdue": 5,
59+
"invoice_amount": float(i * 1000),
60+
"followup_count": 0
61+
}
62+
}
63+
# Force rule-based to guarantee deterministic values
64+
from src.api.routes.risk import scorer
65+
scorer._use_ml_model = False
66+
67+
response = await async_client.post("/risk/score", json=payload, headers=headers)
68+
assert response.status_code == 200
69+
data = response.json()
70+
assert data["invoice_id"] == f"inv_{i}"
71+
72+
expected_score = round( (5/60.0)*0.4 + (i*1000.0/100000.0)*0.2, 3)
73+
assert data["risk_score"] == expected_score
74+
75+
tasks = [fire_risk_request(i) for i in range(10)]
76+
await asyncio.gather(*tasks)

0 commit comments

Comments
 (0)