kairo-code 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kairo/backend/api/agents.py +337 -16
- kairo/backend/app.py +84 -4
- kairo/backend/config.py +4 -2
- kairo/backend/models/agent.py +216 -2
- kairo/backend/models/api_key.py +4 -1
- kairo/backend/models/task.py +31 -0
- kairo/backend/models/user_provider_key.py +26 -0
- kairo/backend/schemas/agent.py +249 -2
- kairo/backend/schemas/api_key.py +3 -0
- kairo/backend/services/agent/__init__.py +52 -0
- kairo/backend/services/agent/agent_alerts_evaluation_service.py +224 -0
- kairo/backend/services/agent/agent_alerts_service.py +201 -0
- kairo/backend/services/agent/agent_commands_service.py +142 -0
- kairo/backend/services/agent/agent_crud_service.py +150 -0
- kairo/backend/services/agent/agent_events_service.py +103 -0
- kairo/backend/services/agent/agent_heartbeat_service.py +207 -0
- kairo/backend/services/agent/agent_metrics_rollup_service.py +248 -0
- kairo/backend/services/agent/agent_metrics_service.py +259 -0
- kairo/backend/services/agent/agent_service.py +315 -0
- kairo/backend/services/agent/agent_setup_service.py +180 -0
- kairo/backend/services/agent/constants.py +28 -0
- kairo/backend/services/agent_service.py +18 -102
- kairo/backend/services/api_key_service.py +23 -3
- kairo/backend/services/byok_service.py +204 -0
- kairo/backend/services/chat_service.py +398 -63
- kairo/backend/services/deep_search_service.py +159 -0
- kairo/backend/services/email_service.py +418 -19
- kairo/backend/services/few_shot_service.py +223 -0
- kairo/backend/services/post_processor.py +261 -0
- kairo/backend/services/rag_service.py +150 -0
- kairo/backend/services/task_service.py +119 -0
- kairo/backend/tests/__init__.py +1 -0
- kairo/backend/tests/e2e/__init__.py +1 -0
- kairo/backend/tests/e2e/agents/__init__.py +1 -0
- kairo/backend/tests/e2e/agents/conftest.py +389 -0
- kairo/backend/tests/e2e/agents/test_agent_alerts.py +802 -0
- kairo/backend/tests/e2e/agents/test_agent_commands.py +456 -0
- kairo/backend/tests/e2e/agents/test_agent_crud.py +455 -0
- kairo/backend/tests/e2e/agents/test_agent_events.py +415 -0
- kairo/backend/tests/e2e/agents/test_agent_heartbeat.py +520 -0
- kairo/backend/tests/e2e/agents/test_agent_metrics.py +587 -0
- kairo/backend/tests/e2e/agents/test_agent_setup.py +349 -0
- kairo/migrations/versions/010_agent_dashboard.py +246 -0
- {kairo_code-0.1.0.dist-info → kairo_code-0.2.0.dist-info}/METADATA +1 -1
- {kairo_code-0.1.0.dist-info → kairo_code-0.2.0.dist-info}/RECORD +50 -16
- {kairo_code-0.1.0.dist-info → kairo_code-0.2.0.dist-info}/top_level.txt +1 -0
- kairo_migrations/env.py +92 -0
- kairo_migrations/versions/001_add_agent_dashboard_extensions.py +450 -0
- {kairo_code-0.1.0.dist-info → kairo_code-0.2.0.dist-info}/WHEEL +0 -0
- {kairo_code-0.1.0.dist-info → kairo_code-0.2.0.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
"""
|
|
2
|
+
End-to-end tests for Agent CRUD operations.
|
|
3
|
+
|
|
4
|
+
Tests cover:
|
|
5
|
+
- Create agent with all fields
|
|
6
|
+
- List agents (empty, with agents)
|
|
7
|
+
- Get agent by ID
|
|
8
|
+
- Update agent fields
|
|
9
|
+
- Delete agent (soft delete)
|
|
10
|
+
- 404 for non-existent agent
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import pytest
|
|
14
|
+
import uuid
|
|
15
|
+
from httpx import AsyncClient
|
|
16
|
+
|
|
17
|
+
from backend.models.agent import Agent
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestCreateAgent:
|
|
21
|
+
"""Tests for POST /api/agents/"""
|
|
22
|
+
|
|
23
|
+
@pytest.mark.asyncio
|
|
24
|
+
async def test_create_agent_with_all_fields(
|
|
25
|
+
self, client: AsyncClient, auth_headers: dict
|
|
26
|
+
):
|
|
27
|
+
"""Should create an agent with all optional fields specified."""
|
|
28
|
+
payload = {
|
|
29
|
+
"name": "My Production Agent",
|
|
30
|
+
"description": "Handles customer support queries",
|
|
31
|
+
"system_prompt": "You are a helpful customer support assistant.",
|
|
32
|
+
"model_preference": "nyx",
|
|
33
|
+
"agent_type": "sdk",
|
|
34
|
+
"tools": ["web_search", "file_read"],
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
response = await client.post(
|
|
38
|
+
"/api/agents/", json=payload, headers=auth_headers
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
assert response.status_code == 200
|
|
42
|
+
data = response.json()
|
|
43
|
+
assert data["name"] == payload["name"]
|
|
44
|
+
assert data["description"] == payload["description"]
|
|
45
|
+
assert data["model_preference"] == payload["model_preference"]
|
|
46
|
+
assert data["agent_type"] == payload["agent_type"]
|
|
47
|
+
assert data["state"] == "created"
|
|
48
|
+
assert data["status"] == "offline"
|
|
49
|
+
assert "id" in data
|
|
50
|
+
assert "created_at" in data
|
|
51
|
+
assert "updated_at" in data
|
|
52
|
+
|
|
53
|
+
@pytest.mark.asyncio
|
|
54
|
+
async def test_create_agent_minimal_fields(
|
|
55
|
+
self, client: AsyncClient, auth_headers: dict
|
|
56
|
+
):
|
|
57
|
+
"""Should create an agent with only required fields."""
|
|
58
|
+
payload = {"name": "Minimal Agent"}
|
|
59
|
+
|
|
60
|
+
response = await client.post(
|
|
61
|
+
"/api/agents/", json=payload, headers=auth_headers
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
assert response.status_code == 200
|
|
65
|
+
data = response.json()
|
|
66
|
+
assert data["name"] == "Minimal Agent"
|
|
67
|
+
assert data["model_preference"] == "nyx" # default
|
|
68
|
+
assert data["agent_type"] == "sdk" # default
|
|
69
|
+
assert data["description"] is None
|
|
70
|
+
|
|
71
|
+
@pytest.mark.asyncio
|
|
72
|
+
async def test_create_agent_validation_error_empty_name(
|
|
73
|
+
self, client: AsyncClient, auth_headers: dict
|
|
74
|
+
):
|
|
75
|
+
"""Should reject agent creation with empty name."""
|
|
76
|
+
payload = {"name": ""}
|
|
77
|
+
|
|
78
|
+
response = await client.post(
|
|
79
|
+
"/api/agents/", json=payload, headers=auth_headers
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
assert response.status_code == 422
|
|
83
|
+
|
|
84
|
+
@pytest.mark.asyncio
|
|
85
|
+
async def test_create_agent_validation_error_name_too_long(
|
|
86
|
+
self, client: AsyncClient, auth_headers: dict
|
|
87
|
+
):
|
|
88
|
+
"""Should reject agent creation with name exceeding 100 chars."""
|
|
89
|
+
payload = {"name": "x" * 101}
|
|
90
|
+
|
|
91
|
+
response = await client.post(
|
|
92
|
+
"/api/agents/", json=payload, headers=auth_headers
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
assert response.status_code == 422
|
|
96
|
+
|
|
97
|
+
@pytest.mark.asyncio
|
|
98
|
+
async def test_create_agent_invalid_agent_type(
|
|
99
|
+
self, client: AsyncClient, auth_headers: dict
|
|
100
|
+
):
|
|
101
|
+
"""Should reject agent creation with invalid agent_type."""
|
|
102
|
+
payload = {"name": "Test Agent", "agent_type": "invalid"}
|
|
103
|
+
|
|
104
|
+
response = await client.post(
|
|
105
|
+
"/api/agents/", json=payload, headers=auth_headers
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
assert response.status_code == 422
|
|
109
|
+
|
|
110
|
+
@pytest.mark.asyncio
|
|
111
|
+
async def test_create_agent_requires_auth(self, client: AsyncClient):
|
|
112
|
+
"""Should require authentication to create agent."""
|
|
113
|
+
payload = {"name": "Test Agent"}
|
|
114
|
+
|
|
115
|
+
response = await client.post("/api/agents/", json=payload)
|
|
116
|
+
|
|
117
|
+
assert response.status_code == 403 # No auth header
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class TestListAgents:
|
|
121
|
+
"""Tests for GET /api/agents/"""
|
|
122
|
+
|
|
123
|
+
@pytest.mark.asyncio
|
|
124
|
+
async def test_list_agents_empty(
|
|
125
|
+
self, client: AsyncClient, auth_headers: dict
|
|
126
|
+
):
|
|
127
|
+
"""Should return empty list when user has no agents."""
|
|
128
|
+
response = await client.get("/api/agents/", headers=auth_headers)
|
|
129
|
+
|
|
130
|
+
assert response.status_code == 200
|
|
131
|
+
data = response.json()
|
|
132
|
+
assert data == []
|
|
133
|
+
|
|
134
|
+
@pytest.mark.asyncio
|
|
135
|
+
async def test_list_agents_with_agents(
|
|
136
|
+
self, client: AsyncClient, auth_headers: dict, agent_factory
|
|
137
|
+
):
|
|
138
|
+
"""Should return list of user's agents with summary metrics."""
|
|
139
|
+
# Create multiple agents
|
|
140
|
+
await agent_factory.create_agent(name="Agent 1", state="online")
|
|
141
|
+
await agent_factory.create_agent(name="Agent 2", state="offline")
|
|
142
|
+
await agent_factory.create_agent(name="Agent 3", state="idle")
|
|
143
|
+
|
|
144
|
+
response = await client.get("/api/agents/", headers=auth_headers)
|
|
145
|
+
|
|
146
|
+
assert response.status_code == 200
|
|
147
|
+
data = response.json()
|
|
148
|
+
assert len(data) == 3
|
|
149
|
+
|
|
150
|
+
# Verify all expected fields are present
|
|
151
|
+
for agent in data:
|
|
152
|
+
assert "id" in agent
|
|
153
|
+
assert "name" in agent
|
|
154
|
+
assert "state" in agent
|
|
155
|
+
assert "model_preference" in agent
|
|
156
|
+
assert "agent_type" in agent
|
|
157
|
+
assert "created_at" in agent
|
|
158
|
+
# Summary metrics should be present (may be 0)
|
|
159
|
+
assert "requests_24h" in agent
|
|
160
|
+
assert "errors_24h" in agent
|
|
161
|
+
assert "tokens_24h" in agent
|
|
162
|
+
assert "error_rate" in agent
|
|
163
|
+
|
|
164
|
+
@pytest.mark.asyncio
|
|
165
|
+
async def test_list_agents_excludes_deleted(
|
|
166
|
+
self, client: AsyncClient, auth_headers: dict, agent_factory, db_session
|
|
167
|
+
):
|
|
168
|
+
"""Should not return soft-deleted agents."""
|
|
169
|
+
from datetime import datetime, UTC
|
|
170
|
+
|
|
171
|
+
agent = await agent_factory.create_agent(name="To Delete")
|
|
172
|
+
agent.deleted_at = datetime.now(UTC)
|
|
173
|
+
await db_session.commit()
|
|
174
|
+
|
|
175
|
+
response = await client.get("/api/agents/", headers=auth_headers)
|
|
176
|
+
|
|
177
|
+
assert response.status_code == 200
|
|
178
|
+
data = response.json()
|
|
179
|
+
assert len(data) == 0
|
|
180
|
+
|
|
181
|
+
@pytest.mark.asyncio
|
|
182
|
+
async def test_list_agents_isolation(
|
|
183
|
+
self, client: AsyncClient, auth_headers: dict, db_session, agent_factory
|
|
184
|
+
):
|
|
185
|
+
"""Should only return agents belonging to the authenticated user."""
|
|
186
|
+
from argon2 import PasswordHasher
|
|
187
|
+
from backend.models.user import User
|
|
188
|
+
|
|
189
|
+
# Create another user with their own agent
|
|
190
|
+
ph = PasswordHasher()
|
|
191
|
+
other_user = User(
|
|
192
|
+
id=str(uuid.uuid4()),
|
|
193
|
+
email="other@example.com",
|
|
194
|
+
hashed_password=ph.hash("password"),
|
|
195
|
+
email_verified=True,
|
|
196
|
+
)
|
|
197
|
+
db_session.add(other_user)
|
|
198
|
+
await db_session.commit()
|
|
199
|
+
|
|
200
|
+
other_agent = Agent(
|
|
201
|
+
id=str(uuid.uuid4()),
|
|
202
|
+
user_id=other_user.id,
|
|
203
|
+
name="Other User Agent",
|
|
204
|
+
state="online",
|
|
205
|
+
)
|
|
206
|
+
db_session.add(other_agent)
|
|
207
|
+
await db_session.commit()
|
|
208
|
+
|
|
209
|
+
# Create agent for test user
|
|
210
|
+
await agent_factory.create_agent(name="My Agent")
|
|
211
|
+
|
|
212
|
+
response = await client.get("/api/agents/", headers=auth_headers)
|
|
213
|
+
|
|
214
|
+
assert response.status_code == 200
|
|
215
|
+
data = response.json()
|
|
216
|
+
assert len(data) == 1
|
|
217
|
+
assert data[0]["name"] == "My Agent"
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
class TestGetAgent:
|
|
221
|
+
"""Tests for GET /api/agents/{agent_id}"""
|
|
222
|
+
|
|
223
|
+
@pytest.mark.asyncio
|
|
224
|
+
async def test_get_agent_success(
|
|
225
|
+
self, client: AsyncClient, auth_headers: dict, test_agent: Agent
|
|
226
|
+
):
|
|
227
|
+
"""Should return agent details for valid ID."""
|
|
228
|
+
response = await client.get(
|
|
229
|
+
f"/api/agents/{test_agent.id}", headers=auth_headers
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
assert response.status_code == 200
|
|
233
|
+
data = response.json()
|
|
234
|
+
assert data["id"] == test_agent.id
|
|
235
|
+
assert data["name"] == test_agent.name
|
|
236
|
+
assert data["description"] == test_agent.description
|
|
237
|
+
assert data["state"] == test_agent.state
|
|
238
|
+
|
|
239
|
+
@pytest.mark.asyncio
|
|
240
|
+
async def test_get_agent_not_found(
|
|
241
|
+
self, client: AsyncClient, auth_headers: dict
|
|
242
|
+
):
|
|
243
|
+
"""Should return 404 for non-existent agent ID."""
|
|
244
|
+
fake_id = str(uuid.uuid4())
|
|
245
|
+
|
|
246
|
+
response = await client.get(
|
|
247
|
+
f"/api/agents/{fake_id}", headers=auth_headers
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
assert response.status_code == 404
|
|
251
|
+
assert response.json()["detail"] == "Agent not found"
|
|
252
|
+
|
|
253
|
+
@pytest.mark.asyncio
|
|
254
|
+
async def test_get_agent_wrong_user(
|
|
255
|
+
self, client: AsyncClient, db_session, test_user
|
|
256
|
+
):
|
|
257
|
+
"""Should return 404 when accessing another user's agent."""
|
|
258
|
+
from argon2 import PasswordHasher
|
|
259
|
+
from backend.core.security import create_access_token
|
|
260
|
+
from backend.models.user import User
|
|
261
|
+
|
|
262
|
+
# Create another user
|
|
263
|
+
ph = PasswordHasher()
|
|
264
|
+
other_user = User(
|
|
265
|
+
id=str(uuid.uuid4()),
|
|
266
|
+
email="other@example.com",
|
|
267
|
+
hashed_password=ph.hash("password"),
|
|
268
|
+
email_verified=True,
|
|
269
|
+
)
|
|
270
|
+
db_session.add(other_user)
|
|
271
|
+
|
|
272
|
+
# Create agent for other user
|
|
273
|
+
other_agent = Agent(
|
|
274
|
+
id=str(uuid.uuid4()),
|
|
275
|
+
user_id=other_user.id,
|
|
276
|
+
name="Other Agent",
|
|
277
|
+
state="online",
|
|
278
|
+
)
|
|
279
|
+
db_session.add(other_agent)
|
|
280
|
+
await db_session.commit()
|
|
281
|
+
|
|
282
|
+
# Try to access with test_user's token
|
|
283
|
+
token = create_access_token(test_user.id)
|
|
284
|
+
headers = {"Authorization": f"Bearer {token}"}
|
|
285
|
+
|
|
286
|
+
response = await client.get(
|
|
287
|
+
f"/api/agents/{other_agent.id}", headers=headers
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
assert response.status_code == 404
|
|
291
|
+
|
|
292
|
+
@pytest.mark.asyncio
|
|
293
|
+
async def test_get_deleted_agent_returns_404(
|
|
294
|
+
self, client: AsyncClient, auth_headers: dict, test_agent: Agent, db_session
|
|
295
|
+
):
|
|
296
|
+
"""Should return 404 for soft-deleted agent."""
|
|
297
|
+
from datetime import datetime, UTC
|
|
298
|
+
|
|
299
|
+
test_agent.deleted_at = datetime.now(UTC)
|
|
300
|
+
await db_session.commit()
|
|
301
|
+
|
|
302
|
+
response = await client.get(
|
|
303
|
+
f"/api/agents/{test_agent.id}", headers=auth_headers
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
assert response.status_code == 404
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
class TestUpdateAgent:
|
|
310
|
+
"""Tests for PATCH /api/agents/{agent_id}"""
|
|
311
|
+
|
|
312
|
+
@pytest.mark.asyncio
|
|
313
|
+
async def test_update_agent_name(
|
|
314
|
+
self, client: AsyncClient, auth_headers: dict, test_agent: Agent
|
|
315
|
+
):
|
|
316
|
+
"""Should update agent name."""
|
|
317
|
+
payload = {"name": "Updated Agent Name"}
|
|
318
|
+
|
|
319
|
+
response = await client.patch(
|
|
320
|
+
f"/api/agents/{test_agent.id}", json=payload, headers=auth_headers
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
assert response.status_code == 200
|
|
324
|
+
data = response.json()
|
|
325
|
+
assert data["name"] == "Updated Agent Name"
|
|
326
|
+
|
|
327
|
+
@pytest.mark.asyncio
|
|
328
|
+
async def test_update_agent_multiple_fields(
|
|
329
|
+
self, client: AsyncClient, auth_headers: dict, test_agent: Agent
|
|
330
|
+
):
|
|
331
|
+
"""Should update multiple agent fields at once."""
|
|
332
|
+
payload = {
|
|
333
|
+
"name": "New Name",
|
|
334
|
+
"description": "New description",
|
|
335
|
+
"model_preference": "nyx-lite",
|
|
336
|
+
"system_prompt": "New system prompt",
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
response = await client.patch(
|
|
340
|
+
f"/api/agents/{test_agent.id}", json=payload, headers=auth_headers
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
assert response.status_code == 200
|
|
344
|
+
data = response.json()
|
|
345
|
+
assert data["name"] == "New Name"
|
|
346
|
+
assert data["description"] == "New description"
|
|
347
|
+
assert data["model_preference"] == "nyx-lite"
|
|
348
|
+
|
|
349
|
+
@pytest.mark.asyncio
|
|
350
|
+
async def test_update_agent_partial(
|
|
351
|
+
self, client: AsyncClient, auth_headers: dict, test_agent: Agent
|
|
352
|
+
):
|
|
353
|
+
"""Should allow partial updates without affecting other fields."""
|
|
354
|
+
original_name = test_agent.name
|
|
355
|
+
payload = {"description": "Only updating description"}
|
|
356
|
+
|
|
357
|
+
response = await client.patch(
|
|
358
|
+
f"/api/agents/{test_agent.id}", json=payload, headers=auth_headers
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
assert response.status_code == 200
|
|
362
|
+
data = response.json()
|
|
363
|
+
assert data["name"] == original_name
|
|
364
|
+
assert data["description"] == "Only updating description"
|
|
365
|
+
|
|
366
|
+
@pytest.mark.asyncio
|
|
367
|
+
async def test_update_agent_not_found(
|
|
368
|
+
self, client: AsyncClient, auth_headers: dict
|
|
369
|
+
):
|
|
370
|
+
"""Should return 404 for non-existent agent."""
|
|
371
|
+
fake_id = str(uuid.uuid4())
|
|
372
|
+
payload = {"name": "New Name"}
|
|
373
|
+
|
|
374
|
+
response = await client.patch(
|
|
375
|
+
f"/api/agents/{fake_id}", json=payload, headers=auth_headers
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
assert response.status_code == 404
|
|
379
|
+
|
|
380
|
+
@pytest.mark.asyncio
|
|
381
|
+
async def test_update_agent_empty_name_rejected(
|
|
382
|
+
self, client: AsyncClient, auth_headers: dict, test_agent: Agent
|
|
383
|
+
):
|
|
384
|
+
"""Should reject update with empty name."""
|
|
385
|
+
payload = {"name": ""}
|
|
386
|
+
|
|
387
|
+
response = await client.patch(
|
|
388
|
+
f"/api/agents/{test_agent.id}", json=payload, headers=auth_headers
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
assert response.status_code == 422
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class TestDeleteAgent:
|
|
395
|
+
"""Tests for DELETE /api/agents/{agent_id}"""
|
|
396
|
+
|
|
397
|
+
@pytest.mark.asyncio
|
|
398
|
+
async def test_delete_agent_success(
|
|
399
|
+
self, client: AsyncClient, auth_headers: dict, test_agent: Agent, db_session
|
|
400
|
+
):
|
|
401
|
+
"""Should soft delete an agent."""
|
|
402
|
+
response = await client.delete(
|
|
403
|
+
f"/api/agents/{test_agent.id}", headers=auth_headers
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
assert response.status_code == 200
|
|
407
|
+
assert response.json()["deleted"] is True
|
|
408
|
+
|
|
409
|
+
# Verify soft delete (agent still exists but has deleted_at set)
|
|
410
|
+
await db_session.refresh(test_agent)
|
|
411
|
+
assert test_agent.deleted_at is not None
|
|
412
|
+
|
|
413
|
+
@pytest.mark.asyncio
|
|
414
|
+
async def test_delete_agent_not_found(
|
|
415
|
+
self, client: AsyncClient, auth_headers: dict
|
|
416
|
+
):
|
|
417
|
+
"""Should return 404 for non-existent agent."""
|
|
418
|
+
fake_id = str(uuid.uuid4())
|
|
419
|
+
|
|
420
|
+
response = await client.delete(
|
|
421
|
+
f"/api/agents/{fake_id}", headers=auth_headers
|
|
422
|
+
)
|
|
423
|
+
|
|
424
|
+
assert response.status_code == 404
|
|
425
|
+
|
|
426
|
+
@pytest.mark.asyncio
|
|
427
|
+
async def test_delete_agent_idempotent(
|
|
428
|
+
self, client: AsyncClient, auth_headers: dict, test_agent: Agent
|
|
429
|
+
):
|
|
430
|
+
"""Should return 404 when trying to delete already deleted agent."""
|
|
431
|
+
# First delete
|
|
432
|
+
response1 = await client.delete(
|
|
433
|
+
f"/api/agents/{test_agent.id}", headers=auth_headers
|
|
434
|
+
)
|
|
435
|
+
assert response1.status_code == 200
|
|
436
|
+
|
|
437
|
+
# Second delete should return 404 (agent is now "deleted")
|
|
438
|
+
response2 = await client.delete(
|
|
439
|
+
f"/api/agents/{test_agent.id}", headers=auth_headers
|
|
440
|
+
)
|
|
441
|
+
assert response2.status_code == 404
|
|
442
|
+
|
|
443
|
+
@pytest.mark.asyncio
|
|
444
|
+
async def test_deleted_agent_not_in_list(
|
|
445
|
+
self, client: AsyncClient, auth_headers: dict, test_agent: Agent
|
|
446
|
+
):
|
|
447
|
+
"""Should not include deleted agent in list response."""
|
|
448
|
+
# Delete the agent
|
|
449
|
+
await client.delete(f"/api/agents/{test_agent.id}", headers=auth_headers)
|
|
450
|
+
|
|
451
|
+
# List should be empty
|
|
452
|
+
response = await client.get("/api/agents/", headers=auth_headers)
|
|
453
|
+
|
|
454
|
+
assert response.status_code == 200
|
|
455
|
+
assert len(response.json()) == 0
|