mud/tests/test_editor_integration.py
Jared Miller a799b6716c
Add editor mode shell integration and edit command
Integrates the Editor class into the MUD server's shell loop, allowing
players to enter and use the text editor from the game.

Changes:
- Add editor field to Player dataclass
- Modify shell input loop to check player mode and route to editor
- Add edit command to enter editor mode from normal mode
- Use inp (not command.strip()) for editor to preserve indentation
- Show line-numbered prompt in editor mode
- Pop mode and clear editor when done=True
- Add comprehensive integration tests
- Fix test isolation issue in test_movement_updates_position
2026-02-07 22:59:37 -05:00

167 lines
4.9 KiB
Python

"""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")
# Cursor starts at 0. The prompt shows "cursor + 1" for 1-indexed display
# This test verifies the cursor field exists and can be used for prompts
assert player.editor.cursor == 0
# Shell loop prompt: f" {player.editor.cursor + 1}> " = " 1> "