mud/tests/test_play_command.py
Jared Miller 602da45ac2
Fix IF bugs: case-insensitive story lookup, double prompt, phantom restore command
- _find_story() now compares path.stem.lower() so "lostpig" matches "LostPig.z8"
- Server no longer writes its own prompt in IF mode (game handles prompting)
- Suppress phantom game output on restore (saved PC past sread causes garbage)
- Route .z5/.z8 files to EmbeddedIFSession now that V5+ is supported
2026-02-10 14:16:19 -05:00

190 lines
5.8 KiB
Python

"""Tests for the play command."""
from unittest.mock import AsyncMock, MagicMock, Mock, patch
import pytest
@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=5, y=5, writer=mock_writer)
def test_play_command_registered():
"""Verify play command is registered."""
import mudlib.commands.play # noqa: F401
from mudlib import commands
assert "play" in commands._registry
cmd = commands._registry["play"]
assert cmd.name == "play"
assert cmd.mode == "normal"
@pytest.mark.asyncio
async def test_play_no_args(player):
"""Playing with no args sends usage message."""
from mudlib.commands.play import cmd_play
await cmd_play(player, "")
player.writer.write.assert_called()
output = player.writer.write.call_args[0][0]
assert "play what?" in output.lower()
@pytest.mark.asyncio
async def test_play_unknown_story(player):
"""Playing unknown story sends error message."""
from mudlib.commands.play import cmd_play
await cmd_play(player, "nosuchgame")
player.writer.write.assert_called()
output = player.writer.write.call_args[0][0]
assert "no story" in output.lower()
assert "nosuchgame" in output.lower()
@pytest.mark.asyncio
async def test_play_enters_if_mode(player):
"""Playing a valid story enters IF mode and creates session."""
from pathlib import Path
from mudlib.commands.play import cmd_play
# Mock IFSession
mock_session = Mock()
mock_session.start = AsyncMock(return_value="Welcome to Zork!")
mock_session.save_path = Mock(spec=Path)
mock_session.save_path.exists = Mock(return_value=False)
with patch("mudlib.commands.play.EmbeddedIFSession") as MockSession:
MockSession.return_value = mock_session
with patch("mudlib.commands.play._find_story") as mock_find:
mock_find.return_value = "/fake/path/zork1.z5"
await cmd_play(player, "zork1")
# Verify session was created and started
mock_session.start.assert_called_once()
# Verify mode was pushed
assert "if" in player.mode_stack
# Verify session was attached to player
assert player.if_session is mock_session
# Verify intro was sent
player.writer.write.assert_called()
output = player.writer.write.call_args[0][0]
assert "Welcome to Zork!" in output
@pytest.mark.asyncio
async def test_play_handles_dfrotz_missing(player):
"""Playing when dfrotz is missing sends error."""
from pathlib import Path
from mudlib.commands.play import cmd_play
# Mock IFSession to raise FileNotFoundError on start
mock_session = Mock()
mock_session.start = AsyncMock(side_effect=FileNotFoundError())
mock_session.stop = AsyncMock()
mock_session.save_path = Mock(spec=Path)
mock_session.save_path.exists = Mock(return_value=False)
with patch("mudlib.commands.play.IFSession") as MockIFSession:
MockIFSession.return_value = mock_session
# Use .zblorb to test dfrotz path (z3/z5/z8 go to embedded)
with patch("mudlib.commands.play._find_story") as mock_find:
mock_find.return_value = "/fake/path/game.zblorb"
await cmd_play(player, "game")
# Verify error message was sent
player.writer.write.assert_called()
output = player.writer.write.call_args[0][0]
assert "dfrotz not found" in output.lower()
# Verify mode was NOT pushed
assert "if" not in player.mode_stack
# Verify session was NOT attached
assert player.if_session is None
# Verify session.stop() was called
mock_session.stop.assert_called_once()
@pytest.mark.asyncio
async def test_play_restores_save_if_exists(player):
"""Playing restores saved game if save file exists (via start())."""
from mudlib.commands.play import cmd_play
# Mock IFSession - restore now happens in start() before thread launches
mock_session = Mock()
restored_output = (
"restoring saved game...\r\nrestored.\r\n\r\n"
"West of House\nYou are standing in an open field."
)
mock_session.start = AsyncMock(return_value=restored_output)
with patch("mudlib.commands.play.EmbeddedIFSession") as MockSession:
MockSession.return_value = mock_session
with patch("mudlib.commands.play._find_story") as mock_find:
mock_find.return_value = "/fake/path/zork1.z5"
await cmd_play(player, "zork1")
# Verify session was created and started
mock_session.start.assert_called_once()
# Verify mode was pushed
assert "if" in player.mode_stack
# Verify restored text was sent (start() returns full output with restore)
calls = [call[0][0] for call in player.writer.write.call_args_list]
full_output = "".join(calls)
assert "West of House" in full_output
assert "open field" in full_output
@pytest.mark.asyncio
async def test_play_no_restore_if_no_save(player):
"""Playing does not restore if no save file exists."""
from mudlib.commands.play import cmd_play
# Mock EmbeddedIFSession
mock_session = Mock()
mock_session.start = AsyncMock(return_value="Welcome to Zork!")
with patch("mudlib.commands.play.EmbeddedIFSession") as MockSession:
MockSession.return_value = mock_session
with patch("mudlib.commands.play._find_story") as mock_find:
mock_find.return_value = "/fake/path/zork1.z5"
await cmd_play(player, "zork1")
# Verify session was created and started
mock_session.start.assert_called_once()
# Verify intro was sent but not restore message
calls = [call[0][0] for call in player.writer.write.call_args_list]
full_output = "".join(calls)
assert "Welcome to Zork!" in full_output
assert "restoring" not in full_output.lower()