Fix native health polling and isolate file operation traffic

This commit is contained in:
2026-10-04 21:50:29 -04:00
parent 8a31de92fa
commit 81f088739c
22 changed files with 446 additions and 44 deletions
+59 -1
View File
@@ -6,7 +6,7 @@ from pathlib import Path
import tempfile
import threading
import unittest
from unittest.mock import mock_open, patch
from unittest.mock import MagicMock, call, mock_open, patch
spec = importlib.util.spec_from_file_location("ec_agent", Path(__file__).resolve().parents[1] / "ec_agent.py")
module = importlib.util.module_from_spec(spec)
@@ -995,6 +995,64 @@ class AgentTests(unittest.TestCase):
self.assertEqual(result["players"], 1)
self.assertTrue(result["ok"])
@staticmethod
def rcon_socket(*replies):
sock = MagicMock()
sock.__enter__.return_value = sock
sock.recv.side_effect = replies
return sock
def test_udp_status_recovers_after_transient_failures(self):
self.create()
for error in (module.socket.timeout(), OSError("unreachable")):
with self.subTest(error=type(error).__name__):
failed = self.rcon_socket(error)
# Keep all response packets, including occupied player rows.
recovered = self.rcon_socket(
b"\xff\xff\xff\xffprint\nmap: mp_kino\nnum score ping guid name\n--- ----- ---- ---- ----\n",
b"\xff\xff\xff\xffprint\n0 42 CNCT abc player\n", module.socket.timeout())
with patch.object(module.socket, "socket", side_effect=[failed, recovered]) as sockets, patch.object(module.time, "sleep") as sleep:
result = self.agent.rcon(INSTANCE, 1, "status")
self.assertTrue(result["ok"])
self.assertEqual(result["players"], 1)
self.assertEqual(sockets.call_count, 2)
sleep.assert_called_once_with(0.25)
failed.__exit__.assert_called_once()
recovered.__exit__.assert_called_once()
def test_udp_status_retries_are_bounded(self):
self.create()
attempts = [self.rcon_socket(module.socket.timeout()) for _ in range(3)]
with patch.object(module.socket, "socket", side_effect=attempts) as sockets, patch.object(module.time, "sleep") as sleep:
result = self.agent.rcon(INSTANCE, 1, "status")
self.assertFalse(result["ok"])
self.assertEqual(result["reason"], "timeout")
self.assertEqual(sockets.call_count, 3)
self.assertEqual(sleep.call_args_list, [call(0.25), call(0.5)])
def test_udp_status_does_not_retry_bad_replies_or_partial_tables(self):
self.create()
for packet in (b"wrong listener", b"\xff\xff\xff\xffprint\nBad rcon_password",
b"\xff\xff\xff\xffprint\nmap: mp_kino\nnum score ping guid name\n"):
with self.subTest(packet=packet):
sock = self.rcon_socket(packet, module.socket.timeout())
with patch.object(module.socket, "socket", return_value=sock) as sockets, patch.object(module.time, "sleep") as sleep:
result = self.agent.rcon(INSTANCE, 1, "status")
self.assertIsNone(result["players"])
sockets.assert_called_once()
sleep.assert_not_called()
def test_udp_commands_with_effects_are_never_replayed(self):
self.create()
for command in ("map_restart", "kick 0", "version"):
with self.subTest(command=command):
sock = self.rcon_socket(module.socket.timeout())
with patch.object(module.socket, "socket", return_value=sock) as sockets, patch.object(module.time, "sleep") as sleep:
result = self.agent.rcon(INSTANCE, 1, command)
self.assertEqual(result["reason"], "timeout")
sockets.assert_called_once()
sleep.assert_not_called()
def test_t6_bot_column_allows_empty_restart_but_holds_players_and_partial_replies(self):
self.create()
header = "map: zm_nuked\nnum score bot ping guid name lastmsg address qport rate\n"