from __future__ import annotations import asyncio import json from pathlib import Path from types import SimpleNamespace from sqlalchemy import select from hockey_data.agent_bridge import VmixAgentHub from hockey_data.auth_bridge import HockeyUser from hockey_data.models import VmixDevice, VmixMappingField, VmixMappingProfile from tests.support import LocalTestDatabase ROOT = Path(__file__).resolve().parents[1] JS = (ROOT / "hockey_data/static/admin-directories.js").read_text(encoding="utf-8") CSS = (ROOT / "hockey_data/static/admin-directories.css").read_text(encoding="utf-8") BRIDGE = (ROOT / "hockey_data/agent_bridge.py").read_text(encoding="utf-8") def test_portable_rebind_uses_title_when_input_key_changed() -> None: fields = [{ "graphic": "score", "data_key": "game.home_score", "vmix_input_key": "old-key", "vmix_input_number": "5", "vmix_input_title": "SCORE BUG", "vmix_field": "HomeScore.Text", "field_type": "text", "enabled": True, "rule": {"enabled": True, "operator": "eq", "right_value": "1"}, }] inventory = {"inputs": [{ "key": "new-key", "number": "17", "title": "SCORE BUG", "type": "GT", "fields": [{"name": "HomeScore.Text", "type": "text", "index": "0"}], }]} rebound, report = VmixAgentHub._rebind_portable_mapping_fields(fields, inventory) assert report["mapped"] == 1 assert report["skipped"] == 0 assert report["matched_by"]["title"] == 1 assert rebound[0]["vmix_input_key"] == "new-key" assert rebound[0]["vmix_input_number"] == "17" assert rebound[0]["vmix_input_title"] == "SCORE BUG" assert rebound[0]["vmix_field"] == "HomeScore.Text" assert rebound[0]["rule"]["operator"] == "eq" def test_portable_rebind_reports_missing_field_without_guessing() -> None: fields = [{ "data_key": "game.clock", "vmix_input_key": "old-key", "vmix_input_title": "SCORE BUG", "vmix_field": "Clock.Text", "field_type": "text", "enabled": True, }] inventory = {"inputs": [{ "key": "new-key", "number": "2", "title": "SCORE BUG", "fields": [{"name": "Period.Text", "type": "text", "index": "0"}], }]} rebound, report = VmixAgentHub._rebind_portable_mapping_fields(fields, inventory) assert rebound == [] assert report["mapped"] == 0 assert report["skipped"] == 1 assert report["skipped_items"][0]["reason"] == "field_not_found" def test_copy_profile_creates_target_specific_mapping_and_keeps_source(tmp_path: Path) -> None: database = LocalTestDatabase(tmp_path / "portable-mapping.sqlite3") database.create_all() hub = VmixAgentHub(database) # type: ignore[arg-type] user = HockeyUser(id="admin-1", login="admin", display_name="Admin") source_inventory = { "fingerprint": "a" * 64, "inputs": [{ "key": "source-score-key", "number": "3", "title": "SCORE BUG", "type": "GT", "fields": [{"name": "HomeScore.Text", "type": "text", "index": "0"}], }], "input_count": 1, "field_count": 1, } target_inventory = { "fingerprint": "b" * 64, "inputs": [{ "key": "target-score-key", "number": "11", "title": "SCORE BUG", "type": "GT", "fields": [{"name": "HomeScore.Text", "type": "text", "index": "4"}], }], "input_count": 1, "field_count": 1, } with database.session() as session: source = VmixMappingProfile( name="KHL MAIN", description="portable", project_fingerprint="a" * 64, inventory_json=json.dumps(source_inventory), version=7, active=True, created_by="admin", updated_by="admin", ) session.add(source) session.flush() source_id = source.id session.add(VmixMappingField( profile_id=source.id, graphic="score", data_key="game.home_score", vmix_input_key="source-score-key", vmix_input_number="", vmix_input_title="SCORE BUG", vmix_field="HomeScore.Text", field_type="text", rule_json="{}", enabled=True, sort_order=0, )) session.add(VmixDevice( device_uuid="GFX-PORTABLE-02", device_secret_hash="x" * 64, name="GFX 2", project_fingerprint="b" * 64, project_inventory_json=json.dumps(target_inventory), project_input_count=1, project_field_count=1, vmix_connected=True, )) payload = SimpleNamespace(device_id="GFX-PORTABLE-02", name="", replace_existing=False, apply_now=False) result = asyncio.run(hub.copy_mapping_profile_to_device(source_id, payload, user)) assert result["report"]["mapped"] == 1 assert result["report"]["matched_by"]["title"] == 1 assert result["profile"]["project_fingerprint"] == "b" * 64 assert result["profile"]["fields"][0]["vmix_input_key"] == "target-score-key" assert result["profile"]["fields"][0]["vmix_input_title"] == "SCORE BUG" with database.session() as session: profiles = list(session.scalars(select(VmixMappingProfile).order_by(VmixMappingProfile.id))) assert len(profiles) == 2 assert profiles[0].id == source_id and profiles[0].active is True assert profiles[0].project_fingerprint == "a" * 64 assert profiles[1].active is True assert profiles[1].project_fingerprint == "b" * 64 def test_admin_ui_has_export_import_and_copy_to_agent_controls() -> None: assert "data-map-export" in JS assert "data-map-copy-to-device" in JS assert "data-mapping-import" in JS assert "/copy-to-device" in JS assert "/api/hockey/admin/vmix-mapping/import" in JS assert "hockey-mapping.json" in JS assert "Input ищется по key → названию → номеру" in JS assert "portable vMix Mapping transfer/import" in CSS assert '"/api/hockey/admin/vmix-mapping/profiles/{profile_id}/export"' in BRIDGE assert '"/api/hockey/admin/vmix-mapping/import"' in BRIDGE