diff --git a/src/mudlib/commands/quit.py b/src/mudlib/commands/quit.py index 535cfab..54699ce 100644 --- a/src/mudlib/commands/quit.py +++ b/src/mudlib/commands/quit.py @@ -2,6 +2,7 @@ from mudlib.commands import CommandDefinition, register from mudlib.player import Player, players +from mudlib.store import save_player async def cmd_quit(player: Player, args: str) -> None: @@ -11,6 +12,9 @@ async def cmd_quit(player: Player, args: str) -> None: player: The player executing the command args: Command arguments (unused) """ + # Save player state before disconnecting + save_player(player) + player.writer.write("Goodbye!\r\n") await player.writer.drain() player.writer.close() diff --git a/tests/test_persistence.py b/tests/test_persistence.py new file mode 100644 index 0000000..c8d671d --- /dev/null +++ b/tests/test_persistence.py @@ -0,0 +1,153 @@ +"""Tests for persistence behavior (save on quit/disconnect).""" + +import os +import tempfile +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from mudlib.commands.quit import cmd_quit +from mudlib.player import Player, players +from mudlib.store import create_account, init_db, load_player_data, save_player + + +@pytest.fixture +def temp_db(): + """Create a temporary database for testing.""" + with tempfile.NamedTemporaryFile(delete=False, suffix=".db") as f: + db_path = f.name + + init_db(db_path) + + yield db_path + + # Cleanup + os.unlink(db_path) + + +@pytest.fixture +def mock_writer(): + writer = MagicMock() + writer.write = MagicMock() + writer.drain = AsyncMock() + writer.close = MagicMock() + writer.is_closing = MagicMock(return_value=False) + return writer + + +@pytest.fixture +def mock_reader(): + return MagicMock() + + +@pytest.mark.asyncio +async def test_quit_saves_player_state(temp_db, mock_reader, mock_writer): + """Quit command saves player state before disconnecting.""" + # Create an account + create_account("TestPlayer", "password") + + # Create player with modified state + player = Player( + name="TestPlayer", + x=42, + y=17, + pl=85.0, + stamina=60.0, + max_stamina=120.0, + flying=True, + reader=mock_reader, + writer=mock_writer, + ) + + # Add to player registry + players["TestPlayer"] = player + + # Execute quit command + await cmd_quit(player, "") + + # Verify player was saved + data = load_player_data("TestPlayer") + assert data is not None + assert data["x"] == 42 + assert data["y"] == 17 + assert data["pl"] == 85.0 + assert data["stamina"] == 60.0 + assert data["max_stamina"] == 120.0 + assert data["flying"] is True + + # Verify player was removed from registry + assert "TestPlayer" not in players + + +@pytest.mark.asyncio +async def test_save_multiple_times(temp_db, mock_reader, mock_writer): + """Player state can be saved multiple times (updates).""" + create_account("TestPlayer", "password") + + player = Player( + name="TestPlayer", + x=10, + y=20, + reader=mock_reader, + writer=mock_writer, + ) + + # Save initial state + players["TestPlayer"] = player + await cmd_quit(player, "") + + # Verify initial save + data = load_player_data("TestPlayer") + assert data is not None + assert data["x"] == 10 + assert data["y"] == 20 + + # Create new player instance with different position + player2 = Player( + name="TestPlayer", + x=50, + y=60, + reader=mock_reader, + writer=mock_writer, + ) + + # Save again + players["TestPlayer"] = player2 + await cmd_quit(player2, "") + + # Verify updated state + data = load_player_data("TestPlayer") + assert data is not None + assert data["x"] == 50 + assert data["y"] == 60 + + +@pytest.mark.asyncio +async def test_autosave_persists_player_changes(temp_db, mock_reader, mock_writer): + """Auto-save persists player state changes during gameplay.""" + create_account("TestPlayer", "password") + + # Create player + player = Player( + name="TestPlayer", + x=10, + y=20, + pl=100.0, + reader=mock_reader, + writer=mock_writer, + ) + + # Simulate player moving and taking damage + player.x = 25 + player.y = 35 + player.pl = 75.0 + + # Manually trigger save (simulating auto-save) + save_player(player) + + # Verify state was saved + data = load_player_data("TestPlayer") + assert data is not None + assert data["x"] == 25 + assert data["y"] == 35 + assert data["pl"] == 75.0