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 a83ea9bdb6 feat: FastAPI Morrowind harness + SOUL.md framework (#821, #854)
## FastAPI Morrowind Harness (#821)
- GET /api/v1/morrowind/perception — reads perception.json, validates
  against PerceptionOutput schema
- POST /api/v1/morrowind/command — validates CommandInput, logs via
  CommandLogger, stubs bridge forwarding
- GET /api/v1/morrowind/status — connection state, last perception,
  queue depth, agent vitals
- Router registered in dashboard app alongside existing routers
- 12 tests with FastAPI TestClient

## SOUL.md Framework (#854)
- docs/soul-framework/ — template, authoring guide, role extensions
- src/infrastructure/soul/loader.py — parse SOUL.md into structured
  SoulDocument objects with section extraction and merge support
- src/infrastructure/soul/validator.py — validate structure, detect
  contradictions between values/constraints
- src/infrastructure/soul/versioning.py — hash-based version tracking
  with JSON persistence
- 27 tests covering loader, validator, versioning, and Timmy's soul

Builds on PR #864 (protocol spec + command log).
Closes #821
Closes #854
2026-03-21 22:43:21 +00:00

228 lines
8.1 KiB
Python

"""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