forked from Rockachopa/Timmy-time-dashboard
Automated salvage commit — agent session ended (exit 124). Work in progress, may need continuation.
163 lines
5.4 KiB
Python
163 lines
5.4 KiB
Python
"""Tests for the GABSClient — uses mocked asyncio streams, no real TCP."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import pytest
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
from bannerlord.gabs_client import GABSClient
|
|
from bannerlord.types import GameState
|
|
|
|
|
|
@pytest.fixture
|
|
def client():
|
|
return GABSClient(host="127.0.0.1", port=4825, timeout=2.0)
|
|
|
|
|
|
class TestGABSClientConnection:
|
|
@pytest.mark.asyncio
|
|
async def test_connect_returns_false_when_refused(self, client):
|
|
with patch(
|
|
"asyncio.open_connection",
|
|
side_effect=ConnectionRefusedError("refused"),
|
|
):
|
|
result = await client.connect()
|
|
assert result is False
|
|
assert not client.is_connected
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_connect_success(self, client):
|
|
mock_reader = AsyncMock()
|
|
mock_writer = MagicMock()
|
|
mock_writer.drain = AsyncMock()
|
|
mock_writer.close = MagicMock()
|
|
mock_writer.wait_closed = AsyncMock()
|
|
|
|
# Simulate tools/list response on connect
|
|
tools_response = json.dumps({"jsonrpc": "2.0", "id": 1, "result": []}) + "\n"
|
|
mock_reader.readline = AsyncMock(return_value=tools_response.encode())
|
|
|
|
with patch("asyncio.open_connection", return_value=(mock_reader, mock_writer)):
|
|
with patch("asyncio.wait_for", side_effect=_passthrough_wait_for):
|
|
result = await client.connect()
|
|
|
|
assert result is True
|
|
assert client.is_connected
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_disconnect_when_not_connected(self, client):
|
|
# Should not raise
|
|
await client.disconnect()
|
|
assert not client.is_connected
|
|
|
|
|
|
class TestGABSClientCall:
|
|
@pytest.mark.asyncio
|
|
async def test_call_returns_none_when_disconnected(self, client):
|
|
result = await client._call("game/get_state")
|
|
assert result is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_call_id_increments(self, client):
|
|
assert client._next_id() == 1
|
|
assert client._next_id() == 2
|
|
assert client._next_id() == 3
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_game_state_returns_empty_when_disconnected(self, client):
|
|
state = await client.get_game_state()
|
|
assert isinstance(state, GameState)
|
|
assert state.tick == 0
|
|
assert not state.has_kingdom()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_move_party_returns_false_when_disconnected(self, client):
|
|
result = await client.move_party("Vlandia")
|
|
assert result is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_propose_peace_returns_false_when_disconnected(self, client):
|
|
result = await client.propose_peace("Vlandia")
|
|
assert result is False
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_assess_prices_returns_empty_dict_when_disconnected(self, client):
|
|
result = await client.assess_prices("Pravend")
|
|
assert result == {}
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_map_patrol_routes_returns_empty_list_when_disconnected(self, client):
|
|
result = await client.map_patrol_routes("Vlandia")
|
|
assert result == []
|
|
|
|
|
|
class TestGameStateParsing:
|
|
def test_parse_game_state_full(self):
|
|
client = GABSClient()
|
|
raw = {
|
|
"tick": 5,
|
|
"in_game_day": 42,
|
|
"party": {
|
|
"location": "Pravend",
|
|
"troops": 200,
|
|
"food_days": 8,
|
|
"wounded_pct": 0.1,
|
|
"denars": 15000,
|
|
"morale": 85.0,
|
|
"prisoners": 3,
|
|
},
|
|
"kingdom": {
|
|
"name": "House Timmerson",
|
|
"fiefs": ["Pravend", "Epicrotea"],
|
|
"daily_income": 500,
|
|
"daily_expenses": 300,
|
|
"vassal_lords": ["Lord A"],
|
|
"active_wars": ["Sturgia"],
|
|
"active_alliances": ["Battania"],
|
|
},
|
|
"factions": [
|
|
{
|
|
"name": "Sturgia",
|
|
"leader": "Raganvad",
|
|
"fiefs": ["Varcheg"],
|
|
"army_strength": 250,
|
|
"treasury": 5000,
|
|
"is_at_war_with": ["House Timmerson"],
|
|
"relations": {"House Timmerson": -50},
|
|
}
|
|
],
|
|
}
|
|
state = client._parse_game_state(raw)
|
|
assert state.tick == 5
|
|
assert state.in_game_day == 42
|
|
assert state.party.location == "Pravend"
|
|
assert state.party.troops == 200
|
|
assert state.kingdom.name == "House Timmerson"
|
|
assert state.fief_count() == 2
|
|
assert len(state.factions) == 1
|
|
assert state.factions[0].name == "Sturgia"
|
|
assert state.has_kingdom()
|
|
assert not state.is_two_front_war()
|
|
|
|
def test_parse_game_state_minimal(self):
|
|
client = GABSClient()
|
|
state = client._parse_game_state({})
|
|
assert isinstance(state, GameState)
|
|
assert not state.has_kingdom()
|
|
|
|
def test_tool_count_zero_before_connect(self):
|
|
client = GABSClient()
|
|
assert client.tool_count() == 0
|
|
assert client.available_tools == []
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helper
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def _passthrough_wait_for(coro, timeout=None):
|
|
"""Stand-in for asyncio.wait_for that just awaits the coroutine."""
|
|
import asyncio
|
|
return await coro
|