"""Tests for the commands listing command.""" from pathlib import Path from unittest.mock import AsyncMock, MagicMock import pytest from mudlib import commands # Import command modules to register their commands from mudlib.commands import ( edit, # noqa: F401 fly, # noqa: F401 help, # noqa: F401 look, # noqa: F401 movement, # noqa: F401 quit, # noqa: F401 ) @pytest.fixture def mock_writer(): writer = MagicMock() writer.write = MagicMock() writer.drain = AsyncMock() return writer @pytest.fixture def mock_reader(): return MagicMock() @pytest.fixture def player(mock_reader, mock_writer): from mudlib.player import Player return Player( name="TestPlayer", x=5, y=5, reader=mock_reader, writer=mock_writer, ) @pytest.fixture def combat_moves(): """Load and register combat moves from content directory.""" from mudlib.combat.commands import register_combat_commands combat_dir = Path(__file__).resolve().parents[1] / "content" / "combat" register_combat_commands(combat_dir) from mudlib.combat import commands as combat_cmds yield combat_cmds.combat_moves # Clean up combat_cmds.combat_moves = {} # Note: commands stay registered in _registry, but that's ok for tests @pytest.mark.asyncio async def test_commands_command_exists(): """Test that commands command is registered.""" assert "commands" in commands._registry assert "cmds" in commands._registry assert commands._registry["commands"] is commands._registry["cmds"] @pytest.mark.asyncio async def test_commands_has_wildcard_mode(): """Test that commands works from any mode.""" assert commands._registry["commands"].mode == "*" @pytest.mark.asyncio async def test_commands_lists_all_commands(player): """Test that commands command lists all unique commands.""" # Execute the command await commands.dispatch(player, "commands") # Collect all output output = "".join([call[0][0] for call in player.writer.write.call_args_list]) # Should contain at least movement and other groups assert "Movement:" in output assert "Other:" in output # Should show some specific commands with their primary names assert "north" in output assert "look" in output assert "quit" in output assert "commands" in output @pytest.mark.asyncio async def test_commands_shows_aliases(player): """Test that aliases are shown in parens after command names.""" await commands.dispatch(player, "commands") output = "".join([call[0][0] for call in player.writer.write.call_args_list]) # Check that some aliases are shown in parentheses assert "north(n)" in output or "north (n)" in output assert "look(l)" in output or "look (l)" in output assert "quit(q)" in output or "quit (q)" in output assert "commands(cmds)" in output or "commands (cmds)" in output @pytest.mark.asyncio async def test_commands_deduplicates_entries(player): """Test that aliases don't create duplicate command entries.""" await commands.dispatch(player, "commands") output = "".join([call[0][0] for call in player.writer.write.call_args_list]) # Count how many times "north" appears as a primary command # It should appear once in the movement group, not multiple times # (Note: it might appear in parens as part of another command, but # we're checking it doesn't appear as a standalone entry multiple times) lines_with_north_command = [ line for line in output.split("\n") if line.strip().startswith("north") ] # Should only have one line starting with "north" as a command assert len(lines_with_north_command) <= 1 @pytest.mark.asyncio async def test_commands_groups_correctly(player): """Test that commands are grouped by type.""" await commands.dispatch(player, "commands") output = "".join([call[0][0] for call in player.writer.write.call_args_list]) # Find the groups movement_pos = output.find("Movement:") other_pos = output.find("Other:") # At minimum, movement and other should exist assert movement_pos != -1 assert other_pos != -1 # Check that movement commands appear in the movement section # (before the "Other:" section) movement_section = output[movement_pos:other_pos] assert "north" in movement_section assert "east" in movement_section # Check that non-movement commands appear in other section other_section = output[other_pos:] assert "quit" in other_section assert "look" in other_section @pytest.mark.asyncio async def test_commands_via_alias(player): """Test that the cmds alias works.""" await commands.dispatch(player, "cmds") output = "".join([call[0][0] for call in player.writer.write.call_args_list]) # Should produce the same output structure assert "Movement:" in output assert "Other:" in output @pytest.mark.asyncio async def test_commands_detail_regular_command(player): """Test commands detail view for a regular command.""" await commands.dispatch(player, "commands look") output = "".join([call[0][0] for call in player.writer.write.call_args_list]) assert "look" in output assert "aliases: l" in output assert "mode: normal" in output @pytest.mark.asyncio async def test_commands_detail_simple_combat_move(player, combat_moves): """Test commands detail view for a simple combat move.""" await commands.dispatch(player, "commands roundhouse") output = "".join([call[0][0] for call in player.writer.write.call_args_list]) assert "roundhouse" in output assert "aliases: rh" in output assert "type: attack" in output assert "stamina: 8.0" in output assert "timing window: 2000ms" in output assert "damage: 25%" in output assert "{attacker} spins into a roundhouse kick!" in output assert "countered by: duck, parry high, parry low" in output @pytest.mark.asyncio async def test_commands_detail_variant_base(player, combat_moves): """Test commands detail view for a variant base command.""" await commands.dispatch(player, "commands punch") output = "".join([call[0][0] for call in player.writer.write.call_args_list]) assert "punch" in output assert "type: attack" in output # Should show both variants assert "punch left" in output assert "aliases: pl" in output assert "{attacker} winds up a left hook!" in output assert "countered by: dodge right, parry high" in output assert "punch right" in output assert "aliases: pr" in output assert "{attacker} winds up a right hook!" in output assert "countered by: dodge left, parry high" in output # Should show shared properties in each variant assert "stamina: 5.0" in output assert "timing window: 1800ms" in output assert "damage: 15%" in output @pytest.mark.asyncio async def test_commands_detail_specific_variant(player, combat_moves): """Test commands detail view for a specific variant.""" await commands.dispatch(player, "commands punch left") output = "".join([call[0][0] for call in player.writer.write.call_args_list]) assert "punch left" in output assert "aliases: pl" in output assert "type: attack" in output assert "stamina: 5.0" in output assert "timing window: 1800ms" in output assert "damage: 15%" in output assert "{attacker} winds up a left hook!" in output assert "countered by: dodge right, parry high" in output # Should NOT show "punch right" assert "punch right" not in output @pytest.mark.asyncio async def test_commands_detail_unknown_command(player): """Test commands detail view for an unknown command.""" await commands.dispatch(player, "commands nonexistent") output = "".join([call[0][0] for call in player.writer.write.call_args_list]) assert "Unknown command: nonexistent" in output @pytest.mark.asyncio async def test_commands_detail_via_alias(player, combat_moves): """Test commands detail view via an alias.""" await commands.dispatch(player, "commands rh") output = "".join([call[0][0] for call in player.writer.write.call_args_list]) # Should show the full command details, not the alias assert "roundhouse" in output assert "aliases: rh" in output assert "type: attack" in output