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:
1 parent
69ba824bc8
commit
b9f21c7199
14 files changed
+2658
-160
No files matched your search
@@ -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()
|
||||
@@ -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
|
||||
Reference in new issue
Block a user