"""Tests for prefix matching in command dispatch.""" import pytest from mudlib import commands from mudlib.commands import CommandDefinition @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, ) async def dummy_handler(player, args): """A simple handler for testing.""" player.writer.write(f"Handler called with args: {args}\r\n") await player.writer.drain() @pytest.fixture def clean_registry(): """Clear registry before each test.""" original_registry = commands._registry.copy() commands._registry.clear() yield commands._registry.clear() commands._registry.update(original_registry) @pytest.mark.asyncio async def test_exact_match_still_works(player, clean_registry): """Test that exact matches work as before.""" commands.register( CommandDefinition(name="look", handler=dummy_handler, mode="normal") ) await commands.dispatch(player, "look") output = "".join([call[0][0] for call in player.writer.write.call_args_list]) assert "Handler called" in output assert "Unknown command" not in output @pytest.mark.asyncio async def test_prefix_match_resolves_unique_prefix(player, clean_registry): """Test that a unique prefix resolves to the full command.""" commands.register( CommandDefinition(name="sweep", handler=dummy_handler, mode="normal") ) await commands.dispatch(player, "swe") output = "".join([call[0][0] for call in player.writer.write.call_args_list]) assert "Handler called" in output assert "Unknown command" not in output @pytest.mark.asyncio async def test_ambiguous_prefix_two_matches(player, clean_registry): """Test that ambiguous prefix with 2 matches shows 'A or B?' message.""" commands.register( CommandDefinition(name="swoop", handler=dummy_handler, mode="normal") ) commands.register( CommandDefinition(name="swallow", handler=dummy_handler, mode="normal") ) await commands.dispatch(player, "sw") output = "".join([call[0][0] for call in player.writer.write.call_args_list]) # Check for either order assert "swallow or swoop?" in output or "swoop or swallow?" in output assert "Handler called" not in output @pytest.mark.asyncio async def test_ambiguous_prefix_three_plus_matches(player, clean_registry): """Test that ambiguous prefix with 3+ matches shows comma-separated.""" commands.register( CommandDefinition(name="send", handler=dummy_handler, mode="normal") ) commands.register( CommandDefinition(name="set", handler=dummy_handler, mode="normal") ) commands.register( CommandDefinition(name="settings", handler=dummy_handler, mode="normal") ) await commands.dispatch(player, "se") output = "".join([call[0][0] for call in player.writer.write.call_args_list]) # Should contain all three with commas and "or" assert "send" in output assert "set" in output assert "settings" in output assert ", or " in output assert "Handler called" not in output @pytest.mark.asyncio async def test_no_match_shows_unknown_command(player, clean_registry): """Test that no match shows unknown command message.""" commands.register( CommandDefinition(name="look", handler=dummy_handler, mode="normal") ) await commands.dispatch(player, "xyz") output = "".join([call[0][0] for call in player.writer.write.call_args_list]) assert "Unknown command: xyz" in output assert "Handler called" not in output @pytest.mark.asyncio async def test_exact_alias_match_wins_over_prefix(player, clean_registry): """Test that exact match on alias takes priority over prefix match.""" commands.register( CommandDefinition( name="southwest", aliases=["sw"], handler=dummy_handler, mode="normal", ) ) commands.register( CommandDefinition(name="sweep", handler=dummy_handler, mode="normal") ) await commands.dispatch(player, "sw") output = "".join([call[0][0] for call in player.writer.write.call_args_list]) # Should call handler (exact match on alias), not show ambiguous assert "Handler called" in output assert "or" not in output @pytest.mark.asyncio async def test_single_char_ambiguous_prefix(player, clean_registry): """Test that single-char ambiguous prefix shows disambiguation.""" commands.register( CommandDefinition(name="quit", handler=dummy_handler, mode="normal") ) commands.register( CommandDefinition(name="query", handler=dummy_handler, mode="normal") ) await commands.dispatch(player, "q") output = "".join([call[0][0] for call in player.writer.write.call_args_list]) assert "quit or query?" in output or "query or quit?" in output assert "Handler called" not in output @pytest.mark.asyncio async def test_prefix_match_deduplicates_aliases(player, clean_registry): """Test that aliases to the same command don't create multiple matches.""" commands.register( CommandDefinition( name="long", aliases=["l", "lon"], handler=dummy_handler, mode="normal", ) ) commands.register( CommandDefinition(name="lock", handler=dummy_handler, mode="normal") ) await commands.dispatch(player, "lo") output = "".join([call[0][0] for call in player.writer.write.call_args_list]) # Should show just "lock or long?" not multiple entries for long's aliases assert "lock" in output assert "long" in output # Should not list long multiple times output_lower = output.lower() # Count occurrences of the word "long" (not as substring like "lock") # Looking for "long" as a standalone word in disambiguation assert output_lower.count("long") == 1 @pytest.mark.asyncio async def test_prefix_match_with_args(player, clean_registry): """Test that prefix matching preserves arguments.""" commands.register( CommandDefinition(name="sweep", handler=dummy_handler, mode="normal") ) await commands.dispatch(player, "swe the floor") output = "".join([call[0][0] for call in player.writer.write.call_args_list]) assert "Handler called with args: the floor" in output @pytest.mark.asyncio async def test_alias_exact_match_over_prefix_collision(player, clean_registry): """Test alias exact match wins over prefix when otherwise ambiguous.""" commands.register( CommandDefinition( name="look", aliases=["l"], handler=dummy_handler, mode="normal" ) ) commands.register( CommandDefinition(name="list", handler=dummy_handler, mode="normal") ) await commands.dispatch(player, "l") output = "".join([call[0][0] for call in player.writer.write.call_args_list]) # Should call handler (exact match on alias), not show ambiguous assert "Handler called" in output assert "or" not in output