This repository has been archived on 2026-03-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Timmy-time-dashboard/tests/test_morrowind_api.py
Perplexity Computer f634886f9b feat: FastAPI Morrowind harness + SOUL.md framework (#821, #854)
## FastAPI Morrowind Harness (#821)
- GET /api/v1/morrowind/perception — reads perception.json, validates against PerceptionOutput
- POST /api/v1/morrowind/command — validates CommandInput, logs via CommandLogger, stubs bridge
- GET /api/v1/morrowind/status — connection state, last perception, queue depth, vitals
- Router registered in dashboard app.py

## SOUL.md Framework (#854)
- Template, authoring guide, and role extensions docs in docs/soul-framework/
- SoulLoader: parse SOUL.md files into structured SoulDocument
- SoulValidator: check required sections, detect contradictions, validate structure
- SoulVersioner: hash-based change detection and JSONL history tracking
- 39 tests covering all endpoints and framework components

Depends on #864

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-21 22:49:56 +00:00

245 lines
8.2 KiB
Python

"""Tests for the Morrowind FastAPI harness endpoints.
Covers:
- GET /api/v1/morrowind/perception
- POST /api/v1/morrowind/command
- GET /api/v1/morrowind/status
"""
from __future__ import annotations
import json
from datetime import UTC, datetime
from pathlib import Path
from unittest.mock import MagicMock, patch
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from infrastructure.morrowind.api import router, _get_command_logger
from infrastructure.morrowind import api as api_module
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
SAMPLE_PERCEPTION = {
"protocol_version": "1.0.0",
"timestamp": "2024-06-15T10:30:00Z",
"agent_id": "timmy",
"location": {
"cell": "Balmora, Guild of Mages",
"x": 1234.5,
"y": 6789.0,
"z": 0.0,
"interior": True,
},
"health": {"current": 80, "max": 100},
"nearby_entities": [
{
"entity_id": "npc_001",
"name": "Ranis Athrys",
"entity_type": "npc",
"distance": 5.2,
"disposition": 65,
}
],
"inventory_summary": {
"gold": 250,
"item_count": 12,
"encumbrance_pct": 0.45,
},
"active_quests": [
{"quest_id": "mq_01", "name": "A Mysterious Note", "stage": 2}
],
"environment": {
"time_of_day": "morning",
"weather": "clear",
"is_combat": False,
"is_dialogue": False,
},
}
SAMPLE_COMMAND = {
"protocol_version": "1.0.0",
"timestamp": "2024-06-15T10:31:00Z",
"agent_id": "timmy",
"command": "move_to",
"params": {"x": 1300.0, "y": 6800.0, "z": 0.0},
"reasoning": "Moving to the guild entrance to speak with the quest giver.",
"episode_id": "ep_001",
}
@pytest.fixture
def app():
"""Create a fresh FastAPI app with the morrowind router."""
test_app = FastAPI()
test_app.include_router(router)
return test_app
@pytest.fixture
def client(app):
"""FastAPI test client."""
with TestClient(app) as c:
yield c
@pytest.fixture
def perception_file(tmp_path):
"""Write sample perception JSON to a temp file and patch the module path."""
p = tmp_path / "perception.json"
p.write_text(json.dumps(SAMPLE_PERCEPTION), encoding="utf-8")
with patch.object(api_module, "PERCEPTION_PATH", p):
yield p
@pytest.fixture
def mock_command_logger():
"""Patch the command logger with a mock."""
mock_logger = MagicMock()
mock_logger.log_command.return_value = 42
mock_logger.count.return_value = 7
with patch.object(api_module, "_command_logger", mock_logger):
yield mock_logger
# ---------------------------------------------------------------------------
# GET /perception
# ---------------------------------------------------------------------------
class TestGetPerception:
def test_success(self, client, perception_file):
"""Perception endpoint returns validated data."""
response = client.get("/api/v1/morrowind/perception")
assert response.status_code == 200
data = response.json()
assert data["agent_id"] == "timmy"
assert data["location"]["cell"] == "Balmora, Guild of Mages"
assert data["health"]["current"] == 80
assert data["health"]["max"] == 100
def test_file_not_found(self, client, tmp_path):
"""Returns 404 when perception file doesn't exist."""
missing = tmp_path / "nonexistent.json"
with patch.object(api_module, "PERCEPTION_PATH", missing):
response = client.get("/api/v1/morrowind/perception")
assert response.status_code == 404
assert "not found" in response.json()["detail"].lower()
def test_invalid_json(self, client, tmp_path):
"""Returns 422 when perception file contains invalid JSON."""
bad_file = tmp_path / "bad.json"
bad_file.write_text("not json", encoding="utf-8")
with patch.object(api_module, "PERCEPTION_PATH", bad_file):
response = client.get("/api/v1/morrowind/perception")
assert response.status_code == 422
def test_schema_validation_failure(self, client, tmp_path):
"""Returns 500 when JSON doesn't match PerceptionOutput schema."""
bad_data = tmp_path / "bad_schema.json"
bad_data.write_text(json.dumps({"not": "valid"}), encoding="utf-8")
with patch.object(api_module, "PERCEPTION_PATH", bad_data):
response = client.get("/api/v1/morrowind/perception")
assert response.status_code == 500
# ---------------------------------------------------------------------------
# POST /command
# ---------------------------------------------------------------------------
class TestPostCommand:
def test_success(self, client, mock_command_logger, perception_file):
"""Command is accepted, logged, and returns a command_id."""
response = client.post(
"/api/v1/morrowind/command",
json=SAMPLE_COMMAND,
)
assert response.status_code == 200
data = response.json()
assert data["command_id"] == 42
assert data["status"] == "accepted"
assert data["bridge_forwarded"] is False
mock_command_logger.log_command.assert_called_once()
def test_invalid_command_type(self, client, mock_command_logger):
"""Rejects commands with unknown command types."""
bad_command = {**SAMPLE_COMMAND, "command": "fly_to_moon"}
response = client.post(
"/api/v1/morrowind/command",
json=bad_command,
)
assert response.status_code == 422
def test_missing_reasoning(self, client, mock_command_logger):
"""Rejects commands without a reasoning field."""
no_reasoning = {**SAMPLE_COMMAND}
del no_reasoning["reasoning"]
response = client.post(
"/api/v1/morrowind/command",
json=no_reasoning,
)
assert response.status_code == 422
def test_empty_reasoning(self, client, mock_command_logger):
"""Rejects commands with empty reasoning."""
empty_reasoning = {**SAMPLE_COMMAND, "reasoning": ""}
response = client.post(
"/api/v1/morrowind/command",
json=empty_reasoning,
)
assert response.status_code == 422
def test_log_failure(self, client, tmp_path):
"""Returns 500 when command logging fails."""
mock_logger = MagicMock()
mock_logger.log_command.side_effect = RuntimeError("DB unavailable")
missing = tmp_path / "no_perception.json"
with (
patch.object(api_module, "_command_logger", mock_logger),
patch.object(api_module, "PERCEPTION_PATH", missing),
):
response = client.post(
"/api/v1/morrowind/command",
json=SAMPLE_COMMAND,
)
assert response.status_code == 500
assert "log command" in response.json()["detail"].lower()
# ---------------------------------------------------------------------------
# GET /morrowind/status
# ---------------------------------------------------------------------------
class TestGetStatus:
def test_connected(self, client, perception_file, mock_command_logger):
"""Status reports connected when perception file exists."""
response = client.get("/api/v1/morrowind/status")
assert response.status_code == 200
data = response.json()
assert data["connected"] is True
assert data["current_cell"] == "Balmora, Guild of Mages"
assert data["command_queue_depth"] == 7
assert data["vitals"]["health"] == "80/100"
def test_disconnected(self, client, tmp_path, mock_command_logger):
"""Status reports disconnected when perception file is missing."""
missing = tmp_path / "nonexistent.json"
with patch.object(api_module, "PERCEPTION_PATH", missing):
response = client.get("/api/v1/morrowind/status")
assert response.status_code == 200
data = response.json()
assert data["connected"] is False
assert data["current_cell"] is None
assert data["last_perception_timestamp"] is None