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,456 @@
|
|
|
1
|
+
"""
|
|
2
|
+
End-to-end tests for Agent Commands functionality.
|
|
3
|
+
|
|
4
|
+
Tests cover:
|
|
5
|
+
- Issue restart command
|
|
6
|
+
- Issue stop command
|
|
7
|
+
- Commands dispatched via heartbeat
|
|
8
|
+
- Command expiry
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
import uuid
|
|
13
|
+
from datetime import datetime, timedelta, UTC
|
|
14
|
+
from httpx import AsyncClient
|
|
15
|
+
from sqlalchemy import select
|
|
16
|
+
|
|
17
|
+
from backend.models.agent import Agent, AgentCommand
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TestIssueRestartCommand:
|
|
21
|
+
"""Tests for POST /api/agents/{agent_id}/restart"""
|
|
22
|
+
|
|
23
|
+
@pytest.mark.asyncio
|
|
24
|
+
async def test_issue_restart_command_success(
|
|
25
|
+
self, client: AsyncClient, auth_headers: dict, test_agent: Agent
|
|
26
|
+
):
|
|
27
|
+
"""Should successfully issue a restart command."""
|
|
28
|
+
response = await client.post(
|
|
29
|
+
f"/api/agents/{test_agent.id}/restart", headers=auth_headers
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
assert response.status_code == 200
|
|
33
|
+
data = response.json()
|
|
34
|
+
assert data["command_type"] == "restart"
|
|
35
|
+
assert data["status"] == "pending"
|
|
36
|
+
assert "id" in data
|
|
37
|
+
assert "created_at" in data
|
|
38
|
+
assert "expires_at" in data
|
|
39
|
+
|
|
40
|
+
@pytest.mark.asyncio
|
|
41
|
+
async def test_restart_increments_restart_count(
|
|
42
|
+
self, client: AsyncClient, auth_headers: dict, test_agent: Agent, db_session
|
|
43
|
+
):
|
|
44
|
+
"""Should increment restart_count when restart command is issued."""
|
|
45
|
+
original_count = test_agent.restart_count
|
|
46
|
+
|
|
47
|
+
await client.post(
|
|
48
|
+
f"/api/agents/{test_agent.id}/restart", headers=auth_headers
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
await db_session.refresh(test_agent)
|
|
52
|
+
|
|
53
|
+
assert test_agent.restart_count == original_count + 1
|
|
54
|
+
assert test_agent.last_restart_at is not None
|
|
55
|
+
assert test_agent.last_restart_reason == "user_requested"
|
|
56
|
+
|
|
57
|
+
@pytest.mark.asyncio
|
|
58
|
+
async def test_restart_creates_command_record(
|
|
59
|
+
self, client: AsyncClient, auth_headers: dict, test_agent: Agent, db_session
|
|
60
|
+
):
|
|
61
|
+
"""Should create a command record in the database."""
|
|
62
|
+
response = await client.post(
|
|
63
|
+
f"/api/agents/{test_agent.id}/restart", headers=auth_headers
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
command_id = response.json()["id"]
|
|
67
|
+
|
|
68
|
+
stmt = select(AgentCommand).where(AgentCommand.id == command_id)
|
|
69
|
+
result = await db_session.execute(stmt)
|
|
70
|
+
command = result.scalar_one_or_none()
|
|
71
|
+
|
|
72
|
+
assert command is not None
|
|
73
|
+
assert command.command_type == "restart"
|
|
74
|
+
assert command.agent_id == test_agent.id
|
|
75
|
+
assert command.status == "pending"
|
|
76
|
+
|
|
77
|
+
@pytest.mark.asyncio
|
|
78
|
+
async def test_restart_agent_not_found(
|
|
79
|
+
self, client: AsyncClient, auth_headers: dict
|
|
80
|
+
):
|
|
81
|
+
"""Should return 404 for non-existent agent."""
|
|
82
|
+
fake_id = str(uuid.uuid4())
|
|
83
|
+
|
|
84
|
+
response = await client.post(
|
|
85
|
+
f"/api/agents/{fake_id}/restart", headers=auth_headers
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
assert response.status_code == 404
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class TestIssueStopCommand:
|
|
92
|
+
"""Tests for POST /api/agents/{agent_id}/stop"""
|
|
93
|
+
|
|
94
|
+
@pytest.mark.asyncio
|
|
95
|
+
async def test_issue_stop_command_success(
|
|
96
|
+
self, client: AsyncClient, auth_headers: dict, test_agent: Agent
|
|
97
|
+
):
|
|
98
|
+
"""Should successfully issue a stop command."""
|
|
99
|
+
response = await client.post(
|
|
100
|
+
f"/api/agents/{test_agent.id}/stop", headers=auth_headers
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
assert response.status_code == 200
|
|
104
|
+
data = response.json()
|
|
105
|
+
assert data["command_type"] == "stop"
|
|
106
|
+
assert data["status"] == "pending"
|
|
107
|
+
|
|
108
|
+
@pytest.mark.asyncio
|
|
109
|
+
async def test_stop_does_not_increment_restart_count(
|
|
110
|
+
self, client: AsyncClient, auth_headers: dict, test_agent: Agent, db_session
|
|
111
|
+
):
|
|
112
|
+
"""Should not increment restart_count for stop command."""
|
|
113
|
+
original_count = test_agent.restart_count
|
|
114
|
+
|
|
115
|
+
await client.post(
|
|
116
|
+
f"/api/agents/{test_agent.id}/stop", headers=auth_headers
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
await db_session.refresh(test_agent)
|
|
120
|
+
|
|
121
|
+
assert test_agent.restart_count == original_count
|
|
122
|
+
|
|
123
|
+
@pytest.mark.asyncio
|
|
124
|
+
async def test_stop_agent_not_found(
|
|
125
|
+
self, client: AsyncClient, auth_headers: dict
|
|
126
|
+
):
|
|
127
|
+
"""Should return 404 for non-existent agent."""
|
|
128
|
+
fake_id = str(uuid.uuid4())
|
|
129
|
+
|
|
130
|
+
response = await client.post(
|
|
131
|
+
f"/api/agents/{fake_id}/stop", headers=auth_headers
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
assert response.status_code == 404
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class TestIssueGenericCommand:
|
|
138
|
+
"""Tests for POST /api/agents/{agent_id}/command"""
|
|
139
|
+
|
|
140
|
+
@pytest.mark.asyncio
|
|
141
|
+
async def test_issue_restart_via_generic_endpoint(
|
|
142
|
+
self, client: AsyncClient, auth_headers: dict, test_agent: Agent
|
|
143
|
+
):
|
|
144
|
+
"""Should issue restart command via generic endpoint."""
|
|
145
|
+
payload = {"command_type": "restart"}
|
|
146
|
+
|
|
147
|
+
response = await client.post(
|
|
148
|
+
f"/api/agents/{test_agent.id}/command",
|
|
149
|
+
json=payload,
|
|
150
|
+
headers=auth_headers,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
assert response.status_code == 200
|
|
154
|
+
assert response.json()["command_type"] == "restart"
|
|
155
|
+
|
|
156
|
+
@pytest.mark.asyncio
|
|
157
|
+
async def test_issue_stop_via_generic_endpoint(
|
|
158
|
+
self, client: AsyncClient, auth_headers: dict, test_agent: Agent
|
|
159
|
+
):
|
|
160
|
+
"""Should issue stop command via generic endpoint."""
|
|
161
|
+
payload = {"command_type": "stop"}
|
|
162
|
+
|
|
163
|
+
response = await client.post(
|
|
164
|
+
f"/api/agents/{test_agent.id}/command",
|
|
165
|
+
json=payload,
|
|
166
|
+
headers=auth_headers,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
assert response.status_code == 200
|
|
170
|
+
assert response.json()["command_type"] == "stop"
|
|
171
|
+
|
|
172
|
+
@pytest.mark.asyncio
|
|
173
|
+
async def test_issue_config_refresh_command(
|
|
174
|
+
self, client: AsyncClient, auth_headers: dict, test_agent: Agent
|
|
175
|
+
):
|
|
176
|
+
"""Should issue config_refresh command."""
|
|
177
|
+
payload = {"command_type": "config_refresh"}
|
|
178
|
+
|
|
179
|
+
response = await client.post(
|
|
180
|
+
f"/api/agents/{test_agent.id}/command",
|
|
181
|
+
json=payload,
|
|
182
|
+
headers=auth_headers,
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
assert response.status_code == 200
|
|
186
|
+
assert response.json()["command_type"] == "config_refresh"
|
|
187
|
+
|
|
188
|
+
@pytest.mark.asyncio
|
|
189
|
+
async def test_issue_command_with_payload(
|
|
190
|
+
self, client: AsyncClient, auth_headers: dict, test_agent: Agent, db_session
|
|
191
|
+
):
|
|
192
|
+
"""Should issue command with custom payload."""
|
|
193
|
+
payload = {
|
|
194
|
+
"command_type": "config_refresh",
|
|
195
|
+
"payload": {"config_key": "max_tokens", "config_value": 4096},
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
response = await client.post(
|
|
199
|
+
f"/api/agents/{test_agent.id}/command",
|
|
200
|
+
json=payload,
|
|
201
|
+
headers=auth_headers,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
assert response.status_code == 200
|
|
205
|
+
command_id = response.json()["id"]
|
|
206
|
+
|
|
207
|
+
stmt = select(AgentCommand).where(AgentCommand.id == command_id)
|
|
208
|
+
result = await db_session.execute(stmt)
|
|
209
|
+
command = result.scalar_one_or_none()
|
|
210
|
+
|
|
211
|
+
assert command.payload == {"config_key": "max_tokens", "config_value": 4096}
|
|
212
|
+
|
|
213
|
+
@pytest.mark.asyncio
|
|
214
|
+
async def test_issue_invalid_command_type(
|
|
215
|
+
self, client: AsyncClient, auth_headers: dict, test_agent: Agent
|
|
216
|
+
):
|
|
217
|
+
"""Should reject invalid command type."""
|
|
218
|
+
payload = {"command_type": "invalid_command"}
|
|
219
|
+
|
|
220
|
+
response = await client.post(
|
|
221
|
+
f"/api/agents/{test_agent.id}/command",
|
|
222
|
+
json=payload,
|
|
223
|
+
headers=auth_headers,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
assert response.status_code == 422
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class TestCommandDispatchViaHeartbeat:
|
|
230
|
+
"""Tests for command dispatch via heartbeat."""
|
|
231
|
+
|
|
232
|
+
@pytest.mark.asyncio
|
|
233
|
+
async def test_command_delivered_via_heartbeat(
|
|
234
|
+
self, client: AsyncClient, auth_headers: dict, api_key_headers: dict,
|
|
235
|
+
test_agent: Agent
|
|
236
|
+
):
|
|
237
|
+
"""Should deliver pending command via heartbeat response."""
|
|
238
|
+
# Issue a restart command
|
|
239
|
+
await client.post(
|
|
240
|
+
f"/api/agents/{test_agent.id}/restart", headers=auth_headers
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
# Send heartbeat
|
|
244
|
+
heartbeat_payload = {
|
|
245
|
+
"agent_id": test_agent.id,
|
|
246
|
+
"status": "online",
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
response = await client.post(
|
|
250
|
+
"/api/agents/heartbeat",
|
|
251
|
+
json=heartbeat_payload,
|
|
252
|
+
headers=api_key_headers,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
assert response.status_code == 200
|
|
256
|
+
data = response.json()
|
|
257
|
+
assert len(data["commands"]) == 1
|
|
258
|
+
assert data["commands"][0]["type"] == "restart"
|
|
259
|
+
|
|
260
|
+
@pytest.mark.asyncio
|
|
261
|
+
async def test_command_has_signature(
|
|
262
|
+
self, client: AsyncClient, auth_headers: dict, api_key_headers: dict,
|
|
263
|
+
test_agent: Agent
|
|
264
|
+
):
|
|
265
|
+
"""Should include signature in delivered command."""
|
|
266
|
+
# Issue command
|
|
267
|
+
await client.post(
|
|
268
|
+
f"/api/agents/{test_agent.id}/stop", headers=auth_headers
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Send heartbeat
|
|
272
|
+
heartbeat_payload = {
|
|
273
|
+
"agent_id": test_agent.id,
|
|
274
|
+
"status": "online",
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
response = await client.post(
|
|
278
|
+
"/api/agents/heartbeat",
|
|
279
|
+
json=heartbeat_payload,
|
|
280
|
+
headers=api_key_headers,
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
data = response.json()
|
|
284
|
+
command = data["commands"][0]
|
|
285
|
+
assert "signature" in command
|
|
286
|
+
assert len(command["signature"]) == 64 # SHA256 hex
|
|
287
|
+
|
|
288
|
+
@pytest.mark.asyncio
|
|
289
|
+
async def test_command_status_changes_to_dispatched(
|
|
290
|
+
self, client: AsyncClient, auth_headers: dict, api_key_headers: dict,
|
|
291
|
+
test_agent: Agent, db_session
|
|
292
|
+
):
|
|
293
|
+
"""Should change command status to 'dispatched' after delivery."""
|
|
294
|
+
# Issue command
|
|
295
|
+
cmd_response = await client.post(
|
|
296
|
+
f"/api/agents/{test_agent.id}/restart", headers=auth_headers
|
|
297
|
+
)
|
|
298
|
+
command_id = cmd_response.json()["id"]
|
|
299
|
+
|
|
300
|
+
# Send heartbeat
|
|
301
|
+
heartbeat_payload = {
|
|
302
|
+
"agent_id": test_agent.id,
|
|
303
|
+
"status": "online",
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
await client.post(
|
|
307
|
+
"/api/agents/heartbeat",
|
|
308
|
+
json=heartbeat_payload,
|
|
309
|
+
headers=api_key_headers,
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
# Check command status
|
|
313
|
+
stmt = select(AgentCommand).where(AgentCommand.id == command_id)
|
|
314
|
+
result = await db_session.execute(stmt)
|
|
315
|
+
command = result.scalar_one_or_none()
|
|
316
|
+
|
|
317
|
+
assert command.status == "dispatched"
|
|
318
|
+
assert command.dispatched_at is not None
|
|
319
|
+
|
|
320
|
+
@pytest.mark.asyncio
|
|
321
|
+
async def test_command_not_delivered_twice(
|
|
322
|
+
self, client: AsyncClient, auth_headers: dict, api_key_headers: dict,
|
|
323
|
+
test_agent: Agent
|
|
324
|
+
):
|
|
325
|
+
"""Should not deliver same command multiple times."""
|
|
326
|
+
# Issue command
|
|
327
|
+
await client.post(
|
|
328
|
+
f"/api/agents/{test_agent.id}/restart", headers=auth_headers
|
|
329
|
+
)
|
|
330
|
+
|
|
331
|
+
heartbeat_payload = {
|
|
332
|
+
"agent_id": test_agent.id,
|
|
333
|
+
"status": "online",
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
# First heartbeat - should get command
|
|
337
|
+
response1 = await client.post(
|
|
338
|
+
"/api/agents/heartbeat",
|
|
339
|
+
json=heartbeat_payload,
|
|
340
|
+
headers=api_key_headers,
|
|
341
|
+
)
|
|
342
|
+
assert len(response1.json()["commands"]) == 1
|
|
343
|
+
|
|
344
|
+
# Second heartbeat - should not get command again
|
|
345
|
+
response2 = await client.post(
|
|
346
|
+
"/api/agents/heartbeat",
|
|
347
|
+
json=heartbeat_payload,
|
|
348
|
+
headers=api_key_headers,
|
|
349
|
+
)
|
|
350
|
+
assert len(response2.json()["commands"]) == 0
|
|
351
|
+
|
|
352
|
+
@pytest.mark.asyncio
|
|
353
|
+
async def test_multiple_commands_delivered_in_order(
|
|
354
|
+
self, client: AsyncClient, auth_headers: dict, api_key_headers: dict,
|
|
355
|
+
test_agent: Agent
|
|
356
|
+
):
|
|
357
|
+
"""Should deliver multiple pending commands in creation order."""
|
|
358
|
+
# Issue multiple commands
|
|
359
|
+
await client.post(
|
|
360
|
+
f"/api/agents/{test_agent.id}/restart", headers=auth_headers
|
|
361
|
+
)
|
|
362
|
+
await client.post(
|
|
363
|
+
f"/api/agents/{test_agent.id}/stop", headers=auth_headers
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
heartbeat_payload = {
|
|
367
|
+
"agent_id": test_agent.id,
|
|
368
|
+
"status": "online",
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
response = await client.post(
|
|
372
|
+
"/api/agents/heartbeat",
|
|
373
|
+
json=heartbeat_payload,
|
|
374
|
+
headers=api_key_headers,
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
data = response.json()
|
|
378
|
+
assert len(data["commands"]) == 2
|
|
379
|
+
assert data["commands"][0]["type"] == "restart"
|
|
380
|
+
assert data["commands"][1]["type"] == "stop"
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
class TestCommandExpiry:
|
|
384
|
+
"""Tests for command expiration."""
|
|
385
|
+
|
|
386
|
+
@pytest.mark.asyncio
|
|
387
|
+
async def test_command_has_expiry_time(
|
|
388
|
+
self, client: AsyncClient, auth_headers: dict, test_agent: Agent
|
|
389
|
+
):
|
|
390
|
+
"""Should set expiry time on command (default ~5 minutes)."""
|
|
391
|
+
response = await client.post(
|
|
392
|
+
f"/api/agents/{test_agent.id}/restart", headers=auth_headers
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
data = response.json()
|
|
396
|
+
expires_at = datetime.fromisoformat(data["expires_at"].replace("Z", "+00:00"))
|
|
397
|
+
created_at = datetime.fromisoformat(data["created_at"].replace("Z", "+00:00"))
|
|
398
|
+
|
|
399
|
+
# Should expire within 3-10 minutes
|
|
400
|
+
assert expires_at > created_at + timedelta(minutes=3)
|
|
401
|
+
assert expires_at < created_at + timedelta(minutes=10)
|
|
402
|
+
|
|
403
|
+
@pytest.mark.asyncio
|
|
404
|
+
async def test_expired_command_not_delivered(
|
|
405
|
+
self, client: AsyncClient, api_key_headers: dict, test_agent: Agent,
|
|
406
|
+
agent_factory
|
|
407
|
+
):
|
|
408
|
+
"""Should not deliver expired commands."""
|
|
409
|
+
# Create an expired command
|
|
410
|
+
await agent_factory.create_command(
|
|
411
|
+
test_agent.id,
|
|
412
|
+
command_type="restart",
|
|
413
|
+
status="pending",
|
|
414
|
+
expires_at=datetime.now(UTC) - timedelta(minutes=10),
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
heartbeat_payload = {
|
|
418
|
+
"agent_id": test_agent.id,
|
|
419
|
+
"status": "online",
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
response = await client.post(
|
|
423
|
+
"/api/agents/heartbeat",
|
|
424
|
+
json=heartbeat_payload,
|
|
425
|
+
headers=api_key_headers,
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
data = response.json()
|
|
429
|
+
assert len(data["commands"]) == 0
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
class TestCommandEventLogging:
|
|
433
|
+
"""Tests for command event logging."""
|
|
434
|
+
|
|
435
|
+
@pytest.mark.asyncio
|
|
436
|
+
async def test_command_issued_event_logged(
|
|
437
|
+
self, client: AsyncClient, auth_headers: dict, test_agent: Agent, db_session
|
|
438
|
+
):
|
|
439
|
+
"""Should log command_issued event when command is created."""
|
|
440
|
+
from backend.models.agent import AgentEvent
|
|
441
|
+
|
|
442
|
+
await client.post(
|
|
443
|
+
f"/api/agents/{test_agent.id}/restart", headers=auth_headers
|
|
444
|
+
)
|
|
445
|
+
|
|
446
|
+
# Check for event
|
|
447
|
+
stmt = (
|
|
448
|
+
select(AgentEvent)
|
|
449
|
+
.where(AgentEvent.agent_id == test_agent.id)
|
|
450
|
+
.where(AgentEvent.event_type == "command_issued")
|
|
451
|
+
)
|
|
452
|
+
result = await db_session.execute(stmt)
|
|
453
|
+
event = result.scalar_one_or_none()
|
|
454
|
+
|
|
455
|
+
assert event is not None
|
|
456
|
+
assert event.event_data["command_type"] == "restart"
|