Prevent double-save when quitting IF sessions
This commit is contained in:
parent
8893525647
commit
bb1f9bbbd8
2 changed files with 117 additions and 0 deletions
|
|
@ -28,6 +28,8 @@ class IFSession:
|
|||
self.process: asyncio.subprocess.Process | None = None
|
||||
# data/ directory is at project root (2 levels up from src/mudlib/)
|
||||
self._data_dir = Path(__file__).resolve().parents[2] / "data"
|
||||
# Track whether we've saved (prevents double-save on quit+stop)
|
||||
self._saved = False
|
||||
|
||||
@property
|
||||
def save_path(self) -> Path:
|
||||
|
|
@ -78,6 +80,8 @@ class IFSession:
|
|||
return IFResponse(output=confirmation, done=False)
|
||||
|
||||
# Regular game input - send to dfrotz
|
||||
# Reset saved flag since game state has changed
|
||||
self._saved = False
|
||||
if self.process and self.process.stdin:
|
||||
stripped = text.strip()
|
||||
self.process.stdin.write(f"{stripped}\n".encode())
|
||||
|
|
@ -100,6 +104,10 @@ class IFSession:
|
|||
|
||||
async def _do_save(self) -> str:
|
||||
"""Save game state to disk. Returns confirmation message."""
|
||||
# Skip if already saved (prevents double-save on quit+stop)
|
||||
if self._saved:
|
||||
return "already saved"
|
||||
|
||||
if not self.process or not self.process.stdin:
|
||||
return "error: game not running"
|
||||
|
||||
|
|
@ -120,6 +128,9 @@ class IFSession:
|
|||
|
||||
# Read confirmation
|
||||
confirmation = await self._read_response()
|
||||
|
||||
# Mark as saved
|
||||
self._saved = True
|
||||
return confirmation
|
||||
|
||||
async def _do_restore(self) -> str:
|
||||
|
|
|
|||
|
|
@ -577,3 +577,109 @@ async def test_stop_saves_before_terminating(tmp_path):
|
|||
assert mock_process.stdin.write.call_args_list[0][0][0] == b"save\n"
|
||||
mock_process.terminate.assert_called_once()
|
||||
mock_process.wait.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_quit_then_stop_does_not_double_save(tmp_path):
|
||||
"""stop() after ::quit doesn't save again (prevents double-save)."""
|
||||
player = MagicMock()
|
||||
player.name = "tester"
|
||||
session = IFSession(player, "/path/to/zork.z5", "zork")
|
||||
session._data_dir = tmp_path
|
||||
|
||||
# Mock process
|
||||
mock_process = MagicMock()
|
||||
mock_process.returncode = None
|
||||
mock_process.stdin = MagicMock()
|
||||
mock_process.stdin.write = MagicMock()
|
||||
mock_process.stdin.drain = AsyncMock()
|
||||
mock_process.stdout = AsyncMock()
|
||||
mock_process.terminate = MagicMock()
|
||||
mock_process.wait = AsyncMock()
|
||||
session.process = mock_process
|
||||
|
||||
# Simulate dfrotz save responses (only expect one save)
|
||||
responses = [b"Enter saved game: \n>", b"Ok.\n>"]
|
||||
response_data = b"".join(responses)
|
||||
|
||||
async def read_side_effect(n):
|
||||
nonlocal response_data
|
||||
if response_data:
|
||||
byte = response_data[:1]
|
||||
response_data = response_data[1:]
|
||||
return byte
|
||||
return b""
|
||||
|
||||
mock_process.stdout.read = AsyncMock(side_effect=read_side_effect)
|
||||
|
||||
# First: handle ::quit (should save)
|
||||
await session.handle_input("::quit")
|
||||
|
||||
# Verify save was called once
|
||||
save_calls = [
|
||||
call for call in mock_process.stdin.write.call_args_list if call[0][0] == b"save\n"
|
||||
]
|
||||
assert len(save_calls) == 1
|
||||
|
||||
# Second: call stop() (should NOT save again)
|
||||
await session.stop()
|
||||
|
||||
# Verify save was still only called once
|
||||
save_calls = [
|
||||
call for call in mock_process.stdin.write.call_args_list if call[0][0] == b"save\n"
|
||||
]
|
||||
assert len(save_calls) == 1, "stop() should not save again after ::quit"
|
||||
mock_process.terminate.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_regular_input_resets_save_flag(tmp_path):
|
||||
"""Regular input resets the saved flag so subsequent saves work."""
|
||||
player = MagicMock()
|
||||
player.name = "tester"
|
||||
session = IFSession(player, "/path/to/zork.z5", "zork")
|
||||
session._data_dir = tmp_path
|
||||
|
||||
# Mock process
|
||||
mock_process = MagicMock()
|
||||
mock_process.returncode = None
|
||||
mock_process.stdin = MagicMock()
|
||||
mock_process.stdin.write = MagicMock()
|
||||
mock_process.stdin.drain = AsyncMock()
|
||||
mock_process.stdout = AsyncMock()
|
||||
session.process = mock_process
|
||||
|
||||
# Simulate dfrotz responses
|
||||
responses = [
|
||||
b"Enter saved game: \n>",
|
||||
b"Ok.\n>", # first save
|
||||
b"You move north.\n>", # regular input
|
||||
b"Enter saved game: \n>",
|
||||
b"Ok.\n>", # second save
|
||||
]
|
||||
response_data = b"".join(responses)
|
||||
|
||||
async def read_side_effect(n):
|
||||
nonlocal response_data
|
||||
if response_data:
|
||||
byte = response_data[:1]
|
||||
response_data = response_data[1:]
|
||||
return byte
|
||||
return b""
|
||||
|
||||
mock_process.stdout.read = AsyncMock(side_effect=read_side_effect)
|
||||
|
||||
# First: save
|
||||
await session.handle_input("::save")
|
||||
|
||||
# Second: regular input (resets saved flag)
|
||||
await session.handle_input("north")
|
||||
|
||||
# Third: save again (should work)
|
||||
await session.handle_input("::save")
|
||||
|
||||
# Verify save was called twice
|
||||
save_calls = [
|
||||
call for call in mock_process.stdin.write.call_args_list if call[0][0] == b"save\n"
|
||||
]
|
||||
assert len(save_calls) == 2, "should be able to save again after regular input"
|
||||
|
|
|
|||
Loading…
Reference in a new issue