тест 10

This commit is contained in:
2026-08-24 18:49:48 +03:00
parent 10a65e4d47
commit 9ab53ac8d5
4 changed files with 82 additions and 10 deletions

3
app.py
View File

@@ -29,7 +29,8 @@ from ui_builder import install_ui_builder
from khl_site.khl_data_center import APP as khl_site_app from khl_site.khl_data_center import APP as khl_site_app
BASE_DIR = Path(__file__).resolve().parent BASE_DIR = Path(__file__).resolve().parent
BUILD_VERSION = "2026.08.24.14" BUILD_VERSION = "2026.08.24.15"
# compatibility: BUILD_VERSION = "2026.08.24.14"
# compatibility: BUILD_VERSION = "2026.08.24.13" # compatibility: BUILD_VERSION = "2026.08.24.13"
# compatibility: BUILD_VERSION = "2026.08.24.8" # compatibility: BUILD_VERSION = "2026.08.24.8"
# compatibility: BUILD_VERSION = "2026.08.24.7" # compatibility: BUILD_VERSION = "2026.08.24.7"

View File

@@ -354,11 +354,21 @@ class VmixAgentHub:
return key.split(".", 1)[0] if key else "" return key.split(".", 1)[0] if key else ""
@staticmethod @staticmethod
def _agent_supports_batch(version: Any) -> bool: def _agent_version_tuple(version: Any) -> tuple[int, int, int] | None:
match = re.search(r"(\d+)\.(\d+)\.(\d+)", str(version or "")) match = re.search(r"(\d+)\.(\d+)\.(\d+)", str(version or ""))
if not match: if not match:
return False return None
return tuple(int(part) for part in match.groups()) >= (1, 4, 0) return tuple(int(part) for part in match.groups())
@classmethod
def _agent_supports_mapping_inventory(cls, version: Any) -> bool:
parsed = cls._agent_version_tuple(version)
return bool(parsed is not None and parsed >= (1, 3, 0))
@classmethod
def _agent_supports_batch(cls, version: Any) -> bool:
parsed = cls._agent_version_tuple(version)
return bool(parsed is not None and parsed >= (1, 4, 0))
@staticmethod @staticmethod
@@ -1068,6 +1078,14 @@ class VmixAgentHub:
if not assignment_id or not match_id: if not assignment_id or not match_id:
return {"ok": False, "reason": "no_match_assignment", "device_id": device_id} return {"ok": False, "reason": "no_match_assignment", "device_id": device_id}
if not fingerprint: if not fingerprint:
if agent_version and not self._agent_supports_mapping_inventory(agent_version):
return {
"ok": False,
"reason": "agent_mapping_unsupported",
"device_id": device_id,
"agent_version": agent_version,
"required_agent_version": "1.3.0",
}
return {"ok": False, "reason": "no_project_inventory", "device_id": device_id} return {"ok": False, "reason": "no_project_inventory", "device_id": device_id}
if not vmix_connected: if not vmix_connected:
return {"ok": False, "reason": "vmix_not_connected", "device_id": device_id} return {"ok": False, "reason": "vmix_not_connected", "device_id": device_id}
@@ -2354,6 +2372,8 @@ class VmixAgentHub:
"device_id": row.device_uuid, "device_id": row.device_uuid,
"name": row.name or row.hostname or row.device_uuid, "name": row.name or row.hostname or row.device_uuid,
"hostname": row.hostname, "hostname": row.hostname,
"agent_version": row.agent_version or "",
"mapping_supported": self._agent_supports_mapping_inventory(row.agent_version),
"online": row.device_uuid in self._live, "online": row.device_uuid in self._live,
"vmix_connected": bool(row.vmix_connected), "vmix_connected": bool(row.vmix_connected),
"vmix_version": row.vmix_version, "vmix_version": row.vmix_version,

View File

@@ -2205,13 +2205,22 @@
const profiles = state.mappingProfiles?.profiles || []; const profiles = state.mappingProfiles?.profiles || [];
const profile = state.mappingActiveProfile; const profile = state.mappingActiveProfile;
const usableDevices = devices.filter((item) => item.project_fingerprint); const usableDevices = devices.filter((item) => item.project_fingerprint);
const deviceRows = devices.length ? devices.map((item) => ` const deviceRows = devices.length ? devices.map((item) => {
const agentVersion = String(item.agent_version || "").trim();
const tooOldForMapping = Boolean(agentVersion && item.mapping_supported === false);
const mappingState = tooOldForMapping
? `<b style="color:#f59e0b">Agent ${escapeHtml(agentVersion)} слишком старый для Mapping · нужен 1.3.0+</b>`
: (item.mapping
? `Mapping: <b>${escapeHtml(item.mapping.name)}</b> · v${Number(item.mapping.version || 1)}`
: (item.project_fingerprint ? "Mapping не назначен" : "Ожидание структуры vMix"));
return `
<article class="hockey-mapping-device ${item.online ? "is-online" : ""}"> <article class="hockey-mapping-device ${item.online ? "is-online" : ""}">
<div><strong>${escapeHtml(item.name || item.device_id)}</strong><code>${escapeHtml(item.device_id)}</code></div> <div><strong>${escapeHtml(item.name || item.device_id)}</strong><code>${escapeHtml(item.device_id)}</code></div>
<span>${item.vmix_connected ? "vMix подключён" : "vMix не найден"}</span> <span>${item.vmix_connected ? "vMix подключён" : "vMix не найден"}${agentVersion ? ` · Agent ${escapeHtml(agentVersion)}` : ""}</span>
<span>${Number(item.input_count || 0)} Inputs · ${Number(item.field_count || 0)} полей</span> <span>${Number(item.input_count || 0)} Inputs · ${Number(item.field_count || 0)} полей</span>
<span>${item.mapping ? `Mapping: <b>${escapeHtml(item.mapping.name)}</b> · v${Number(item.mapping.version || 1)}` : (item.project_fingerprint ? "Mapping не назначен" : "Ожидание структуры vMix")}</span> <span>${mappingState}</span>
</article>`).join("") : `<div class="hockey-directory-empty">Agent пока не передал структуру ни одного vMix.</div>`; </article>`;
}).join("") : `<div class="hockey-directory-empty">Agent пока не передал структуру ни одного vMix.</div>`;
const selectedDeviceId = String(localStorage.getItem("hockey.vmix.selected_device") || "").trim(); const selectedDeviceId = String(localStorage.getItem("hockey.vmix.selected_device") || "").trim();
const selectedDevice = devices.find((item) => String(item.device_id || "") === selectedDeviceId) || null; const selectedDevice = devices.find((item) => String(item.device_id || "") === selectedDeviceId) || null;
@@ -2242,7 +2251,14 @@
editor = `<div class="hockey-directory-empty">Выберите профиль слева или создайте новый из подключённого vMix.</div>`; editor = `<div class="hockey-directory-empty">Выберите профиль слева или создайте новый из подключённого vMix.</div>`;
} else { } else {
const currentInput = mappingCurrentInput(profile); const currentInput = mappingCurrentInput(profile);
const matchingDevices = devices.filter((item) => item.project_fingerprint && item.project_fingerprint === profile.project_fingerprint && item.online && item.vmix_connected); const profileId = Number(profile.id || 0);
const matchingDevices = devices.filter((item) => {
if (!item.online || !item.vmix_connected || item.mapping_supported === false) return false;
const exactFingerprint = Boolean(item.project_fingerprint && item.project_fingerprint === profile.project_fingerprint);
const resolvedProfileId = Number(item.mapping?.source_profile_id || item.mapping?.id || 0);
return exactFingerprint || (profileId > 0 && resolvedProfileId === profileId);
});
const onlineOldAgents = devices.filter((item) => item.online && item.vmix_connected && item.mapping_supported === false);
if (!state.mappingTestDeviceId || !matchingDevices.some((item) => item.device_id === state.mappingTestDeviceId)) state.mappingTestDeviceId = matchingDevices[0]?.device_id || ""; if (!state.mappingTestDeviceId || !matchingDevices.some((item) => item.device_id === state.mappingTestDeviceId)) state.mappingTestDeviceId = matchingDevices[0]?.device_id || "";
const catalog = mappingDataCatalog(); const catalog = mappingDataCatalog();
const gameReady = Boolean(catalog.game && (catalog.game.external_id || catalog.game.id)); const gameReady = Boolean(catalog.game && (catalog.game.external_id || catalog.game.id));
@@ -2260,7 +2276,7 @@
</div> </div>
<div class="hockey-map-livebar ${gameReady ? "is-ready" : ""}"> <div class="hockey-map-livebar ${gameReady ? "is-ready" : ""}">
<div><span>${gameReady ? "● ЖИВОЙ ПРИМЕР" : "○ НЕТ ТЕСТОВОГО МАТЧА"}</span><strong>${escapeHtml(catalog.gameLabel)}</strong><small>${gameReady ? "Значения получены через SQL Data Sources из PostgreSQL." : "Выберите матч; системный context game_id заполнится автоматически."}</small></div> <div><span>${gameReady ? "● ЖИВОЙ ПРИМЕР" : "○ НЕТ ТЕСТОВОГО МАТЧА"}</span><strong>${escapeHtml(catalog.gameLabel)}</strong><small>${gameReady ? "Значения получены через SQL Data Sources из PostgreSQL." : "Выберите матч; системный context game_id заполнится автоматически."}</small></div>
<label>Тестовый Agent<select data-map-test-device><option value="">— нет подходящего online Agent —</option>${matchingDevices.map((item) => `<option value="${escapeHtml(item.device_id)}" ${item.device_id === state.mappingTestDeviceId ? "selected" : ""}>${escapeHtml(item.name || item.device_id)}</option>`).join("")}</select></label> <label>Тестовый Agent<select data-map-test-device><option value="">${onlineOldAgents.length ? `— Agent ${escapeHtml(onlineOldAgents[0].agent_version || "")} слишком старый, нужен 1.3.0+ —` : "— нет подходящего online Agent —"}</option>${matchingDevices.map((item) => `<option value="${escapeHtml(item.device_id)}" ${item.device_id === state.mappingTestDeviceId ? "selected" : ""}>${escapeHtml(item.name || item.device_id)}${item.agent_version ? ` · v${escapeHtml(item.agent_version)}` : ""}</option>`).join("")}</select></label>
</div> </div>
<div class="hockey-map-visual-workspace"> <div class="hockey-map-visual-workspace">
${mappingDataBrowser(profile, currentInput)} ${mappingDataBrowser(profile, currentInput)}

View File

@@ -0,0 +1,35 @@
from pathlib import Path
import asyncio
from hockey_data.agent_bridge import VmixAgentHub
ROOT = Path(__file__).resolve().parents[1]
JS = (ROOT / 'hockey_data/static/admin-directories.js').read_text(encoding='utf-8')
BRIDGE = (ROOT / 'hockey_data/agent_bridge.py').read_text(encoding='utf-8')
APP = (ROOT / 'app.py').read_text(encoding='utf-8')
def test_agent_version_gates():
assert VmixAgentHub._agent_supports_mapping_inventory('1.2.4') is False
assert VmixAgentHub._agent_supports_mapping_inventory('1.3.0') is True
assert VmixAgentHub._agent_supports_mapping_inventory('1.4.0') is True
assert VmixAgentHub._agent_supports_batch('1.3.9') is False
assert VmixAgentHub._agent_supports_batch('1.4.0') is True
def test_mapping_device_payload_exposes_version_support():
assert '"agent_version": row.agent_version or ""' in BRIDGE
assert '"mapping_supported": self._agent_supports_mapping_inventory(row.agent_version)' in BRIDGE
assert '"reason": "agent_mapping_unsupported"' in BRIDGE
assert '"required_agent_version": "1.3.0"' in BRIDGE
def test_mapping_editor_uses_resolved_profile_not_only_exact_fingerprint():
assert 'resolvedProfileId === profileId' in JS
assert 'item.mapping_supported === false' in JS
assert 'слишком старый для Mapping' in JS
assert 'нужен 1.3.0+' in JS
def test_runtime_version():
assert 'BUILD_VERSION = "2026.08.24.15"' in APP