Files
ledgrab/server/tests/core/test_cs2_adapter.py
T
alexei.dolgolyov 02cd9d519c
Lint & Test / test (push) Successful in 1m56s
refactor: rename project to LedGrab, split HA integration into separate repo
- Rename Python package: wled_controller -> ledgrab
- Rename env var prefix: WLED_ -> LEDGRAB_ (with auto-migration for old vars)
- Rename localStorage key: wled_api_key -> ledgrab_api_key (with migration)
- Rename HA integration domain: wled_screen_controller -> ledgrab
- Update all imports, build scripts, Docker, installer, config, docs
- Remove HA integration (moved to ledgrab-haos-integration repo)
- Remove hacs.json (belongs in HA repo now)
- Add startup warning for users with old WLED_ env vars
- All tests pass (715/715), ruff clean, tsc clean, frontend builds
2026-04-12 22:45:28 +03:00

280 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