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 hockey_data.models import OperatorSession from tests.support import LocalTestDatabase class FakeWebSocket: def __init__(self) -> None: self.client = SimpleNamespace(host="127.0.0.1") self.sent: list[dict] = [] self.closed = False async def send_json(self, payload: dict) -> None: self.sent.append(payload) async def close(self, **_kwargs) -> None: self.closed = True async def _ack_latest(hub: VmixAgentHub, device_id: str, ws: FakeWebSocket) -> dict: command = None for _ in range(100): await asyncio.sleep(0) commands = [item for item in ws.sent if item.get("type") == "vmix.command"] if commands: command = commands[-1] break assert command is not None await hub.receive_command_ack( device_id, {"type": "command.ack", "request_id": command["request_id"], "ok": True}, ) return command def test_multiple_agents_stay_enabled_and_runtime_routes_to_explicit_browser_agent(tmp_path: Path) -> None: database = LocalTestDatabase(tmp_path / "multi-agent.sqlite3") database.create_all() hub = VmixAgentHub(database) # type: ignore[arg-type] user = HockeyUser(id="multi-1", login="multi", display_name="Multi") ws_test = FakeWebSocket() ws_live = FakeWebSocket() async def scenario() -> None: for device_id, secret, ws in ( ("GFX-MULTI-TEST", "t" * 40, ws_test), ("GFX-MULTI-LIVE", "l" * 40, ws_live), ): await hub.register( ws, # type: ignore[arg-type] { "device_id": device_id, "device_secret": secret, "device_name": device_id, "hostname": device_id, "agent_version": "1.4.0", "vmix": {"connected": True, "url": "http://127.0.0.1:8088/api/"}, }, ) await hub.pair_device(device_id, user) devices = await hub.list_for_user(user) assert set(devices["active_device_ids"]) == {"GFX-MULTI-TEST", "GFX-MULTI-LIVE"} with database.session() as session: session.add( OperatorSession( session_token="multi-session", wfl_user_id=user.id, login_snapshot=user.login, tournament_external_id="1437", game_external_id="902918", status="active", ) ) assert await hub.assign_match( wfl_user_id=user.id, tournament_external_id="1437", game_external_id="902918", device_id="GFX-MULTI-TEST", operator_session_token="multi-session", ) assert await hub.assign_match( wfl_user_id=user.id, tournament_external_id="1437", game_external_id="902918", device_id="GFX-MULTI-LIVE", operator_session_token="multi-session", ) # Ignore pairing / assignment traffic and count only runtime vmix.command messages. before_test = len([item for item in ws_test.sent if item.get("type") == "vmix.command"]) before_live = len([item for item in ws_live.sent if item.get("type") == "vmix.command"]) task_test = asyncio.create_task( hub.run_vmix_sequence_for_user( user, [{"Function": "OverlayInput1In", "Input": "score-key"}], device_id="GFX-MULTI-TEST", session_token="multi-session", ) ) command_test = await _ack_latest(hub, "GFX-MULTI-TEST", ws_test) result_test = await task_test assert result_test["device_id"] == "GFX-MULTI-TEST" assert command_test["command"]["Function"] == "OverlayInput1In" assert len([item for item in ws_test.sent if item.get("type") == "vmix.command"]) == before_test + 1 assert len([item for item in ws_live.sent if item.get("type") == "vmix.command"]) == before_live task_live = asyncio.create_task( hub.run_vmix_sequence_for_user( user, [{"Function": "OverlayInput1Out", "Input": "score-key"}], device_id="GFX-MULTI-LIVE", session_token="multi-session", ) ) command_live = await _ack_latest(hub, "GFX-MULTI-LIVE", ws_live) result_live = await task_live assert result_live["device_id"] == "GFX-MULTI-LIVE" assert command_live["command"]["Function"] == "OverlayInput1Out" assert len([item for item in ws_live.sent if item.get("type") == "vmix.command"]) == before_live + 1 assert len([item for item in ws_test.sent if item.get("type") == "vmix.command"]) == before_test + 1 asyncio.run(scenario())