sentinel / tests /integration /test_fastapi_server.py
harry1911's picture
Sync from GitHub main
d0d9ca3 verified
Raw
History Blame Contribute Delete
6.73 kB
"""Integration tests for the SENTINEL FastAPI server.
Tests full episode flows via TestClient covering:
- Requirements 1.5, 1.6
"""
import pytest
from fastapi.testclient import TestClient
import sentinel.api.server as server_module
from sentinel.api.server import app
# ---------------------------------------------------------------------------
# Action payloads
# ---------------------------------------------------------------------------
QUERY_LOGS = {
"agent": "holmes",
"category": "investigative",
"name": "QueryLogs",
"params": {"service": "cart-service", "time_range": [0, 60]},
}
QUERY_METRICS = {
"agent": "argus",
"category": "investigative",
"name": "QueryMetrics",
"params": {"service": "order-service", "metric_name": "cpu", "time_range": [0, 60]},
}
FORM_HYPOTHESIS = {
"agent": "holmes",
"category": "investigative",
"name": "FormHypothesis",
"params": {"service": "cart-service", "failure_type": "memory_leak", "confidence": 0.7},
}
RESTART_SERVICE = {
"agent": "forge",
"category": "remediation",
"name": "RestartService",
"params": {"service": "cart-service"},
}
SCALE_SERVICE = {
"agent": "forge",
"category": "remediation",
"name": "ScaleService",
"params": {"service": "redis-cache", "replicas": 3},
}
CLOSE_INCIDENT = {
"agent": "hermes",
"category": "meta",
"name": "CloseIncident",
"params": {"resolution_summary": "Incident resolved"},
}
QUERY_TRACE = {
"agent": "holmes",
"category": "investigative",
"name": "QueryTrace",
"params": {"trace_id": "trace-001"},
}
CANARY_DEPLOY = {
"agent": "hermes",
"category": "deployment",
"name": "CanaryDeploy",
"params": {"service": "cart-service", "version": "v2", "traffic_percent": 0.1},
}
# ---------------------------------------------------------------------------
# Observation keys expected in every step response
# ---------------------------------------------------------------------------
OBS_KEYS = {
"metrics_snapshot",
"causal_graph_snapshot",
"active_alerts",
"recent_logs",
"active_traces",
"incident_context",
"sla_state",
}
def _assert_step_response(data: dict) -> None:
"""Validate the structure of a /step response body."""
assert "observation" in data
assert "reward" in data
assert "terminated" in data
assert "truncated" in data
assert "info" in data
assert isinstance(data["observation"], dict)
assert OBS_KEYS.issubset(data["observation"].keys())
assert isinstance(data["reward"], (int, float))
assert isinstance(data["terminated"], bool)
assert isinstance(data["truncated"], bool)
assert isinstance(data["info"], dict)
# ---------------------------------------------------------------------------
# Fixture
# ---------------------------------------------------------------------------
@pytest.fixture(autouse=True)
def reset_server_env():
"""Reset the server's _env singleton before and after each test."""
server_module._env = None
yield
if server_module._env is not None:
try:
server_module._env.close()
except Exception:
pass
server_module._env = None
# ---------------------------------------------------------------------------
# Tests
# ---------------------------------------------------------------------------
def test_full_episode_investigative_actions():
"""Full episode: investigative actions + render + close.
Validates: Requirements 1.5, 1.6
"""
client = TestClient(app, raise_server_exceptions=False)
# Reset
resp = client.post("/reset", json={"seed": 42})
assert resp.status_code == 200
reset_data = resp.json()
assert "observation" in reset_data
assert "info" in reset_data
# Step 1: QueryLogs
resp = client.post("/step", json={"action": QUERY_LOGS})
assert resp.status_code == 200
_assert_step_response(resp.json())
# Step 2: QueryMetrics
resp = client.post("/step", json={"action": QUERY_METRICS})
assert resp.status_code == 200
_assert_step_response(resp.json())
# Step 3: FormHypothesis
resp = client.post("/step", json={"action": FORM_HYPOTHESIS})
assert resp.status_code == 200
_assert_step_response(resp.json())
# Render
resp = client.get("/render")
assert resp.status_code == 200
assert "render" in resp.json()
# Close
resp = client.post("/close")
assert resp.status_code == 200
assert resp.json()["status"] == "closed"
def test_full_episode_remediation_and_close():
"""Full episode: remediation actions ending with CloseIncident.
Validates: Requirements 1.5, 1.6
"""
client = TestClient(app, raise_server_exceptions=False)
# Reset
resp = client.post("/reset", json={"seed": 7})
assert resp.status_code == 200
# Step 1: RestartService
resp = client.post("/step", json={"action": RESTART_SERVICE})
assert resp.status_code == 200
_assert_step_response(resp.json())
# Step 2: ScaleService
resp = client.post("/step", json={"action": SCALE_SERVICE})
assert resp.status_code == 200
_assert_step_response(resp.json())
# Step 3: CloseIncident — should terminate the episode
resp = client.post("/step", json={"action": CLOSE_INCIDENT})
assert resp.status_code == 200
data = resp.json()
_assert_step_response(data)
assert data["terminated"] is True
def test_full_episode_deployment_flow():
"""Full episode: trace query + canary deploy + close, then health check.
Validates: Requirements 1.5, 1.6
"""
client = TestClient(app, raise_server_exceptions=False)
# Reset
resp = client.post("/reset", json={"seed": 99})
assert resp.status_code == 200
# Step 1: QueryTrace
resp = client.post("/step", json={"action": QUERY_TRACE})
assert resp.status_code == 200
_assert_step_response(resp.json())
# Step 2: CanaryDeploy
resp = client.post("/step", json={"action": CANARY_DEPLOY})
assert resp.status_code == 200
_assert_step_response(resp.json())
# Step 3: CloseIncident
resp = client.post("/step", json={"action": CLOSE_INCIDENT})
assert resp.status_code == 200
_assert_step_response(resp.json())
# Health check — env still alive (not closed), step_count >= 3
resp = client.get("/health")
assert resp.status_code == 200
health = resp.json()
assert health["initialized"] is True
assert health["step_count"] >= 3