From f1e4cfa4dd55fb01b99cd96cd944502356f2253d Mon Sep 17 00:00:00 2001 From: Jared Miller Date: Sun, 8 Feb 2026 13:32:32 -0500 Subject: [PATCH] Add prefix matching tests --- tests/test_prefix_matching.py | 221 ++++++++++++++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 tests/test_prefix_matching.py diff --git a/tests/test_prefix_matching.py b/tests/test_prefix_matching.py new file mode 100644 index 0000000..bcc41b4 --- /dev/null +++ b/tests/test_prefix_matching.py @@ -0,0 +1,221 @@ +"""Tests for prefix matching in command dispatch.""" + +from unittest.mock import AsyncMock, MagicMock + +import pytest + +from mudlib import commands +from mudlib.commands import CommandDefinition + + +@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, + ) + + +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(autouse=True) +def clean_registry(): + """Clear registry before each test.""" + commands._registry.clear() + yield + commands._registry.clear() + + +@pytest.mark.asyncio +async def test_exact_match_still_works(player): + """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): + """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): + """Test that ambiguous prefix with 2 matches shows 'A or B?' message.""" + commands.register( + CommandDefinition(name="sweep", handler=dummy_handler, mode="normal") + ) + commands.register( + CommandDefinition( + name="southwest", 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 ( + "sweep or southwest?" in output or "southwest or sweep?" in output + ) + assert "Handler called" not in output + + +@pytest.mark.asyncio +async def test_ambiguous_prefix_three_plus_matches(player): + """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): + """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): + """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): + """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): + """Test that aliases to the same command don't create multiple matches.""" + commands.register( + CommandDefinition( + name="look", + aliases=["l", "lo"], + 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 look?" not multiple entries for look's aliases + assert "lock" in output + assert "look" in output + # Should not list look multiple times + output_lower = output.lower() + # Count occurrences of the word "look" (not as substring) + look_count = len( + [word for word in output_lower.split() if word.startswith("look")] + ) + assert look_count == 1 + + +@pytest.mark.asyncio +async def test_prefix_match_with_args(player): + """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