diff --git a/tests/test_embedded_if.py b/tests/test_embedded_if.py new file mode 100644 index 0000000..a045404 --- /dev/null +++ b/tests/test_embedded_if.py @@ -0,0 +1,242 @@ +"""Tests for embedded z-machine MUD integration. + +Tests the MUD UI components and EmbeddedIFSession integration with real zork1.z3. +""" + +import threading +from dataclasses import dataclass +from pathlib import Path + +import pytest + +from mudlib.zmachine.mud_ui import MudFilesystem, MudInputStream, MudScreen + +ZORK_PATH = Path(__file__).parent.parent / "content" / "stories" / "zork1.z3" +requires_zork = pytest.mark.skipif(not ZORK_PATH.exists(), reason="zork1.z3 not found") + + +@dataclass +class MockWriter: + def write(self, data): + pass + + async def drain(self): + pass + + +# Unit tests for MUD UI components + + +def test_mud_screen_captures_output(): + """MudScreen captures written text and flush returns it.""" + screen = MudScreen() + screen.write("Hello ") + screen.write("world!") + output = screen.flush() + assert output == "Hello world!" + + +def test_mud_screen_flush_clears_buffer(): + """MudScreen flush clears buffer, second flush returns empty.""" + screen = MudScreen() + screen.write("test") + first = screen.flush() + assert first == "test" + + second = screen.flush() + assert second == "" + + +def test_mud_input_stream_feed_and_read(): + """MudInputStream feed and read_line work with threading.""" + stream = MudInputStream() + result = [] + + def reader(): + result.append(stream.read_line()) + + t = threading.Thread(target=reader) + t.start() + # Wait for stream to signal it's waiting + stream._waiting.wait(timeout=2) + stream.feed("hello") + t.join(timeout=2) + assert result == ["hello"] + + +def test_mud_filesystem_save_restore(tmp_path): + """MudFilesystem save and restore bytes correctly.""" + save_path = tmp_path / "test.qzl" + filesystem = MudFilesystem(save_path) + + test_data = b"\x01\x02\x03\x04\x05" + success = filesystem.save_game(test_data) + assert success + assert save_path.exists() + + restored = filesystem.restore_game() + assert restored == test_data + + +# Integration tests with real zork1.z3 + + +@requires_zork +@pytest.mark.asyncio +async def test_embedded_session_start(): + """EmbeddedIFSession starts and returns intro containing game info.""" + from mudlib.embedded_if_session import EmbeddedIFSession + from mudlib.player import Player + + mock_writer = MockWriter() + player = Player(name="tester", x=5, y=5, writer=mock_writer) + + session = EmbeddedIFSession(player, str(ZORK_PATH), "zork1") + intro = await session.start() + + assert intro is not None + assert len(intro) > 0 + # Intro should contain game title or location + assert "ZORK" in intro or "West of House" in intro + + +@requires_zork +@pytest.mark.asyncio +async def test_embedded_session_handle_input(): + """EmbeddedIFSession handles input and returns response.""" + from mudlib.embedded_if_session import EmbeddedIFSession + from mudlib.player import Player + + mock_writer = MockWriter() + player = Player(name="tester", x=5, y=5, writer=mock_writer) + + session = EmbeddedIFSession(player, str(ZORK_PATH), "zork1") + await session.start() + + response = await session.handle_input("look") + + assert response is not None + assert response.done is False + assert len(response.output) > 0 + # Looking should describe the starting location + assert "West of House" in response.output or "house" in response.output + + +@requires_zork +@pytest.mark.asyncio +async def test_embedded_session_escape_help(): + """EmbeddedIFSession ::help returns help text.""" + from mudlib.embedded_if_session import EmbeddedIFSession + from mudlib.player import Player + + mock_writer = MockWriter() + player = Player(name="tester", x=5, y=5, writer=mock_writer) + + session = EmbeddedIFSession(player, str(ZORK_PATH), "zork1") + await session.start() + + response = await session.handle_input("::help") + + assert response.done is False + assert "::quit" in response.output + assert "::save" in response.output + assert "::help" in response.output + + +@requires_zork +@pytest.mark.asyncio +async def test_embedded_session_escape_quit(): + """EmbeddedIFSession ::quit returns done=True.""" + from mudlib.embedded_if_session import EmbeddedIFSession + from mudlib.player import Player + + mock_writer = MockWriter() + player = Player(name="tester", x=5, y=5, writer=mock_writer) + + session = EmbeddedIFSession(player, str(ZORK_PATH), "zork1") + await session.start() + + response = await session.handle_input("::quit") + + assert response.done is True + assert "saved" in response.output.lower() + + +@requires_zork +@pytest.mark.asyncio +async def test_embedded_session_location_name(): + """EmbeddedIFSession get_location_name returns location after input.""" + from mudlib.embedded_if_session import EmbeddedIFSession + from mudlib.player import Player + + mock_writer = MockWriter() + player = Player(name="tester", x=5, y=5, writer=mock_writer) + + session = EmbeddedIFSession(player, str(ZORK_PATH), "zork1") + await session.start() + + # Send a command to advance game state + await session.handle_input("look") + + location = session.get_location_name() + + # Location may be None or a string depending on game state + assert location is None or isinstance(location, str) + + +@requires_zork +@pytest.mark.asyncio +async def test_embedded_session_room_objects(): + """EmbeddedIFSession get_room_objects returns a list after start.""" + from mudlib.embedded_if_session import EmbeddedIFSession + from mudlib.player import Player + + mock_writer = MockWriter() + player = Player(name="tester", x=5, y=5, writer=mock_writer) + + session = EmbeddedIFSession(player, str(ZORK_PATH), "zork1") + await session.start() + + objects = session.get_room_objects() + + assert isinstance(objects, list) + # Zork1 starting location usually has some objects + assert len(objects) >= 0 # May or may not have visible objects initially + + +@requires_zork +@pytest.mark.asyncio +async def test_embedded_session_save_and_restore(): + """Save a game, create new session, restore it.""" + from mudlib.embedded_if_session import EmbeddedIFSession + from mudlib.player import Player + + mock_writer = MockWriter() + # Start first session + player = Player(name="testplayer", writer=mock_writer, x=0, y=0) + session = EmbeddedIFSession(player, str(ZORK_PATH), "zork1") + await session.start() + + # Do something to change state + await session.handle_input("open mailbox") + + # Save + save_result = await session.handle_input("::save") + assert "saved" in save_result.output.lower() + await session.stop() + + # Start new session - should auto-restore via play.py pattern + session2 = EmbeddedIFSession(player, str(ZORK_PATH), "zork1") + await session2.start() + result = await session2._do_restore() + assert result # Should have restored + + # The game state should reflect the restored state + # (location may differ after restore, just verify it works) + response = await session2.handle_input("look") + assert response.output # Should get some output + await session2.stop() + + # Clean up save file + if session2.save_path.exists(): + session2.save_path.unlink() diff --git a/tests/test_play_command.py b/tests/test_play_command.py index f5beeaf..bbd693a 100644 --- a/tests/test_play_command.py +++ b/tests/test_play_command.py @@ -70,9 +70,9 @@ async def test_play_enters_if_mode(player): with patch("mudlib.commands.play.IFSession") as MockIFSession: MockIFSession.return_value = mock_session - # Ensure story file exists check passes + # Use .z5 to test dfrotz path with patch("mudlib.commands.play._find_story") as mock_find: - mock_find.return_value = "/fake/path/zork1.z3" + mock_find.return_value = "/fake/path/zork1.z5" await cmd_play(player, "zork1") @@ -108,8 +108,9 @@ async def test_play_handles_dfrotz_missing(player): with patch("mudlib.commands.play.IFSession") as MockIFSession: MockIFSession.return_value = mock_session + # Use .z5 to test dfrotz path with patch("mudlib.commands.play._find_story") as mock_find: - mock_find.return_value = "/fake/path/zork1.z3" + mock_find.return_value = "/fake/path/zork1.z5" await cmd_play(player, "zork1") @@ -147,8 +148,9 @@ async def test_play_restores_save_if_exists(player): with patch("mudlib.commands.play.IFSession") as MockIFSession: MockIFSession.return_value = mock_session + # Use .z5 to test dfrotz path with patch("mudlib.commands.play._find_story") as mock_find: - mock_find.return_value = "/fake/path/zork1.z3" + mock_find.return_value = "/fake/path/zork1.z5" await cmd_play(player, "zork1") @@ -186,8 +188,9 @@ async def test_play_no_restore_if_no_save(player): with patch("mudlib.commands.play.IFSession") as MockIFSession: MockIFSession.return_value = mock_session + # Use .z5 to test dfrotz path with patch("mudlib.commands.play._find_story") as mock_find: - mock_find.return_value = "/fake/path/zork1.z3" + mock_find.return_value = "/fake/path/zork1.z5" await cmd_play(player, "zork1")