тест 10
This commit is contained in:
3
app.py
3
app.py
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)}
|
||||||
|
|||||||
35
tests/test_build112_agent_mapping_version.py
Normal file
35
tests/test_build112_agent_mapping_version.py
Normal 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
|
||||||
Reference in New Issue
Block a user