"""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()