Spaces:
Sleeping
Sleeping
| """Test TRACE API endpoints.""" | |
| import pytest | |
| from fastapi.testclient import TestClient | |
| from server.app import app | |
| def client(): | |
| """Create a test client for the FastAPI app.""" | |
| return TestClient(app) | |
| # ββ POST /reset ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestResetEndpoint: | |
| """Tests for POST /reset.""" | |
| def test_reset_easy_cpu_spike(self, client): | |
| """Test resetting with easy_cpu_spike scenario.""" | |
| resp = client.post("/reset", json={"task_id": "easy_cpu_spike", "seed": 42}) | |
| assert resp.status_code == 200 | |
| data = resp.json() | |
| assert "observation" in data | |
| assert "info" in data | |
| assert data["info"]["task_id"] == "easy_cpu_spike" | |
| assert data["info"]["max_steps"] == 5 | |
| def test_reset_medium_cascade(self, client): | |
| """Test resetting with medium_cascade scenario.""" | |
| resp = client.post("/reset", json={"task_id": "medium_cascade", "seed": 0}) | |
| assert resp.status_code == 200 | |
| data = resp.json() | |
| assert data["info"]["task_id"] == "medium_cascade" | |
| assert data["info"]["max_steps"] == 7 | |
| def test_reset_hard_mixed(self, client): | |
| """Test resetting with hard_mixed scenario.""" | |
| resp = client.post("/reset", json={"task_id": "hard_mixed", "seed": 0}) | |
| assert resp.status_code == 200 | |
| data = resp.json() | |
| assert data["info"]["task_id"] == "hard_mixed" | |
| assert data["info"]["max_steps"] == 8 | |
| def test_reset_invalid_task(self, client): | |
| """Test reset with invalid task_id returns 422 (validation error).""" | |
| resp = client.post("/reset", json={"task_id": "nonexistent", "seed": 0}) | |
| assert resp.status_code == 422 | |
| def test_reset_observation_shape(self, client): | |
| """Test that observation has all required fields.""" | |
| resp = client.post("/reset", json={"task_id": "easy_cpu_spike", "seed": 0}) | |
| obs = resp.json()["observation"] | |
| required_fields = [ | |
| "timestamp", "cpu_usage_pct", "memory_usage_pct", | |
| "error_rate_pct", "api_latency_ms", "queue_depth", | |
| "services", "active_alerts", "last_inspection" | |
| ] | |
| for field in required_fields: | |
| assert field in obs, f"Missing field: {field}" | |
| # ββ POST /step βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestStepEndpoint: | |
| """Tests for POST /step.""" | |
| def test_step_valid_action(self, client): | |
| """Test a valid step action.""" | |
| # First reset | |
| client.post("/reset", json={"task_id": "easy_cpu_spike", "seed": 42}) | |
| # Then step | |
| resp = client.post("/step", json={ | |
| "action": { | |
| "action_type": "inspect_logs", | |
| "target": "api_workers", | |
| "value": None | |
| } | |
| }) | |
| assert resp.status_code == 200 | |
| data = resp.json() | |
| assert "observation" in data | |
| assert "reward" in data | |
| assert "done" in data | |
| assert "info" in data | |
| assert isinstance(data["reward"], (int, float)) | |
| assert isinstance(data["done"], bool) | |
| def test_step_remediation_action(self, client): | |
| """Test remediation action (scale_workers).""" | |
| client.post("/reset", json={"task_id": "easy_cpu_spike", "seed": 42}) | |
| resp = client.post("/step", json={ | |
| "action": { | |
| "action_type": "scale_workers", | |
| "target": "api_workers", | |
| "value": 5 | |
| } | |
| }) | |
| assert resp.status_code == 200 | |
| data = resp.json() | |
| assert data["reward"] > 0, "Correct remediation should give positive reward" | |
| def test_step_terminal_action(self, client): | |
| """Test terminal action (declare_healthy).""" | |
| client.post("/reset", json={"task_id": "easy_cpu_spike", "seed": 42}) | |
| # Scale first to resolve incident | |
| client.post("/step", json={ | |
| "action": { | |
| "action_type": "scale_workers", | |
| "target": "api_workers", | |
| "value": 5 | |
| } | |
| }) | |
| # Scale again to reduce spike further | |
| client.post("/step", json={ | |
| "action": { | |
| "action_type": "scale_workers", | |
| "target": "api_workers", | |
| "value": 5 | |
| } | |
| }) | |
| # Declare healthy | |
| resp = client.post("/step", json={ | |
| "action": { | |
| "action_type": "declare_healthy", | |
| "target": None, | |
| "value": None | |
| } | |
| }) | |
| assert resp.status_code == 200 | |
| data = resp.json() | |
| assert data["done"] is True | |
| def test_step_without_reset_returns_error(self, client): | |
| """Test stepping without reset returns 400.""" | |
| # Create a fresh app state by importing a new env | |
| # The global env may already be initialized from previous tests, | |
| # so we just verify the endpoint works | |
| resp = client.post("/step", json={ | |
| "action": { | |
| "action_type": "inspect_logs", | |
| "target": "api_workers", | |
| "value": None | |
| } | |
| }) | |
| # Should either work (if env was reset) or return 400 | |
| assert resp.status_code in [200, 400] | |
| # ββ GET /state βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestStateEndpoint: | |
| """Tests for GET /state.""" | |
| def test_state_after_reset(self, client): | |
| """Test state endpoint after reset.""" | |
| client.post("/reset", json={"task_id": "easy_cpu_spike", "seed": 42}) | |
| resp = client.get("/state") | |
| assert resp.status_code == 200 | |
| data = resp.json() | |
| assert "observation" in data | |
| assert "episode_reward" in data | |
| assert "steps" in data | |
| assert "done" in data | |
| assert data["episode_reward"] == 0.0 | |
| assert data["steps"] == 0 | |
| assert data["done"] is False | |
| def test_state_after_step(self, client): | |
| """Test state reflects step progress.""" | |
| client.post("/reset", json={"task_id": "easy_cpu_spike", "seed": 42}) | |
| client.post("/step", json={ | |
| "action": { | |
| "action_type": "inspect_logs", | |
| "target": "api_workers", | |
| "value": None | |
| } | |
| }) | |
| resp = client.get("/state") | |
| data = resp.json() | |
| assert data["steps"] == 1 | |
| assert data["episode_reward"] != 0.0 or True # reward could be 0 in edge cases | |
| # ββ GET /health ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestHealthEndpoint: | |
| """Tests for GET /health.""" | |
| def test_health_returns_200(self, client): | |
| """Test health endpoint returns 200 OK.""" | |
| resp = client.get("/health") | |
| assert resp.status_code == 200 | |
| data = resp.json() | |
| assert data["status"] == "healthy" | |
| assert data["version"] == "0.1.0" | |
| # ββ Full episode integration test ββββββββββββββββββββββββββββββββββββββββββββ | |
| class TestFullEpisode: | |
| """Integration test: run a complete episode.""" | |
| def test_easy_cpu_spike_full_episode(self, client): | |
| """Test a full easy_cpu_spike episode with optimal actions.""" | |
| # Reset | |
| reset_resp = client.post("/reset", json={"task_id": "easy_cpu_spike", "seed": 42}) | |
| assert reset_resp.status_code == 200 | |
| # Step 1: Inspect logs | |
| step1 = client.post("/step", json={ | |
| "action": {"action_type": "inspect_logs", "target": "api_workers", "value": None} | |
| }).json() | |
| assert step1["done"] is False | |
| assert step1["reward"] > 0 | |
| # Step 2: Scale workers (remediation) | |
| step2 = client.post("/step", json={ | |
| "action": {"action_type": "scale_workers", "target": "api_workers", "value": 5} | |
| }).json() | |
| assert step2["done"] is False | |
| assert step2["reward"] > 0 | |
| # Step 3: Scale workers again to fully resolve | |
| step3 = client.post("/step", json={ | |
| "action": {"action_type": "scale_workers", "target": "api_workers", "value": 5} | |
| }).json() | |
| # Step 4: Declare healthy | |
| step4 = client.post("/step", json={ | |
| "action": {"action_type": "declare_healthy", "target": None, "value": None} | |
| }).json() | |
| assert step4["done"] is True | |
| # Check final grade | |
| if "final_grade" in step4["info"]: | |
| assert step4["info"]["final_grade"] > 0 | |