888f8fd16e
ruff --select UP007,UP045 --fix converted ~1760 sites across the backend: `Optional[T]` → `T | None`, `Union[X, Y]` → `X | Y`. The remaining module-level alias targets that ruff conservatively skips (BindableFloatInput, ColorList, DeviceConfig) were converted by hand earlier in the pass. black -formatted the result so the wider unions fit cleanly under the 100-char line budget. pyproject.toml now sets [tool.ruff.lint] extend-select = ["UP007", "UP045"] so future legacy imports fire CI on every push. The pre-commit ruff hook was bumped from v0.8.0 -> v0.15.12 to recognise UP045 (split off from UP007 in v0.13).
279 lines
11 KiB
Python
279 lines
11 KiB
Python
"""Tests for CS2 Game State Integration adapter."""
|
|
|
|
import pytest
|
|
|
|
from ledgrab.core.game_integration.adapters.cs2_adapter import CS2Adapter
|
|
|
|
# ── Realistic CS2 GSI payload samples ────────────────────────────────────
|
|
|
|
|
|
def _make_cs2_payload(
|
|
*,
|
|
health: int = 100,
|
|
armor: int = 100,
|
|
money: int = 800,
|
|
kills: int = 0,
|
|
deaths: int = 0,
|
|
round_phase: str | None = None,
|
|
bomb: str | None = None,
|
|
flashed: int = 0,
|
|
team: str = "CT",
|
|
ammo_clip: int | None = None,
|
|
ammo_clip_max: int | None = None,
|
|
auth_token: str | None = None,
|
|
) -> dict:
|
|
"""Build a realistic CS2 GSI payload."""
|
|
payload: dict = {
|
|
"player": {
|
|
"steamid": "76561198012345678",
|
|
"name": "TestPlayer",
|
|
"team": team,
|
|
"state": {
|
|
"health": health,
|
|
"armor": armor,
|
|
"helmet": True,
|
|
"flashed": flashed,
|
|
"smoked": 0,
|
|
"burning": 0,
|
|
"money": money,
|
|
"round_kills": 0,
|
|
"round_killhs": 0,
|
|
"equip_value": 4400,
|
|
},
|
|
"match_stats": {
|
|
"kills": kills,
|
|
"assists": 0,
|
|
"deaths": deaths,
|
|
"mvps": 0,
|
|
"score": kills * 2,
|
|
},
|
|
"weapons": {},
|
|
},
|
|
}
|
|
|
|
if ammo_clip is not None and ammo_clip_max is not None:
|
|
payload["player"]["weapons"]["weapon_0"] = {
|
|
"name": "weapon_ak47",
|
|
"paintkit": "default",
|
|
"type": "Rifle",
|
|
"ammo_clip": ammo_clip,
|
|
"ammo_clip_max": ammo_clip_max,
|
|
"ammo_reserve": 90,
|
|
"state": "active",
|
|
}
|
|
|
|
if round_phase is not None:
|
|
payload["round"] = {"phase": round_phase}
|
|
|
|
if bomb is not None:
|
|
payload.setdefault("round", {})["bomb"] = bomb
|
|
|
|
if auth_token is not None:
|
|
payload["auth"] = {"token": auth_token}
|
|
|
|
return payload
|
|
|
|
|
|
# ── Health/Armor/Money tests ─────────────────────────────────────────────
|
|
|
|
|
|
class TestCS2ContinuousEvents:
|
|
def test_health_normalized(self) -> None:
|
|
payload = _make_cs2_payload(health=75)
|
|
events, _ = CS2Adapter.parse_payload(payload, {"adapter_id": "cs2_test"}, {})
|
|
health_events = [e for e in events if e.event_type == "health"]
|
|
assert len(health_events) == 1
|
|
assert health_events[0].value == pytest.approx(0.75)
|
|
|
|
def test_health_zero(self) -> None:
|
|
payload = _make_cs2_payload(health=0)
|
|
events, _ = CS2Adapter.parse_payload(payload, {"adapter_id": "cs2_test"}, {})
|
|
health_events = [e for e in events if e.event_type == "health"]
|
|
assert health_events[0].value == 0.0
|
|
|
|
def test_armor_normalized(self) -> None:
|
|
payload = _make_cs2_payload(armor=50)
|
|
events, _ = CS2Adapter.parse_payload(payload, {"adapter_id": "cs2_test"}, {})
|
|
armor_events = [e for e in events if e.event_type == "armor"]
|
|
assert len(armor_events) == 1
|
|
assert armor_events[0].value == pytest.approx(0.5)
|
|
|
|
def test_money_normalized(self) -> None:
|
|
payload = _make_cs2_payload(money=8000)
|
|
events, _ = CS2Adapter.parse_payload(payload, {"adapter_id": "cs2_test"}, {})
|
|
gold_events = [e for e in events if e.event_type == "gold"]
|
|
assert len(gold_events) == 1
|
|
assert gold_events[0].value == pytest.approx(8000.0 / 16000.0)
|
|
|
|
def test_ammo_normalized(self) -> None:
|
|
payload = _make_cs2_payload(ammo_clip=15, ammo_clip_max=30)
|
|
events, _ = CS2Adapter.parse_payload(payload, {"adapter_id": "cs2_test"}, {})
|
|
ammo_events = [e for e in events if e.event_type == "ammo"]
|
|
assert len(ammo_events) == 1
|
|
assert ammo_events[0].value == pytest.approx(0.5)
|
|
|
|
def test_adapter_id_passed_through(self) -> None:
|
|
payload = _make_cs2_payload(health=100)
|
|
events, _ = CS2Adapter.parse_payload(payload, {"adapter_id": "my_cs2"}, {})
|
|
assert all(e.adapter_id == "my_cs2" for e in events)
|
|
|
|
|
|
# ── Kill/Death diff detection tests ──────────────────────────────────────
|
|
|
|
|
|
class TestCS2DiffDetection:
|
|
def test_kill_detected_on_increase(self) -> None:
|
|
payload1 = _make_cs2_payload(kills=3)
|
|
_, state1 = CS2Adapter.parse_payload(payload1, {"adapter_id": "t"}, {})
|
|
|
|
payload2 = _make_cs2_payload(kills=4)
|
|
events, _ = CS2Adapter.parse_payload(payload2, {"adapter_id": "t"}, state1)
|
|
kill_events = [e for e in events if e.event_type == "kill"]
|
|
assert len(kill_events) == 1
|
|
|
|
def test_no_kill_on_first_payload(self) -> None:
|
|
payload = _make_cs2_payload(kills=5)
|
|
events, _ = CS2Adapter.parse_payload(payload, {"adapter_id": "t"}, {})
|
|
kill_events = [e for e in events if e.event_type == "kill"]
|
|
assert len(kill_events) == 0
|
|
|
|
def test_multiple_kills_detected(self) -> None:
|
|
payload1 = _make_cs2_payload(kills=2)
|
|
_, state1 = CS2Adapter.parse_payload(payload1, {"adapter_id": "t"}, {})
|
|
|
|
payload2 = _make_cs2_payload(kills=5)
|
|
events, _ = CS2Adapter.parse_payload(payload2, {"adapter_id": "t"}, state1)
|
|
kill_events = [e for e in events if e.event_type == "kill"]
|
|
assert len(kill_events) == 3
|
|
|
|
def test_death_detected(self) -> None:
|
|
payload1 = _make_cs2_payload(deaths=0)
|
|
_, state1 = CS2Adapter.parse_payload(payload1, {"adapter_id": "t"}, {})
|
|
|
|
payload2 = _make_cs2_payload(deaths=1)
|
|
events, _ = CS2Adapter.parse_payload(payload2, {"adapter_id": "t"}, state1)
|
|
death_events = [e for e in events if e.event_type == "death"]
|
|
assert len(death_events) == 1
|
|
|
|
def test_no_death_on_same_count(self) -> None:
|
|
payload1 = _make_cs2_payload(deaths=2)
|
|
_, state1 = CS2Adapter.parse_payload(payload1, {"adapter_id": "t"}, {})
|
|
|
|
payload2 = _make_cs2_payload(deaths=2)
|
|
events, _ = CS2Adapter.parse_payload(payload2, {"adapter_id": "t"}, state1)
|
|
death_events = [e for e in events if e.event_type == "death"]
|
|
assert len(death_events) == 0
|
|
|
|
|
|
# ── Round/Bomb trigger tests ────────────────────────────────────────────
|
|
|
|
|
|
class TestCS2Triggers:
|
|
def test_round_start(self) -> None:
|
|
payload = _make_cs2_payload(round_phase="live")
|
|
events, state = CS2Adapter.parse_payload(payload, {"adapter_id": "t"}, {})
|
|
rs = [e for e in events if e.event_type == "round_start"]
|
|
assert len(rs) == 1
|
|
assert state["round_phase"] == "live"
|
|
|
|
def test_round_end(self) -> None:
|
|
prev = {"round_phase": "live"}
|
|
payload = _make_cs2_payload(round_phase="over")
|
|
events, _ = CS2Adapter.parse_payload(payload, {"adapter_id": "t"}, prev)
|
|
re = [e for e in events if e.event_type == "round_end"]
|
|
assert len(re) == 1
|
|
|
|
def test_no_round_event_on_same_phase(self) -> None:
|
|
prev = {"round_phase": "live"}
|
|
payload = _make_cs2_payload(round_phase="live")
|
|
events, _ = CS2Adapter.parse_payload(payload, {"adapter_id": "t"}, prev)
|
|
round_events = [e for e in events if e.event_type in ("round_start", "round_end")]
|
|
assert len(round_events) == 0
|
|
|
|
def test_bomb_planted(self) -> None:
|
|
payload = _make_cs2_payload(round_phase="live", bomb="planted")
|
|
events, _ = CS2Adapter.parse_payload(payload, {"adapter_id": "t"}, {})
|
|
bomb = [e for e in events if e.event_type == "objective_captured"]
|
|
assert len(bomb) == 1
|
|
|
|
def test_bomb_defused(self) -> None:
|
|
prev = {"bomb": "planted"}
|
|
payload = _make_cs2_payload(round_phase="live", bomb="defused")
|
|
events, _ = CS2Adapter.parse_payload(payload, {"adapter_id": "t"}, prev)
|
|
defuse = [e for e in events if e.event_type == "objective_lost"]
|
|
assert len(defuse) == 1
|
|
|
|
def test_flashbang_detected(self) -> None:
|
|
payload = _make_cs2_payload(flashed=200)
|
|
events, _ = CS2Adapter.parse_payload(payload, {"adapter_id": "t"}, {"flashed": 0})
|
|
flash = [e for e in events if e.event_type == "blinded"]
|
|
assert len(flash) == 1
|
|
assert flash[0].value == pytest.approx(200.0 / 255.0)
|
|
|
|
def test_no_flashbang_when_already_flashed(self) -> None:
|
|
prev = {"flashed": 200}
|
|
payload = _make_cs2_payload(flashed=150)
|
|
events, _ = CS2Adapter.parse_payload(payload, {"adapter_id": "t"}, prev)
|
|
flash = [e for e in events if e.event_type == "blinded"]
|
|
# Already flashed (prev > 0), so no new flash event
|
|
assert len(flash) == 0
|
|
|
|
def test_team_ct(self) -> None:
|
|
payload = _make_cs2_payload(team="CT")
|
|
events, _ = CS2Adapter.parse_payload(payload, {"adapter_id": "t"}, {})
|
|
team = [e for e in events if e.event_type == "team_a"]
|
|
assert len(team) == 1
|
|
|
|
def test_team_t(self) -> None:
|
|
payload = _make_cs2_payload(team="T")
|
|
events, _ = CS2Adapter.parse_payload(payload, {"adapter_id": "t"}, {})
|
|
team = [e for e in events if e.event_type == "team_b"]
|
|
assert len(team) == 1
|
|
|
|
|
|
# ── Auth validation tests ───────────────────────────────────────────────
|
|
|
|
|
|
class TestCS2Auth:
|
|
def test_auth_valid(self) -> None:
|
|
payload = _make_cs2_payload(auth_token="mysecret")
|
|
result = CS2Adapter.validate_auth({}, payload, {"auth_token": "mysecret"})
|
|
assert result is True
|
|
|
|
def test_auth_invalid(self) -> None:
|
|
payload = _make_cs2_payload(auth_token="wrong")
|
|
result = CS2Adapter.validate_auth({}, payload, {"auth_token": "mysecret"})
|
|
assert result is False
|
|
|
|
def test_auth_no_token_configured_accepts_all(self) -> None:
|
|
payload = _make_cs2_payload()
|
|
result = CS2Adapter.validate_auth({}, payload, {})
|
|
assert result is True
|
|
|
|
def test_auth_missing_payload_token(self) -> None:
|
|
payload = _make_cs2_payload() # no auth in payload
|
|
result = CS2Adapter.validate_auth({}, payload, {"auth_token": "mysecret"})
|
|
assert result is False
|
|
|
|
|
|
# ── Metadata tests ──────────────────────────────────────────────────────
|
|
|
|
|
|
class TestCS2Metadata:
|
|
def test_adapter_type(self) -> None:
|
|
assert CS2Adapter.ADAPTER_TYPE == "cs2"
|
|
|
|
def test_supported_events(self) -> None:
|
|
assert "health" in CS2Adapter.SUPPORTED_EVENTS
|
|
assert "kill" in CS2Adapter.SUPPORTED_EVENTS
|
|
assert "death" in CS2Adapter.SUPPORTED_EVENTS
|
|
|
|
def test_config_schema(self) -> None:
|
|
schema = CS2Adapter.get_config_schema()
|
|
assert "auth_token" in schema["properties"]
|
|
|
|
def test_setup_instructions(self) -> None:
|
|
instructions = CS2Adapter.get_setup_instructions()
|
|
assert "gamestate_integration" in instructions
|
|
assert "CS2" in instructions
|