Add station listing and cargo queries to admin client
All checks were successful
Continuous Integration / lint-and-security (pull_request) Successful in 20s
Continuous Integration / tests-and-coverage (pull_request) Successful in 24s

Extends the AdminBridge GameScript JSON channel (the same relay used by
list_vehicles/get_timetable) with station support:

- list_stations(): enumerate stations, fire-and-forget like list_vehicles().
- get_station(): authoritative per-cargo snapshot of a station's live state,
  with both the real-time waiting amount (GSStation.GetCargoWaiting) and the
  planned cargodist link-graph flow (GetCargoPlanned), plus rating.
- get_station_cargo(): break one cargo type down by source station and by
  next hop (the cargodist routing destination) for both waiting and planned
  amounts, with optional from_station/via_station filters.

All three use stock GameScript API (no server patch, unlike timetables).
Refactors the shared GS request/reply correlation out of get_timetable and
get_station into a _gs_query() helper. Companion handlers must be added to
the server-side AdminBridge GameScript (not tracked in this repo).

Includes unit + e2e tests, a worked demo in main_admin.py, and protocol docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-23 21:48:20 +02:00
parent c39f970ef9
commit 81a4d9333d
6 changed files with 418 additions and 17 deletions

View File

@@ -464,6 +464,133 @@ async def test_e2e_admin_get_timetable_invalid_vehicle(connected_admin):
with pytest.raises(ValueError, match="invalid_vehicle"):
await connected_admin.get_timetable(65535, timeout=10.0)
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_e2e_admin_list_stations_all_companies(connected_admin):
# Public function: list_stations()
# Input 1: all companies (no company_id)
responses = []
connected_admin.on_gamescript = lambda data: responses.append(data)
await connected_admin.update_frequency(AdminUpdateType.Gamescript, AdminUpdateFrequency.Automatic)
await connected_admin.list_stations()
await asyncio.sleep(0.5)
assert not connected_admin.shutdown_event.is_set()
assert len(responses) >= 1
assert "stations" in responses[-1]
assert isinstance(responses[-1]["stations"], list)
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_e2e_admin_list_stations_specific_company(connected_admin):
# Public function: list_stations()
# Input 2: specific company_id
responses = []
connected_admin.on_gamescript = lambda data: responses.append(data)
await connected_admin.update_frequency(AdminUpdateType.Gamescript, AdminUpdateFrequency.Automatic)
await connected_admin.list_stations(company_id=0)
await asyncio.sleep(0.5)
assert not connected_admin.shutdown_event.is_set()
assert len(responses) >= 1
assert "stations" in responses[-1]
assert isinstance(responses[-1]["stations"], list)
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_e2e_admin_get_station_valid_station(connected_admin):
# Public function: get_station()
# Input 1: a real station id discovered via list_stations
responses = []
connected_admin.on_gamescript = lambda data: responses.append(data)
await connected_admin.update_frequency(AdminUpdateType.Gamescript, AdminUpdateFrequency.Automatic)
await connected_admin.list_stations()
await asyncio.sleep(0.5)
assert len(responses) >= 1 and "stations" in responses[-1]
stations = responses[-1]["stations"]
if not stations:
pytest.skip("No stations on the test server to query.")
sid = stations[0]["id"]
data = await connected_admin.get_station(sid, timeout=10.0)
assert data["station_id"] == sid
assert "cargo" in data
assert isinstance(data["cargo"], list)
for cargo in data["cargo"]:
for key in ("cargo_id", "waiting", "planned", "rating"):
assert key in cargo
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_e2e_admin_get_station_invalid_station(connected_admin):
# Public function: get_station()
# Input 2: an id no station can have -> GameScript reports invalid_station
with pytest.raises(ValueError, match="invalid_station"):
await connected_admin.get_station(65535, timeout=10.0)
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_e2e_admin_get_station_cargo_breakdown(connected_admin):
# Public function: get_station_cargo()
# Input 1: a real station + a cargo it has handled, discovered via list_stations/get_station
responses = []
connected_admin.on_gamescript = lambda data: responses.append(data)
await connected_admin.update_frequency(AdminUpdateType.Gamescript, AdminUpdateFrequency.Automatic)
await connected_admin.list_stations()
await asyncio.sleep(0.5)
assert responses and "stations" in responses[-1]
stations = responses[-1]["stations"]
if not stations:
pytest.skip("No stations on the test server to query.")
# Find a station/cargo pair that actually has cargo data.
target = None
for st in stations:
detail = await connected_admin.get_station(st["id"], timeout=10.0)
if detail["cargo"]:
target = (st["id"], detail["cargo"][0]["cargo_id"])
break
if target is None:
pytest.skip("No station with handled cargo to break down.")
sid, cid = target
data = await connected_admin.get_station_cargo(sid, cid, timeout=10.0)
assert data["station_id"] == sid and data["cargo_id"] == cid
for key in ("waiting", "planned",
"waiting_by_from", "planned_by_from", "waiting_by_via", "planned_by_via"):
assert key in data
for key in ("waiting_by_from", "planned_by_from", "waiting_by_via", "planned_by_via"):
assert isinstance(data[key], list)
for entry in data[key]:
assert "station" in entry and "amount" in entry
# Input 2: the same query narrowed by a next-hop (via) filter is accepted and echoes it back.
filtered = await connected_admin.get_station_cargo(sid, cid, via_station=sid, timeout=10.0)
assert filtered["via_station"] == sid
@pytest.mark.e2e
@pytest.mark.asyncio
async def test_e2e_admin_get_station_cargo_invalid_cargo(connected_admin):
# Public function: get_station_cargo()
# A cargo id no cargo can have -> GameScript reports invalid_cargo. Needs a valid station.
responses = []
connected_admin.on_gamescript = lambda data: responses.append(data)
await connected_admin.update_frequency(AdminUpdateType.Gamescript, AdminUpdateFrequency.Automatic)
await connected_admin.list_stations()
await asyncio.sleep(0.5)
assert responses and "stations" in responses[-1]
stations = responses[-1]["stations"]
if not stations:
pytest.skip("No stations on the test server to query.")
with pytest.raises(ValueError, match="invalid_cargo"):
await connected_admin.get_station_cargo(stations[0]["id"], 250, timeout=10.0)
# --- Protocol Public Functions ---