from __future__ import annotations from fastapi.testclient import TestClient from app.main import app from app.routes import env_service client = TestClient(app) def test_root_endpoint_returns_service_info() -> None: response = client.get("/") assert response.status_code == 200 payload = response.json() assert payload["status"] == "ok" assert payload["health"] == "/health" def test_tasks_endpoint_returns_json() -> None: response = client.get("/tasks") assert response.status_code == 200 payload = response.json() assert "tasks" in payload assert "action_schema" in payload assert isinstance(payload["tasks"], list) assert isinstance(payload["action_schema"], dict) for task in payload["tasks"]: assert "name" in task assert "description" in task assert "difficulty" in task assert "parameters" in task assert "success_condition" in task action_schema = payload["action_schema"] assert action_schema["type"] == "object" assert action_schema["required"] == ["move", "accept_order", "deliver_order", "wait"] assert action_schema["additionalProperties"] is False assert "properties" in action_schema assert "move" in action_schema["properties"] assert "accept_order" in action_schema["properties"] assert "deliver_order" in action_schema["properties"] assert "wait" in action_schema["properties"] assert set(action_schema["properties"]["move"]["enum"]) == { "up", "down", "left", "right", "stay", None, } def test_state_before_reset_returns_uninitialized_state() -> None: env_service._env = None env_service._task = None env_service._success_condition = {} env_service._trajectory = [] response = client.get("/state") assert response.status_code == 200 payload = response.json() assert payload["initialized"] is False assert payload["done"] is False assert payload["step_count"] == 0 assert payload["observation"] is None def test_reset_step_state_baseline_and_grader_flow() -> None: reset_response = client.post("/reset", json={"task": "easy", "seed": 7}) assert reset_response.status_code == 200 baseline_response = client.get("/baseline", params={"task": "easy", "seed": 7, "max_steps": 20}) assert baseline_response.status_code == 200 baseline_payload = baseline_response.json() assert baseline_payload["agent"] == "BaselineGreedyAgent" assert baseline_payload["task"] == "easy" assert baseline_payload["steps_executed"] <= 20 assert 0.0 <= baseline_payload["score"] <= 1.0 assert "report" in baseline_payload step_response = client.post( "/step", json={"move": None, "accept_order": None, "deliver_order": False, "wait": True}, ) assert step_response.status_code == 200 state_response = client.get("/state") assert state_response.status_code == 200 grader_response = client.get("/grader") assert grader_response.status_code == 200 grader_payload = grader_response.json() assert "report" in grader_payload assert 0.0 <= grader_payload["report"]["score"] <= 1.0 def test_reset_without_body_uses_defaults() -> None: response = client.post("/reset") assert response.status_code == 200 payload = response.json() assert "agent_location" in payload def test_step_accepts_direct_action_payload() -> None: client.post("/reset", json={"task": "easy", "seed": 7}) response = client.post( "/step", json={"move": None, "accept_order": None, "deliver_order": False, "wait": True}, ) assert response.status_code == 200 payload = response.json() assert {"observation", "reward", "done", "info"}.issubset(payload.keys()) assert isinstance(payload["reward"], float) assert isinstance(payload["done"], bool) assert isinstance(payload["info"], dict) def test_step_rejects_wrapper_payload_for_strict_schema() -> None: client.post("/reset", json={"task": "easy", "seed": 7}) response = client.post( "/step", json={ "action": { "move": None, "accept_order": None, "deliver_order": False, "wait": True, } }, ) assert response.status_code == 400 payload = response.json() assert payload["detail"]["error"] == "bad_request" def test_step_invalid_payload_returns_clean_error() -> None: client.post("/reset", json={"task": "easy", "seed": 7}) response = client.post("/step", json={"move": "right"}) assert response.status_code == 400 payload = response.json() assert payload["detail"]["error"] == "bad_request" def test_grader_before_reset_returns_clean_error() -> None: env_service._env = None env_service._task = None env_service._success_condition = {} env_service._trajectory = [] response = client.get("/grader") assert response.status_code == 400 payload = response.json() assert payload["detail"]["error"] == "bad_request" def test_baseline_runs_without_active_environment() -> None: env_service._env = None env_service._task = None env_service._success_condition = {} env_service._trajectory = [] response = client.get("/baseline", params={"task": "easy", "seed": 1, "max_steps": 10}) assert response.status_code == 200 payload = response.json() assert payload["agent"] == "BaselineGreedyAgent" assert payload["task"] == "easy" assert payload["steps_executed"] <= 10 assert isinstance(payload["done"], bool) assert 0.0 <= payload["score"] <= 1.0 def test_baseline_invalid_task_returns_clean_error() -> None: response = client.get("/baseline", params={"task": "unknown_task"}) assert response.status_code == 400 payload = response.json() assert payload["detail"]["error"] == "bad_request" def test_reset_from_scenario_accepts_real_world_payload() -> None: response = client.post( "/reset_from_scenario", json={ "seed": 99, "success_condition": { "completion_rate_min": 1.0, "max_steps": 20, "invalid_action_rate_max": 0.2, "battery_depletion": False, }, "scenario": { "width": 6, "height": 6, "max_steps": 20, "agent_start": {"x": 0, "y": 0}, "orders": [ { "order_id": "r1", "pickup": {"x": 1, "y": 0}, "dropoff": {"x": 2, "y": 0}, "delivery_locations": [{"x": 2, "y": 0}], "priority": "high", } ], "obstacles": [{"x": 4, "y": 4}], "dynamic_obstacles": [{"x": 4, "y": 5}], "traffic_zones": [{"location": {"x": 3, "y": 0}, "extra_cost": 3}], "charging_stations": [{"x": 0, "y": 0}], "battery_profile": { "enabled": True, "capacity": 10, "recharge_rate": 4, "initial_level": 7, }, }, }, ) assert response.status_code == 200 payload = response.json() assert payload["grid_width"] == 6 assert payload["grid_height"] == 6 assert payload["battery_level"] == 7 assert len(payload["pending_orders"]) == 1 assert sorted((item["x"], item["y"]) for item in payload["dynamic_obstacles"]) == [(4, 5)] step_response = client.post( "/step", json={"move": None, "accept_order": None, "deliver_order": False, "wait": True}, ) assert step_response.status_code == 200 grader_response = client.get("/grader") assert grader_response.status_code == 200 grader_payload = grader_response.json() assert grader_payload["task"] == "scenario" assert grader_payload["report"]["success_condition_used"]["max_steps"] == 20 def test_reset_from_scenario_rejects_invalid_world_geometry() -> None: response = client.post( "/reset_from_scenario", json={ "scenario": { "width": 6, "height": 6, "max_steps": 20, "orders": [ { "pickup": {"x": 1, "y": 1}, "dropoff": {"x": 2, "y": 2}, } ], "obstacles": [{"x": 9, "y": 9}], "dynamic_obstacles": [], "traffic_zones": [], "charging_stations": [{"x": 0, "y": 0}], "battery_profile": {"enabled": False}, } }, ) assert response.status_code == 400 payload = response.json() assert payload["detail"]["error"] == "bad_request"