"""Tests for player alias system.""" import os import tempfile from pathlib import Path from unittest.mock import MagicMock import pytest from mudlib.commands import CommandDefinition, dispatch, register from mudlib.player import Player from mudlib.store import ( create_account, init_db, load_aliases, save_aliases, save_player, ) # Persistence tests def test_save_and_load_aliases_roundtrip(): """Save and load aliases from database.""" with tempfile.TemporaryDirectory() as tmpdir: db_path = Path(tmpdir) / "test.db" init_db(db_path) aliases = {"pr": "punch right", "pl": "punch left", "l": "look"} save_aliases("goku", aliases, db_path) loaded = load_aliases("goku", db_path) assert loaded == aliases def test_load_aliases_empty(): """Loading aliases for player with none returns empty dict.""" with tempfile.TemporaryDirectory() as tmpdir: db_path = Path(tmpdir) / "test.db" init_db(db_path) loaded = load_aliases("goku", db_path) assert loaded == {} def test_save_aliases_overwrites_existing(): """Saving new aliases replaces old ones.""" with tempfile.TemporaryDirectory() as tmpdir: db_path = Path(tmpdir) / "test.db" init_db(db_path) save_aliases("goku", {"pr": "punch right"}, db_path) save_aliases("goku", {"pl": "punch left", "l": "look"}, db_path) loaded = load_aliases("goku", db_path) assert loaded == {"pl": "punch left", "l": "look"} assert "pr" not in loaded # Alias command tests @pytest.mark.asyncio async def test_alias_list_empty(player): """alias with no args shows message when no aliases defined.""" from mudlib.commands.alias import cmd_alias await cmd_alias(player, "") player.writer.write.assert_called_with("No aliases defined.\r\n") @pytest.mark.asyncio async def test_alias_create(player): """alias creates an alias.""" from mudlib.commands.alias import cmd_alias await cmd_alias(player, "pr punch right") assert player.aliases["pr"] == "punch right" player.writer.write.assert_called_with("Alias set: pr -> punch right\r\n") @pytest.mark.asyncio async def test_alias_list_with_aliases(player): """alias with no args lists all aliases.""" from mudlib.commands.alias import cmd_alias player.aliases = {"pr": "punch right", "pl": "punch left", "l": "look"} await cmd_alias(player, "") output = player.writer.write.call_args[0][0] assert "pr -> punch right" in output assert "pl -> punch left" in output assert "l -> look" in output @pytest.mark.asyncio async def test_alias_show_single(player): """alias shows that specific alias.""" from mudlib.commands.alias import cmd_alias player.aliases = {"pr": "punch right", "pl": "punch left"} await cmd_alias(player, "pr") player.writer.write.assert_called_with("pr -> punch right\r\n") @pytest.mark.asyncio async def test_unalias_removes_alias(player): """unalias removes an alias.""" from mudlib.commands.alias import cmd_unalias player.aliases = {"pr": "punch right"} await cmd_unalias(player, "pr") assert "pr" not in player.aliases player.writer.write.assert_called_with("Alias removed: pr\r\n") @pytest.mark.asyncio async def test_unalias_no_such_alias(player): """unalias on non-existent alias shows error.""" from mudlib.commands.alias import cmd_unalias await cmd_unalias(player, "pr") player.writer.write.assert_called_with("No such alias: pr\r\n") @pytest.mark.asyncio async def test_alias_cannot_override_builtin(player): """Cannot alias over existing built-in commands.""" import mudlib.commands.look # noqa: F401 - needed to register look command from mudlib.commands.alias import cmd_alias await cmd_alias(player, "look punch right") player.writer.write.assert_called_with( "Cannot alias over built-in command: look\r\n" ) assert "look" not in player.aliases # Dispatch integration tests @pytest.mark.asyncio async def test_alias_expands_in_dispatch(player): """Aliases are expanded before command dispatch.""" called_with = [] async def test_handler(p, args): called_with.append(args) register(CommandDefinition("testcmd", test_handler)) player.aliases["tc"] = "testcmd" await dispatch(player, "tc hello") assert called_with == ["hello"] @pytest.mark.asyncio async def test_alias_with_extra_args(player): """Alias expansion preserves additional arguments.""" called_with = [] async def test_handler(p, args): called_with.append(args) register(CommandDefinition("testcmd", test_handler)) player.aliases["tc"] = "testcmd arg1" await dispatch(player, "tc arg2 arg3") # Expansion: "testcmd arg1" + " arg2 arg3" = "testcmd arg1 arg2 arg3" assert called_with == ["arg1 arg2 arg3"] @pytest.mark.asyncio async def test_nested_alias_max_depth(player): """Nested aliases are limited to prevent infinite recursion.""" called = [] async def test_handler(p, args): called.append(True) register(CommandDefinition("final", test_handler)) # Create a chain: a -> b -> c -> ... -> final player.aliases["a"] = "b" player.aliases["b"] = "c" player.aliases["c"] = "d" player.aliases["d"] = "e" player.aliases["e"] = "f" player.aliases["f"] = "g" player.aliases["g"] = "h" player.aliases["h"] = "i" player.aliases["i"] = "j" player.aliases["j"] = "final" await dispatch(player, "a") assert len(called) == 1 # Should complete successfully # Now create an 11-deep chain that exceeds limit player.aliases["j"] = "k" player.aliases["k"] = "final" called.clear() await dispatch(player, "a") # Should fail and send error message assert len(called) == 0 assert "Too many nested aliases" in player.writer.write.call_args[0][0] @pytest.mark.asyncio async def test_unknown_alias_falls_through(player): """Unknown aliases fall through to normal command resolution.""" player.aliases = {} # No aliases defined # This should hit normal "Unknown command" path await dispatch(player, "nonexistent") assert "Unknown command" in player.writer.write.call_args[0][0] # Integration test for persistence def test_aliases_persist_on_save_player(): """Aliases are saved when save_player is called.""" with tempfile.NamedTemporaryFile(delete=False, suffix=".db") as f: db_path = f.name try: init_db(db_path) create_account("Goku", "password") # Create player with aliases mock_writer = MagicMock() player = Player( name="Goku", x=10, y=20, reader=MagicMock(), writer=mock_writer, ) player.aliases = {"pr": "punch right", "pl": "punch left", "l": "look"} # Save player (should save aliases too) save_player(player) # Load aliases directly from database loaded = load_aliases("Goku") assert loaded == {"pr": "punch right", "pl": "punch left", "l": "look"} finally: os.unlink(db_path)