"""Tests for builder commands.""" from unittest.mock import AsyncMock, MagicMock import pytest from mudlib.player import Player, players from mudlib.zone import Zone from mudlib.zones import get_zone, register_zone, zone_registry @pytest.fixture(autouse=True) def clear_state(): players.clear() zone_registry.clear() yield players.clear() zone_registry.clear() @pytest.fixture def zone(): terrain = [["." for _ in range(10)] for _ in range(10)] z = Zone(name="hub", width=10, height=10, terrain=terrain) register_zone("hub", z) return z @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(zone, mock_writer, mock_reader): p = Player( name="builder", x=5, y=5, writer=mock_writer, reader=mock_reader, location=zone, is_admin=True, ) zone._contents.append(p) players["builder"] = p return p # --- @goto --- @pytest.mark.asyncio async def test_goto_existing_zone(player): """@goto teleports player to named zone's spawn point.""" from mudlib.commands.build import cmd_goto target = Zone( name="forest", width=5, height=5, terrain=[["." for _ in range(5)] for _ in range(5)], spawn_x=2, spawn_y=3, ) register_zone("forest", target) await cmd_goto(player, "forest") assert player.location is target assert player.x == 2 assert player.y == 3 @pytest.mark.asyncio async def test_goto_nonexistent_zone(player, mock_writer): """@goto with unknown zone name shows error.""" from mudlib.commands.build import cmd_goto await cmd_goto(player, "nowhere") mock_writer.write.assert_called() written = mock_writer.write.call_args_list[-1][0][0] assert "not found" in written.lower() or "no zone" in written.lower() @pytest.mark.asyncio async def test_goto_no_args(player, mock_writer): """@goto without arguments shows usage.""" from mudlib.commands.build import cmd_goto await cmd_goto(player, "") mock_writer.write.assert_called() written = mock_writer.write.call_args_list[0][0][0] assert "usage" in written.lower() or "goto" in written.lower() # --- @dig --- @pytest.mark.asyncio async def test_dig_creates_zone(player): """@dig creates a new zone and teleports player there.""" from mudlib.commands.build import cmd_dig await cmd_dig(player, "cave 8 6") new_zone = get_zone("cave") assert new_zone is not None assert new_zone.width == 8 assert new_zone.height == 6 assert player.location is new_zone assert player.x == 0 assert player.y == 0 @pytest.mark.asyncio async def test_dig_creates_terrain(player): """@dig creates terrain filled with '.' tiles.""" from mudlib.commands.build import cmd_dig await cmd_dig(player, "mine 5 4") new_zone = get_zone("mine") assert new_zone is not None assert len(new_zone.terrain) == 4 assert len(new_zone.terrain[0]) == 5 assert all(tile == "." for row in new_zone.terrain for tile in row) @pytest.mark.asyncio async def test_dig_zone_not_toroidal(player): """@dig creates bounded (non-toroidal) zones.""" from mudlib.commands.build import cmd_dig await cmd_dig(player, "room 3 3") new_zone = get_zone("room") assert new_zone is not None assert new_zone.toroidal is False @pytest.mark.asyncio async def test_dig_existing_name(player, mock_writer): """@dig with existing zone name shows error.""" from mudlib.commands.build import cmd_dig await cmd_dig(player, "hub 5 5") mock_writer.write.assert_called() written = mock_writer.write.call_args_list[-1][0][0] assert "already exists" in written.lower() @pytest.mark.asyncio async def test_dig_bad_args(player, mock_writer): """@dig with wrong number of args shows usage.""" from mudlib.commands.build import cmd_dig await cmd_dig(player, "cave") mock_writer.write.assert_called() written = mock_writer.write.call_args_list[0][0][0] assert "usage" in written.lower() or "dig" in written.lower() @pytest.mark.asyncio async def test_dig_non_numeric_dimensions(player, mock_writer): """@dig with non-numeric dimensions shows error.""" from mudlib.commands.build import cmd_dig await cmd_dig(player, "cave abc def") mock_writer.write.assert_called() written = mock_writer.write.call_args_list[0][0][0] assert "numbers" in written.lower() @pytest.mark.asyncio async def test_dig_zero_or_negative_dimensions(player, mock_writer): """@dig with zero or negative dimensions shows error.""" from mudlib.commands.build import cmd_dig await cmd_dig(player, "cave 0 5") mock_writer.write.assert_called() written = mock_writer.write.call_args_list[0][0][0] assert "at least 1x1" in written.lower() mock_writer.write.reset_mock() await cmd_dig(player, "cave 5 -2") mock_writer.write.assert_called() written = mock_writer.write.call_args_list[0][0][0] assert "at least 1x1" in written.lower() # --- @save --- @pytest.mark.asyncio async def test_save_zone(player, zone, tmp_path, mock_writer): """@save writes zone to TOML file.""" from mudlib.commands.build import cmd_save, set_content_dir set_content_dir(tmp_path) zones_dir = tmp_path / "zones" zones_dir.mkdir() await cmd_save(player, "") # Check file was created saved_file = zones_dir / "hub.toml" assert saved_file.exists() content = saved_file.read_text() assert 'name = "hub"' in content # Check confirmation message mock_writer.write.assert_called() written = mock_writer.write.call_args_list[-1][0][0] assert "saved" in written.lower() # --- @place --- @pytest.mark.asyncio async def test_place_thing(player, zone, mock_writer): """@place puts a thing at player's position.""" from mudlib.commands.build import cmd_place from mudlib.things import ThingTemplate, thing_templates thing_templates["sign"] = ThingTemplate( name="sign", description="a wooden sign", ) await cmd_place(player, "sign") # Check thing is in zone at player's position from mudlib.thing import Thing things = [ obj for obj in zone.contents_at(5, 5) if isinstance(obj, Thing) and obj.name == "sign" ] assert len(things) == 1 assert things[0].location is zone # Clean up del thing_templates["sign"] @pytest.mark.asyncio async def test_place_unknown_thing(player, mock_writer): """@place with unknown thing name shows error.""" from mudlib.commands.build import cmd_place await cmd_place(player, "unicorn") mock_writer.write.assert_called() written = mock_writer.write.call_args_list[0][0][0] assert "not found" in written.lower() or "unknown" in written.lower() @pytest.mark.asyncio async def test_place_no_args(player, mock_writer): """@place without arguments shows usage.""" from mudlib.commands.build import cmd_place await cmd_place(player, "") mock_writer.write.assert_called() written = mock_writer.write.call_args_list[0][0][0] assert "usage" in written.lower() or "place" in written.lower() # --- Permission checks --- @pytest.mark.asyncio async def test_builder_commands_require_admin(zone, mock_writer, mock_reader): """Non-admin players cannot use builder commands.""" from mudlib.commands import dispatch non_admin = Player( name="player", x=5, y=5, writer=mock_writer, reader=mock_reader, location=zone, is_admin=False, ) await dispatch(non_admin, "@goto hub") mock_writer.write.assert_called() written = mock_writer.write.call_args_list[-1][0][0] assert "permission" in written.lower() mock_writer.write.reset_mock() await dispatch(non_admin, "@dig test 5 5") mock_writer.write.assert_called() written = mock_writer.write.call_args_list[-1][0][0] assert "permission" in written.lower() mock_writer.write.reset_mock() await dispatch(non_admin, "@save") mock_writer.write.assert_called() written = mock_writer.write.call_args_list[-1][0][0] assert "permission" in written.lower() mock_writer.write.reset_mock() await dispatch(non_admin, "@place thing") mock_writer.write.assert_called() written = mock_writer.write.call_args_list[-1][0][0] assert "permission" in written.lower() # --- @zones --- @pytest.mark.asyncio async def test_zones_lists_registered_zones(player): """@zones lists all registered zones.""" from mudlib.commands.build import cmd_zones # Register additional zones forest = Zone( name="forest", width=15, height=12, terrain=[["." for _ in range(15)] for _ in range(12)], ) register_zone("forest", forest) tavern = Zone( name="tavern", width=8, height=6, terrain=[["." for _ in range(8)] for _ in range(6)], ) register_zone("tavern", tavern) await cmd_zones(player, "") # Check all zones are listed all_output = "".join(call[0][0] for call in player.writer.write.call_args_list) assert "hub" in all_output assert "forest" in all_output assert "tavern" in all_output @pytest.mark.asyncio async def test_zones_shows_dimensions(player): """@zones shows width x height for each zone.""" from mudlib.commands.build import cmd_zones forest = Zone( name="forest", width=20, height=15, terrain=[["." for _ in range(20)] for _ in range(15)], ) register_zone("forest", forest) await cmd_zones(player, "") all_output = "".join(call[0][0] for call in player.writer.write.call_args_list) assert "10x10" in all_output # hub from fixture assert "20x15" in all_output # forest @pytest.mark.asyncio async def test_zones_highlights_current_zone(player): """@zones marks the player's current zone.""" from mudlib.commands.build import cmd_zones forest = Zone( name="forest", width=15, height=12, terrain=[["." for _ in range(15)] for _ in range(12)], ) register_zone("forest", forest) await cmd_zones(player, "") all_output = "".join(call[0][0] for call in player.writer.write.call_args_list) # hub should be marked as current (player is in hub via fixture) assert "[here]" in all_output # [here] should be on the same line as hub hub_line_idx = all_output.find("hub") here_idx = all_output.find("[here]") assert hub_line_idx < here_idx < hub_line_idx + 50 @pytest.mark.asyncio async def test_zones_empty_registry(zone, mock_writer, mock_reader): """@zones with no zones shows appropriate message.""" from mudlib.commands.build import cmd_zones # Create player not in a zone fixture zone_registry.clear() temp_zone = Zone( name="temp", width=5, height=5, terrain=[["." for _ in range(5)] for _ in range(5)], ) p = Player( name="builder", x=0, y=0, writer=mock_writer, reader=mock_reader, location=temp_zone, is_admin=True, ) await cmd_zones(p, "") mock_writer.write.assert_called() written = mock_writer.write.call_args_list[0][0][0] assert "no zones" in written.lower() @pytest.mark.asyncio async def test_zones_requires_admin(zone, mock_writer, mock_reader): """Non-admin players cannot use @zones.""" from mudlib.commands import dispatch non_admin = Player( name="player", x=5, y=5, writer=mock_writer, reader=mock_reader, location=zone, is_admin=False, ) await dispatch(non_admin, "@zones") mock_writer.write.assert_called() written = mock_writer.write.call_args_list[-1][0][0] assert "permission" in written.lower()