from __future__ import annotations import asyncio from pathlib import Path from types import SimpleNamespace from hockey_data.agent_bridge import VmixAgentHub from hockey_data.auth_bridge import HockeyUser from tests.support import LocalTestDatabase ROOT = Path(__file__).resolve().parents[1] JS = (ROOT / "hockey_data" / "static" / "tournament-menu.js").read_text(encoding="utf-8") AGENT = (ROOT / "hockey_data" / "agent_bridge.py").read_text(encoding="utf-8") ROUTER = (ROOT / "hockey_data" / "router.py").read_text(encoding="utf-8") APP = (ROOT / "app.py").read_text(encoding="utf-8") class FakeWebSocket: def __init__(self) -> None: self.client = SimpleNamespace(host="127.0.0.1") self.sent: list[dict] = [] async def send_json(self, payload: dict) -> None: self.sent.append(payload) async def close(self, **_kwargs) -> None: return None def test_stale_browser_device_falls_back_to_only_live_active_agent(tmp_path: Path) -> None: database = LocalTestDatabase(tmp_path / "build111.sqlite3") database.create_all() hub = VmixAgentHub(database) # type: ignore[arg-type] ws = FakeWebSocket() user = HockeyUser(id="111", login="operator111", display_name="Operator 111") async def scenario() -> None: await hub.register( ws, # type: ignore[arg-type] { "device_id": "GFX-BUILD111", "device_secret": "x" * 40, "device_name": "GFX BUILD111", "hostname": "GFX-BUILD111", "agent_version": "1.4.0", "vmix": {"connected": True, "url": "http://127.0.0.1:8088/api/"}, }, ) await hub.pair_device("GFX-BUILD111", user) result = await hub.assign_match( wfl_user_id=user.id, tournament_external_id="1437", game_external_id="902918", device_id="STALE-BROWSER-DEVICE", wait_for_mapping=False, ) assert result is not None assert result["device_id"] == "GFX-BUILD111" assert result["delivered"] is True assert result["mapping_apply"]["deferred"] is True match_assigns = [item for item in ws.sent if item.get("type") == "match.assign"] assert len(match_assigns) == 1 assert match_assigns[0]["game_id"] == "902918" asyncio.run(scenario()) def test_browser_retries_match_assign_without_mapping_when_needed() -> None: assert "async function ensureSessionAgentAssignment(session)" in JS assert 'body: JSON.stringify({ session_token: token, apply_mapping: false })' in JS assert 'item?.paired_to_me' in JS assert 'item?.active_for_account' in JS assert 'item?.online' in JS assert 'live.length === 1' in JS def test_mapping_is_refreshed_only_after_roster_enrichment() -> None: assert "async function refreshSessionAgentMapping(session)" in JS branch = JS.index("if (!manual && !cachedRostersReady)") enrich = JS.index("await enrichOpenedGame", branch) mapping = JS.index("refreshSessionAgentMapping(session)", enrich) assert enrich < mapping open_session = ROUTER[ROUTER.index('@router.post("/sessions")'):ROUTER.index('@router.get("/sessions/{token}")')] assert "agent_hub.apply_mapping_to_device" not in open_session def test_select_session_supports_assignment_without_early_mapping() -> None: assert "apply_mapping: bool = True" in AGENT assert "wait_for_mapping=bool(payload.apply_mapping)" in AGENT def test_runtime_version_build111() -> None: assert 'BUILD_VERSION = "2026.08.24.14"' in APP