docs: complete documentation and add web monitoring dashboard

- Add comprehensive configuration guide (docs/configuration.md)
- Add development guide with setup instructions (docs/development.md)
- Add complete API reference documentation (docs/api_reference.md)
- Update README.md with new features and project structure
- Update TODO.md to reflect current implementation status
- Add web monitoring dashboard with FastAPI and WebSocket support
- Add tests for sync integration and web monitoring
- Update config example with monitoring settings

The MUD AI Player is now feature-complete with full documentation.
This commit is contained in:
truetype committed 2026-02-09 10:44:25 +01:00
1 parent 69ba824bc8
commit b9f21c7199
14 files changed
+2658 -160

No files matched your search

+178
View File
@@ -0,0 +1,178 @@
"""Integration tests for the hybrid storage system (SyncManager + WorldMapper)"""
import os
import tempfile
import pytest
from src.modules.mapper import WorldMapper
class TestSyncManagerIntegration:
"""Test that WorldMapper correctly triggers snapshots via SyncManager."""
def test_sync_disabled(self):
"""Test mapper works without sync enabled."""
with tempfile.TemporaryDirectory() as tmpdir:
db_path = os.path.join(tmpdir, "world.db")
mapper = WorldMapper(
db_path=db_path,
enable_sync=False,
)
# Should work normally without sync
room_id = mapper.on_room_enter(
title="Test Room",
description="A test room",
exits=["north", "south"],
)
assert room_id == 1
assert mapper.sync_stats["enabled"] is False
mapper.close()
def test_sync_enabled_no_trigger(self):
"""Test sync enabled but trigger threshold not reached."""
with tempfile.TemporaryDirectory() as tmpdir:
db_path = os.path.join(tmpdir, "world.db")
archive_dir = os.path.join(tmpdir, "archive")
mapper = WorldMapper(
db_path=db_path,
archive_dir=archive_dir,
enable_sync=True,
room_count_trigger=100, # High threshold
)
# Add a few rooms
for i in range(5):
mapper.on_room_enter(
title=f"Room {i}",
description=f"Description {i}",
exits=["north"],
)
# Sync stats should show no sync yet
stats = mapper.sync_stats
assert stats["enabled"] is True
assert stats["snapshot_version"] == 0 # No snapshot yet
assert stats["rooms_since_last_sync"] == 5
mapper.close()
def test_sync_trigger_on_room_count(self):
"""Test snapshot triggers when room threshold is reached."""
with tempfile.TemporaryDirectory() as tmpdir:
db_path = os.path.join(tmpdir, "world.db")
archive_dir = os.path.join(tmpdir, "archive")
os.makedirs(archive_dir, exist_ok=True)
mapper = WorldMapper(
db_path=db_path,
archive_dir=archive_dir,
enable_sync=True,
room_count_trigger=3, # Low threshold for testing
)
# Add rooms until trigger
for i in range(5):
mapper.on_room_enter(
title=f"Room {i}",
description=f"Description {i}",
exits=["north", "south"],
)
# Should have triggered at least once
stats = mapper.sync_stats
assert stats["snapshot_version"] >= 1
# Check archive was created
mapper.close()
# Verify snapshot files exist
archive_files = os.listdir(archive_dir)
json_files = [f for f in archive_files if f.endswith('.json')]
assert len(json_files) >= 1
def test_session_end_snapshot(self):
"""Test snapshot is created on session end."""
with tempfile.TemporaryDirectory() as tmpdir:
db_path = os.path.join(tmpdir, "world.db")
archive_dir = os.path.join(tmpdir, "archive")
os.makedirs(archive_dir, exist_ok=True)
mapper = WorldMapper(
db_path=db_path,
archive_dir=archive_dir,
enable_sync=True,
room_count_trigger=1000, # Won't trigger during test
)
# Add some rooms
for i in range(3):
mapper.on_room_enter(
title=f"Room {i}",
description=f"Description {i}",
exits=["north"],
)
# Close should trigger final snapshot
mapper.close()
# Verify snapshot was created
archive_files = os.listdir(archive_dir)
json_files = [f for f in archive_files if f.endswith('.json')]
assert len(json_files) >= 1
def test_semantic_search_available(self):
"""Test that semantic search is available when sync is enabled."""
with tempfile.TemporaryDirectory() as tmpdir:
db_path = os.path.join(tmpdir, "world.db")
archive_dir = os.path.join(tmpdir, "archive")
mapper = WorldMapper(
db_path=db_path,
archive_dir=archive_dir,
enable_sync=True,
)
# Add some rooms
mapper.on_room_enter(
title="Dark Forest",
description="A dark forest with goblins lurking",
exits=["north"],
npcs=["goblin"],
)
mapper.on_room_enter(
title="Bright Village",
description="A peaceful village with shops",
exits=["south"],
)
# Force a sync to build the search index
mapper.close()
# Note: Semantic search requires the sync to have built an index
# The keyword fallback should work without FAISS
def test_sync_stats_reporting(self):
"""Test that sync stats are correctly reported."""
with tempfile.TemporaryDirectory() as tmpdir:
db_path = os.path.join(tmpdir, "world.db")
archive_dir = os.path.join(tmpdir, "archive")
mapper = WorldMapper(
db_path=db_path,
archive_dir=archive_dir,
enable_sync=True,
room_count_trigger=10,
)
stats = mapper.sync_stats
assert "enabled" in stats
assert "snapshot_version" in stats
assert "rooms_since_last_sync" in stats
assert "trigger_threshold" in stats
assert stats["enabled"] is True
assert stats["trigger_threshold"] == 10
mapper.close()
+205
View File
@@ -0,0 +1,205 @@
"""Tests for the web monitoring server"""
import pytest
import asyncio
from unittest.mock import Mock, MagicMock
from src.modules.web.server import MonitoringServer
class TestMonitoringServer:
"""Test the monitoring server functionality."""
@pytest.fixture
def mock_game_state(self):
"""Create a mock game state."""
gs = Mock()
gs.hp = 80
gs.max_hp = 100
gs.mp = 50
gs.max_mp = 100
gs.sp = 100
gs.max_sp = 100
gs.level = 5
gs.current_room = Mock()
gs.current_room.title = "Test Room"
gs.current_room.description = "A test room"
gs.current_room.exits = {"north": 0, "south": 0}
gs.current_room.npcs = ["goblin"]
gs.current_room.items = ["sword"]
gs.in_combat = False
return gs
@pytest.fixture
def mock_world_mapper(self):
"""Create a mock world mapper."""
wm = Mock()
wm.total_rooms = 42
wm.total_exits = 85
wm.sync_stats = {
"enabled": True,
"snapshot_version": 3,
"rooms_since_last_sync": 10,
"trigger_threshold": 500,
}
return wm
@pytest.fixture
def mock_decision_engine(self):
"""Create a mock decision engine."""
de = Mock()
de.current_state = "EXPLORING"
de.goal_manager = Mock()
# Mock goals
goal1 = Mock()
goal1.name = "Explore"
goal1.description = "Explore the area"
goal1.priority.name = "HIGH"
goal1.progress = 0.5
goal2 = Mock()
goal2.name = "Kill Goblin"
goal2.description = "Defeat the goblin"
goal2.priority.name = "MEDIUM"
goal2.progress = 0.0
de.goal_manager.get_active_goals.return_value = [goal1, goal2]
return de
def test_server_creation(self, mock_game_state, mock_world_mapper, mock_decision_engine):
"""Test that the server can be created."""
server = MonitoringServer(
host="127.0.0.1",
port=8888,
game_state=mock_game_state,
world_mapper=mock_world_mapper,
decision_engine=mock_decision_engine,
)
assert server.host == "127.0.0.1"
assert server.port == 8888
assert server.game_state == mock_game_state
assert server.world_mapper == mock_world_mapper
assert server.decision_engine == mock_decision_engine
def test_get_status_data(self, mock_game_state, mock_world_mapper, mock_decision_engine):
"""Test getting status data."""
server = MonitoringServer(
game_state=mock_game_state,
world_mapper=mock_world_mapper,
decision_engine=mock_decision_engine,
)
data = server._get_status_data()
assert "vitals" in data
assert data["vitals"]["hp"] == 80
assert data["vitals"]["max_hp"] == 100
assert data["vitals"]["level"] == 5
assert "room" in data
assert data["room"]["title"] == "Test Room"
assert "north" in data["room"]["exits"]
assert data["ai_state"] == "EXPLORING"
assert data["in_combat"] == False
def test_get_map_data(self, mock_game_state, mock_world_mapper, mock_decision_engine):
"""Test getting map data."""
server = MonitoringServer(
game_state=mock_game_state,
world_mapper=mock_world_mapper,
decision_engine=mock_decision_engine,
)
data = server._get_map_data()
assert data["map_stats"]["rooms"] == 42
assert data["map_stats"]["exits"] == 85
assert data["sync_stats"]["enabled"] == True
assert data["sync_stats"]["snapshot_version"] == 3
def test_get_goals_data(self, mock_game_state, mock_world_mapper, mock_decision_engine):
"""Test getting goals data."""
server = MonitoringServer(
game_state=mock_game_state,
world_mapper=mock_world_mapper,
decision_engine=mock_decision_engine,
)
data = server._get_goals_data()
assert len(data["goals"]) == 2
assert data["goals"][0]["name"] == "Explore"
assert data["goals"][0]["priority"] == "HIGH"
assert data["goals"][1]["name"] == "Kill Goblin"
def test_event_logging(self, mock_game_state, mock_world_mapper, mock_decision_engine):
"""Test event logging."""
server = MonitoringServer(
game_state=mock_game_state,
world_mapper=mock_world_mapper,
decision_engine=mock_decision_engine,
)
# Log some events
server.log_event("ROOM", "Entered Dark Forest")
server.log_event("COMBAT", "Attacked goblin", {"damage": 15})
assert len(server._event_log) == 2
assert server._event_log[0]["type"] == "ROOM"
assert server._event_log[1]["type"] == "COMBAT"
assert "time" in server._event_log[0]
def test_event_log_limit(self, mock_game_state, mock_world_mapper, mock_decision_engine):
"""Test that event log respects max size."""
server = MonitoringServer(
game_state=mock_game_state,
world_mapper=mock_world_mapper,
decision_engine=mock_decision_engine,
)
server._max_log_entries = 5
# Log more events than max
for i in range(10):
server.log_event("TEST", f"Event {i}")
# Should only keep last 5
assert len(server._event_log) == 5
assert server._event_log[-1]["message"] == "Event 9"
def test_dashboard_html(self, mock_game_state, mock_world_mapper, mock_decision_engine):
"""Test that dashboard HTML is generated."""
server = MonitoringServer(
game_state=mock_game_state,
world_mapper=mock_world_mapper,
decision_engine=mock_decision_engine,
)
html = server._get_dashboard_html()
assert "<!DOCTYPE html>" in html
assert "MUD AI Player Monitor" in html
assert "Character Vitals" in html
assert "Event Log" in html
assert "WebSocket" in html
def test_app_creation(self, mock_game_state, mock_world_mapper, mock_decision_engine):
"""Test that FastAPI app is created correctly."""
server = MonitoringServer(
game_state=mock_game_state,
world_mapper=mock_world_mapper,
decision_engine=mock_decision_engine,
)
app = server.app
# Check routes exist
routes = [r.path for r in app.routes]
assert "/" in routes
assert "/api/status" in routes
assert "/api/map" in routes
assert "/api/goals" in routes
assert "/api/stats" in routes
assert "/api/events" in routes
assert "/ws" in routes