"""Tests for TOML help topic loading.""" import textwrap from unittest.mock import AsyncMock, MagicMock import pytest from mudlib import commands from mudlib.commands import help as help_mod # noqa: F401 from mudlib.commands import helpadmin # noqa: F401 from mudlib.commands.help import _help_topics from mudlib.content import HelpTopic, load_help_topics @pytest.fixture def help_dir(tmp_path): """Create a temp directory with sample help TOML files.""" topic = tmp_path / "combat.toml" topic.write_text( textwrap.dedent("""\ name = "combat" title = "combat primer" body = \"\"\" combat is initiated when you attack another entity. use skills to learn your available moves. \"\"\" """) ) admin_topic = tmp_path / "building.toml" admin_topic.write_text( textwrap.dedent("""\ name = "building" title = "builder's guide" admin = true body = \"\"\" use @dig to create zones and @paint to edit terrain. \"\"\" """) ) return tmp_path def test_load_help_topics(help_dir): topics = load_help_topics(help_dir) assert "combat" in topics assert "building" in topics def test_help_topic_fields(help_dir): topics = load_help_topics(help_dir) combat = topics["combat"] assert combat.name == "combat" assert combat.title == "combat primer" assert combat.admin is False assert "combat is initiated" in combat.body def test_help_topic_admin_flag(help_dir): topics = load_help_topics(help_dir) building = topics["building"] assert building.admin is True def test_help_topic_title_defaults_to_name(tmp_path): topic = tmp_path / "simple.toml" topic.write_text('name = "simple"\nbody = "just a test"\n') topics = load_help_topics(tmp_path) assert topics["simple"].title == "simple" def test_load_help_topics_empty_dir(tmp_path): topics = load_help_topics(tmp_path) assert topics == {} def test_load_help_topics_skips_bad_files(tmp_path): bad = tmp_path / "broken.toml" bad.write_text("not valid toml [[[") good = tmp_path / "good.toml" good.write_text('name = "good"\nbody = "works"\n') topics = load_help_topics(tmp_path) assert "good" in topics assert "broken" not in topics @pytest.fixture def mock_writer(): writer = MagicMock() writer.write = MagicMock() writer.drain = AsyncMock() return writer @pytest.fixture def player(mock_writer): from mudlib.player import Player return Player(name="Tester", x=0, y=0, reader=MagicMock(), writer=mock_writer) @pytest.fixture def admin_player(mock_writer): from mudlib.player import Player p = Player(name="Admin", x=0, y=0, reader=MagicMock(), writer=mock_writer) p.is_admin = True return p @pytest.fixture(autouse=True) def _clear_topics(): _help_topics.clear() yield _help_topics.clear() @pytest.mark.asyncio async def test_help_shows_toml_topic(player): _help_topics["combat"] = HelpTopic( name="combat", body="fight stuff", title="combat primer" ) await commands.dispatch(player, "help combat") output = "".join(c[0][0] for c in player.writer.write.call_args_list) assert "combat primer" in output assert "fight stuff" in output @pytest.mark.asyncio async def test_help_admin_topic_hidden_from_players(player): _help_topics["secret"] = HelpTopic( name="secret", body="hidden", title="secret stuff", admin=True ) await commands.dispatch(player, "help secret") output = "".join(c[0][0] for c in player.writer.write.call_args_list) assert "hidden" not in output assert "unknown" in output.lower() @pytest.mark.asyncio async def test_help_admin_topic_visible_to_admins(admin_player): _help_topics["secret"] = HelpTopic( name="secret", body="hidden", title="secret stuff", admin=True ) await commands.dispatch(admin_player, "help secret") output = "".join(c[0][0] for c in admin_player.writer.write.call_args_list) assert "hidden" in output def test_zones_toml_loads_from_content(): from pathlib import Path help_dir = Path(__file__).resolve().parents[1] / "content" / "help" topics = load_help_topics(help_dir) assert "zones" in topics assert topics["zones"].admin is True assert "@zones" in topics["zones"].body def test_player_has_pending_input(): from mudlib.player import Player p = Player(name="Test", x=0, y=0, reader=MagicMock(), writer=MagicMock()) assert p.pending_input is None @pytest.mark.asyncio async def test_at_help_lists_topics(admin_player): _help_topics["combat"] = HelpTopic( name="combat", body="fight", title="combat primer" ) _help_topics["zones"] = HelpTopic( name="zones", body="build", title="zone guide", admin=True ) await commands.dispatch(admin_player, "@help") output = "".join(c[0][0] for c in admin_player.writer.write.call_args_list) assert "combat" in output assert "combat primer" in output assert "zones" in output assert "[admin]" in output @pytest.mark.asyncio async def test_at_help_requires_admin(player): await commands.dispatch(player, "@help") output = "".join(c[0][0] for c in player.writer.write.call_args_list) # Should be rejected by dispatch (admin=True check) assert "permission" in output.lower() @pytest.mark.asyncio async def test_at_help_create_with_name_prompts_title(admin_player): """@help create should prompt for title.""" await commands.dispatch(admin_player, "@help create combat") output = "".join(c[0][0] for c in admin_player.writer.write.call_args_list) assert "title" in output.lower() @pytest.mark.asyncio async def test_at_help_create_no_args_prompts_name(admin_player): """@help create with no args prompts for the topic name.""" await commands.dispatch(admin_player, "@help create") output = "".join(c[0][0] for c in admin_player.writer.write.call_args_list) assert "name" in output.lower() @pytest.mark.asyncio async def test_at_help_edit_unknown_topic(admin_player): await commands.dispatch(admin_player, "@help edit bogus") output = "".join(c[0][0] for c in admin_player.writer.write.call_args_list) assert "not found" in output.lower() @pytest.mark.asyncio async def test_at_help_edit_no_args(admin_player): await commands.dispatch(admin_player, "@help edit") output = "".join(c[0][0] for c in admin_player.writer.write.call_args_list) assert "usage" in output.lower() @pytest.mark.asyncio async def test_at_help_edit_prompts_title(admin_player): _help_topics["combat"] = HelpTopic( name="combat", body="old body", title="combat primer" ) await commands.dispatch(admin_player, "@help edit combat") output = "".join(c[0][0] for c in admin_player.writer.write.call_args_list) # Should show current title as default assert "combat primer" in output @pytest.mark.asyncio async def test_at_help_remove_unknown_topic(admin_player): await commands.dispatch(admin_player, "@help remove bogus") output = "".join(c[0][0] for c in admin_player.writer.write.call_args_list) assert "not found" in output.lower() @pytest.mark.asyncio async def test_at_help_remove_no_args(admin_player): await commands.dispatch(admin_player, "@help remove") output = "".join(c[0][0] for c in admin_player.writer.write.call_args_list) assert "usage" in output.lower() @pytest.mark.asyncio async def test_at_help_remove_prompts_confirmation(admin_player): _help_topics["combat"] = HelpTopic( name="combat", body="fight", title="combat primer" ) await commands.dispatch(admin_player, "@help remove combat") output = "".join(c[0][0] for c in admin_player.writer.write.call_args_list) assert "y/n" in output.lower() or "confirm" in output.lower() def test_at_help_toml_loads_from_content(): from pathlib import Path help_dir = Path(__file__).resolve().parents[1] / "content" / "help" if help_dir.exists(): topics = load_help_topics(help_dir) assert "@help" in topics assert topics["@help"].admin is True