"""Tests for editor integration with the shell and command system.""" from unittest.mock import AsyncMock, MagicMock import pytest from mudlib import commands from mudlib.commands.edit import cmd_edit from mudlib.editor import Editor from mudlib.player import Player @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): return Player(name="TestPlayer", x=5, y=5, reader=mock_reader, writer=mock_writer) @pytest.mark.asyncio async def test_edit_command_enters_editor_mode(player): """Test that edit command sets up editor mode.""" assert player.mode == "normal" assert player.editor is None await cmd_edit(player, "") assert player.mode == "editor" assert player.editor is not None assert isinstance(player.editor, Editor) assert player.mode_stack == ["normal", "editor"] @pytest.mark.asyncio async def test_edit_command_sends_welcome_message(player, mock_writer): """Test that edit command sends welcome message.""" await cmd_edit(player, "") assert mock_writer.write.called output = "".join([call[0][0] for call in mock_writer.write.call_args_list]) assert "editor" in output.lower() assert ":h" in output @pytest.mark.asyncio async def test_edit_command_sets_save_callback(player): """Test that edit command sets up a save callback.""" await cmd_edit(player, "") assert player.editor is not None assert player.editor.save_callback is not None @pytest.mark.asyncio async def test_editor_handle_input_preserves_whitespace(player): """Test that editor receives input with whitespace preserved.""" await cmd_edit(player, "") # Simulate code with indentation response = await player.editor.handle_input(" def foo():") assert player.editor.buffer == [" def foo():"] assert response.done is False @pytest.mark.asyncio async def test_editor_done_clears_editor_and_pops_mode(player): """Test that when editor returns done=True, mode is popped and editor cleared.""" await cmd_edit(player, "") assert player.mode == "editor" assert player.editor is not None # Quit the editor response = await player.editor.handle_input(":q") assert response.done is True # Simulate what the shell loop should do player.editor = None player.mode_stack.pop() assert player.mode == "normal" assert player.editor is None @pytest.mark.asyncio async def test_editor_save_callback_sends_message(player, mock_writer): """Test that save callback sends confirmation to player.""" await cmd_edit(player, "") mock_writer.reset_mock() await player.editor.handle_input("test line") response = await player.editor.handle_input(":w") assert response.saved is True # Save callback should have sent a message assert mock_writer.write.called output = "".join([call[0][0] for call in mock_writer.write.call_args_list]) assert "saved" in output.lower() @pytest.mark.asyncio async def test_edit_command_only_works_in_normal_mode(player): """Test that edit command has mode='normal' restriction.""" # Push a different mode onto the stack player.mode_stack.append("combat") # Try to invoke edit command through dispatch await commands.dispatch(player, "edit") # Should be blocked by mode check assert player.mode == "combat" assert player.editor is None @pytest.mark.asyncio async def test_mode_stack_push_and_pop(player): """Test mode stack mechanics for editor mode.""" assert player.mode_stack == ["normal"] assert player.mode == "normal" # Enter editor mode player.mode_stack.append("editor") assert player.mode == "editor" assert player.mode_stack == ["normal", "editor"] # Exit editor mode player.mode_stack.pop() assert player.mode == "normal" assert player.mode_stack == ["normal"] @pytest.mark.asyncio async def test_empty_input_allowed_in_editor(player): """Test that empty lines are valid in editor mode (for blank lines in code).""" await cmd_edit(player, "") # Empty line should be appended to buffer response = await player.editor.handle_input("") assert response.done is False assert player.editor.buffer == [""] @pytest.mark.asyncio async def test_editor_prompt_uses_cursor(player): """Test that editor prompt should show line number from cursor.""" await cmd_edit(player, "") # Add some lines await player.editor.handle_input("line 1") await player.editor.handle_input("line 2") # After appending, cursor should be at the last line (1 in 0-indexed) # This test verifies the cursor field exists and can be used for prompts assert player.editor.cursor == 1 # Shell loop prompt: f" {player.editor.cursor + 1}> " = " 2> " @pytest.mark.asyncio async def test_edit_no_args_opens_blank_editor(player): """Test that edit with no args opens a blank editor.""" await cmd_edit(player, "") assert player.editor is not None assert player.editor.buffer == [] assert player.mode == "editor" @pytest.mark.asyncio async def test_edit_combat_move_opens_toml(player, tmp_path): """Test that edit roundhouse opens the TOML file for editing.""" from mudlib.combat import commands as combat_commands # Create a test TOML file toml_content = """name = "roundhouse" aliases = ["rh"] move_type = "attack" stamina_cost = 8.0 timing_window_ms = 2000 """ toml_file = tmp_path / "roundhouse.toml" toml_file.write_text(toml_content) # Set up combat moves combat_commands.combat_moves = {"roundhouse": MagicMock(command="roundhouse")} combat_commands.combat_content_dir = tmp_path await cmd_edit(player, "roundhouse") assert player.editor is not None assert player.mode == "editor" assert toml_content in "\n".join(player.editor.buffer) @pytest.mark.asyncio async def test_edit_combat_move_saves_to_disk(player, tmp_path, mock_writer): """Test that saving in editor writes back to the TOML file.""" from mudlib.combat import commands as combat_commands # Create a test TOML file original_content = """name = "roundhouse" aliases = ["rh"] move_type = "attack" """ toml_file = tmp_path / "roundhouse.toml" toml_file.write_text(original_content) # Set up combat moves combat_commands.combat_moves = {"roundhouse": MagicMock(command="roundhouse")} combat_commands.combat_content_dir = tmp_path await cmd_edit(player, "roundhouse") mock_writer.reset_mock() # Modify the buffer player.editor.buffer = [ 'name = "roundhouse"', 'aliases = ["rh"]', 'move_type = "attack"', "stamina_cost = 9.0", ] # Save await player.editor.handle_input(":w") # Check that file was written saved_content = toml_file.read_text() assert "stamina_cost = 9.0" in saved_content assert mock_writer.write.called @pytest.mark.asyncio async def test_edit_variant_base_opens_toml(player, tmp_path): """Test that edit punch opens punch.toml (variant base name).""" from mudlib.combat import commands as combat_commands # Create punch.toml with variants toml_content = """name = "punch" move_type = "attack" [variants.left] aliases = ["pl"] """ toml_file = tmp_path / "punch.toml" toml_file.write_text(toml_content) # Set up combat moves with variant combat_commands.combat_moves = { "punch left": MagicMock(command="punch", variant="left") } combat_commands.combat_content_dir = tmp_path await cmd_edit(player, "punch") assert player.editor is not None assert player.mode == "editor" assert "[variants.left]" in "\n".join(player.editor.buffer) @pytest.mark.asyncio async def test_edit_unknown_content_shows_error(player, mock_writer, tmp_path): """Test that edit nonexistent shows an error.""" from mudlib.combat import commands as combat_commands combat_commands.combat_moves = {} combat_commands.combat_content_dir = tmp_path await cmd_edit(player, "nonexistent") assert player.editor is None assert player.mode == "normal" assert mock_writer.write.called output = "".join([call[0][0] for call in mock_writer.write.call_args_list]) assert "unknown" in output.lower() assert "nonexistent" in output.lower() @pytest.mark.asyncio async def test_edit_combat_move_uses_toml_content_type(player, tmp_path): """Test that editor for combat moves uses toml content type.""" from mudlib.combat import commands as combat_commands toml_file = tmp_path / "roundhouse.toml" toml_file.write_text("name = 'roundhouse'\n") combat_commands.combat_moves = {"roundhouse": MagicMock(command="roundhouse")} combat_commands.combat_content_dir = tmp_path await cmd_edit(player, "roundhouse") assert player.editor is not None assert player.editor.content_type == "toml"