"""Tests for the Morrowind FastAPI harness endpoints.""" from __future__ import annotations import json from datetime import UTC, datetime from pathlib import Path from unittest.mock import patch import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from src.infrastructure.morrowind.api import ( MorrowindStatus, router, ) from src.infrastructure.morrowind.schemas import ( CommandType, PerceptionOutput, ) # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- NOW = datetime(2026, 3, 21, 14, 30, 0, tzinfo=UTC) SAMPLE_PERCEPTION = { "timestamp": NOW.isoformat(), "agent_id": "timmy", "location": {"cell": "Balmora", "x": 1024.5, "y": -512.3, "z": 64.0, "interior": False}, "health": {"current": 85, "max": 100}, "nearby_entities": [], "inventory_summary": {"gold": 42, "item_count": 5, "encumbrance_pct": 0.3}, "active_quests": [{"quest_id": "mq_01", "name": "Report to Caius", "stage": 10}], "environment": { "time_of_day": "afternoon", "weather": "clear", "is_combat": False, "is_dialogue": False, }, } SAMPLE_COMMAND = { "timestamp": NOW.isoformat(), "agent_id": "timmy", "command": "move_to", "params": {"target_cell": "Balmora", "target_x": 1050.0}, "reasoning": "Moving closer to the quest target.", } @pytest.fixture() def morrowind_app(): """Create a minimal FastAPI app with only the morrowind router.""" import src.infrastructure.morrowind.api as api_mod # Reset module-level state between tests. api_mod._command_logger = None api_mod._command_queue.clear() api_mod._last_perception = None api_mod._last_perception_ts = None app = FastAPI() app.include_router(router) return app @pytest.fixture() def client(morrowind_app): with TestClient(morrowind_app) as c: yield c # --------------------------------------------------------------------------- # GET /perception # --------------------------------------------------------------------------- class TestGetPerception: def test_returns_perception_when_file_exists(self, client, tmp_path): pfile = tmp_path / "perception.json" pfile.write_text(json.dumps(SAMPLE_PERCEPTION), encoding="utf-8") with patch("src.infrastructure.morrowind.api.PERCEPTION_FILE", pfile): resp = client.get("/api/v1/morrowind/perception") assert resp.status_code == 200 data = resp.json() assert data["agent_id"] == "timmy" assert data["location"]["cell"] == "Balmora" assert data["health"]["current"] == 85 def test_404_when_file_missing(self, client, tmp_path): missing = tmp_path / "does_not_exist.json" with patch("src.infrastructure.morrowind.api.PERCEPTION_FILE", missing): resp = client.get("/api/v1/morrowind/perception") assert resp.status_code == 404 assert "not found" in resp.json()["detail"].lower() def test_500_when_file_is_invalid_json(self, client, tmp_path): pfile = tmp_path / "perception.json" pfile.write_text("not json", encoding="utf-8") with patch("src.infrastructure.morrowind.api.PERCEPTION_FILE", pfile): resp = client.get("/api/v1/morrowind/perception") assert resp.status_code == 500 def test_422_when_schema_invalid(self, client, tmp_path): pfile = tmp_path / "perception.json" # Missing required fields. pfile.write_text(json.dumps({"agent_id": "timmy"}), encoding="utf-8") with patch("src.infrastructure.morrowind.api.PERCEPTION_FILE", pfile): resp = client.get("/api/v1/morrowind/perception") assert resp.status_code == 422 # --------------------------------------------------------------------------- # POST /command # --------------------------------------------------------------------------- class TestPostCommand: def test_accepts_valid_command(self, client, tmp_path): # Provide a CommandLogger backed by a temp DB. from src.infrastructure.morrowind.command_log import CommandLogger db_url = f"sqlite:///{tmp_path / 'cmd.db'}" mock_logger = CommandLogger(db_url=db_url) with patch("src.infrastructure.morrowind.api._get_command_logger", return_value=mock_logger): resp = client.post("/api/v1/morrowind/command", json=SAMPLE_COMMAND) assert resp.status_code == 200 data = resp.json() assert data["status"] == "accepted" assert "command_id" in data assert data["command_id"] >= 1 def test_rejects_invalid_command_type(self, client): bad_cmd = {**SAMPLE_COMMAND, "command": "fly_to_moon"} resp = client.post("/api/v1/morrowind/command", json=bad_cmd) assert resp.status_code == 422 def test_rejects_empty_reasoning(self, client): bad_cmd = {**SAMPLE_COMMAND, "reasoning": ""} resp = client.post("/api/v1/morrowind/command", json=bad_cmd) assert resp.status_code == 422 def test_rejects_missing_fields(self, client): resp = client.post("/api/v1/morrowind/command", json={"command": "noop"}) assert resp.status_code == 422 def test_command_queued_for_forwarding(self, client, tmp_path): import src.infrastructure.morrowind.api as api_mod from src.infrastructure.morrowind.command_log import CommandLogger db_url = f"sqlite:///{tmp_path / 'cmd.db'}" mock_logger = CommandLogger(db_url=db_url) with patch("src.infrastructure.morrowind.api._get_command_logger", return_value=mock_logger): client.post("/api/v1/morrowind/command", json=SAMPLE_COMMAND) assert len(api_mod._command_queue) == 1 assert api_mod._command_queue[0]["command"]["command"] == "move_to" # --------------------------------------------------------------------------- # GET /morrowind/status # --------------------------------------------------------------------------- class TestGetStatus: def test_status_when_no_perception(self, client, tmp_path): missing = tmp_path / "no_such_file.json" with patch("src.infrastructure.morrowind.api.PERCEPTION_FILE", missing): resp = client.get("/api/v1/morrowind/status") assert resp.status_code == 200 data = resp.json() assert data["connected"] is False assert data["last_perception_at"] is None assert data["command_queue_depth"] == 0 assert data["current_cell"] is None def test_status_after_perception_read(self, client, tmp_path): pfile = tmp_path / "perception.json" pfile.write_text(json.dumps(SAMPLE_PERCEPTION), encoding="utf-8") with patch("src.infrastructure.morrowind.api.PERCEPTION_FILE", pfile): # First read perception to populate cache. client.get("/api/v1/morrowind/perception") # Then check status. resp = client.get("/api/v1/morrowind/status") assert resp.status_code == 200 data = resp.json() assert data["connected"] is True assert data["last_perception_at"] is not None assert data["current_cell"] == "Balmora" assert data["vitals"]["health_current"] == 85 assert data["vitals"]["health_max"] == 100 def test_status_includes_queue_depth(self, client, tmp_path): import src.infrastructure.morrowind.api as api_mod from src.infrastructure.morrowind.command_log import CommandLogger db_url = f"sqlite:///{tmp_path / 'cmd.db'}" mock_logger = CommandLogger(db_url=db_url) with ( patch("src.infrastructure.morrowind.api._get_command_logger", return_value=mock_logger), patch("src.infrastructure.morrowind.api.PERCEPTION_FILE", tmp_path / "nope.json"), ): # Submit two commands. client.post("/api/v1/morrowind/command", json=SAMPLE_COMMAND) client.post("/api/v1/morrowind/command", json=SAMPLE_COMMAND) resp = client.get("/api/v1/morrowind/status") assert resp.json()["command_queue_depth"] == 2