File size: 7,063 Bytes
363abf3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 | """
Tests for the new server routes: /ui/, root redirect, /state/render, /auto_step.
Run with: pytest tests/test_server_routes.py -v
"""
import pytest
from fastapi.testclient import TestClient
# Ensure the project root is importable (mirrors server/app.py sys.path setup)
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from server.app import app
client = TestClient(app, follow_redirects=False)
# ββ / redirect βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_root_redirects_to_ui():
r = client.get("/")
assert r.status_code in (307, 308), f"Expected redirect, got {r.status_code}"
assert r.headers.get("location", "").startswith("/ui")
# ββ /ui/ static serving ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_ui_serves_html():
r = TestClient(app, follow_redirects=True).get("/ui/")
# If frontend/ dir exists the page should be served; if not, we get 404
# (acceptable in CI if frontend/ hasn't been built yet)
assert r.status_code in (200, 404)
if r.status_code == 200:
assert "text/html" in r.headers.get("content-type", "")
# ββ /health βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_health():
r = client.get("/health")
assert r.status_code == 200
data = r.json()
assert data["status"] == "ok"
# ββ /state/render before reset βββββββββββββββββββββββββββββββββββββββββββββββ
def test_state_render_before_reset_returns_400():
# Force uninitialised state
from server.app import _env
_env.grid = None
_env._current_obs = None
r = client.get("/state/render")
assert r.status_code == 400
# ββ /state/render after reset ββββββββββββββββββββββββββββββββββββββββββββββββ
def test_state_render_after_reset():
client.post("/reset?task_id=easy&seed=42")
r = client.get("/state/render")
assert r.status_code == 200
data = r.json()
assert "grid" in data
assert "weather" in data
assert "resources" in data
# Easy tier = 15Γ15
assert len(data["grid"]) == 15
assert len(data["grid"][0]) == 15
# Each cell has the expected fields
cell = data["grid"][0][0]
for field in ("row", "col", "fire_state", "fire_intensity", "fuel_type",
"is_populated", "crew_present"):
assert field in cell, f"Missing field '{field}' in render cell"
# ββ /auto_step without prior reset ββββββββββββββββββββββββββββββββββββββββββ
def test_auto_step_without_reset_returns_400():
import sys
smod = sys.modules["server.app"]
smod._env._current_obs = None
smod._active_agent = None
r = client.post("/auto_step?n=1&agent=heuristic")
assert r.status_code == 400
# ββ /auto_step heuristic ββββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_auto_step_heuristic():
client.post("/reset?task_id=easy&seed=42")
r = client.post("/auto_step?n=3&agent=heuristic")
assert r.status_code == 200
data = r.json()
assert "steps" in data
assert "done" in data
assert len(data["steps"]) <= 3
for snap in data["steps"]:
assert "observation" in snap
assert "reward" in snap
assert "done" in snap
assert "info" in snap
assert "action_taken" in snap
# ββ /auto_step random βββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def test_auto_step_random():
client.post("/reset?task_id=easy&seed=0")
r = client.post("/auto_step?n=1&agent=random")
assert r.status_code == 200
data = r.json()
assert len(data["steps"]) >= 1
# ββ /auto_step agent persists across calls βββββββββββββββββββββββββββββββββββ
def test_auto_step_agent_persists():
"""
Calling /auto_step n=1 twice should not recreate the agent,
so the heuristic's internal step_count must increment correctly.
"""
import sys
smod = sys.modules["server.app"]
client.post("/reset?task_id=easy&seed=42")
assert smod._active_agent is None # cleared by /reset
client.post("/auto_step?n=1&agent=heuristic")
agent_after_first = smod._active_agent
assert agent_after_first is not None
client.post("/auto_step?n=1&agent=heuristic")
agent_after_second = smod._active_agent
# Same instance (not re-created)
assert agent_after_first is agent_after_second
# ββ /reset clears active agent ββββββββββββββββββββββββββββββββββββββββββββββ
def test_reset_clears_active_agent():
import sys
smod = sys.modules["server.app"]
client.post("/reset?task_id=easy&seed=42")
client.post("/auto_step?n=1&agent=heuristic")
assert smod._active_agent is not None
client.post("/reset?task_id=easy&seed=42")
assert smod._active_agent is None
# ββ /reset returns Observation shape βββββββββββββββββββββββββββββββββββββββββ
def test_reset_returns_observation_not_step_result():
r = client.post("/reset?task_id=easy&seed=42")
assert r.status_code == 200
data = r.json()
# Must be an Observation: has grid, weather, resources, stats
for field in ("grid", "weather", "resources", "stats"):
assert field in data, f"Expected Observation field '{field}' missing"
# Must NOT be wrapped in StepResult
assert "observation" not in data
assert "reward" not in data
# ββ /step returns StepResult shape βββββββββββββββββββββββββββββββββββββββββββ
def test_step_returns_step_result():
client.post("/reset?task_id=easy&seed=42")
action = {"action_type": "idle", "reason": "test"}
r = client.post("/step", json=action)
assert r.status_code == 200
data = r.json()
for field in ("observation", "reward", "done", "info"):
assert field in data, f"Expected StepResult field '{field}' missing"
# Observation is nested
assert "grid" in data["observation"]
|