1. поправлено работа с таймерами в vMix
2. поправлено немного Заготовки
This commit is contained in:
@@ -8,9 +8,10 @@ APP = (ROOT / "app.py").read_text(encoding="utf-8")
|
||||
def test_game_countdown_resume_is_reseeded_before_start():
|
||||
block = APP_JS.split("async function syncActiveVmixGameCountdown", 1)[1].split("function penaltyMirrorKey", 1)[0]
|
||||
assert '["timer_start", "timer_restart", "timer_resume"].includes(eventName)' in block
|
||||
set_pos = block.index('Function: "SetCountdown"')
|
||||
start_pos = block.index('Function: "StartCountdown"')
|
||||
assert set_pos < start_pos
|
||||
assert 'vmixCountdownSyncCommands(input, selectedName, valueProvider, "start")' in block
|
||||
helper = APP_JS.split("function vmixCountdownSyncCommands", 1)[1].split("function buildShortcutRuntimeContext", 1)[0]
|
||||
start_branch = helper.rsplit("return [", 1)[1]
|
||||
assert start_branch.index('Function: "SetCountdown"') < start_branch.index('Function: "StartCountdown"')
|
||||
assert 'eventName === "timer_resume"' not in block
|
||||
|
||||
|
||||
@@ -18,8 +19,9 @@ def test_hockey_timer_shortcut_reseeds_game_and_penalty_on_resume():
|
||||
block = APP_JS.split('case "hockey_vmix_timers_start":', 1)[1].split('case "delay":', 1)[0]
|
||||
# Resume must no longer have a StartCountdown-only branch.
|
||||
assert '} else if (action === "resume") {' not in block
|
||||
assert block.count('Function: "SetCountdown"') >= 2
|
||||
assert block.count('Function: "StartCountdown"') >= 2
|
||||
assert block.count('vmixCountdownSyncCommands(') >= 2
|
||||
assert '() => gameTimerState.currentMs' in block
|
||||
assert '() => event.remainingMs' in block
|
||||
|
||||
|
||||
def test_penalty_rebalance_pairs_every_start_with_current_web_value():
|
||||
|
||||
@@ -27,20 +27,21 @@ def test_penalty_vmix_display_uses_only_started_or_paused_active_penalties():
|
||||
|
||||
def test_main_countdown_start_pause_stop_all_hard_sync_web_value():
|
||||
block = _block("async function syncActiveVmixGameCountdown", "function penaltyMirrorKey")
|
||||
start = block.split('if (["timer_start", "timer_restart", "timer_resume"].includes(eventName))', 1)[1].split('} else if (eventName === "timer_pause")', 1)[0]
|
||||
assert start.index('Function: "SetCountdown"') < start.index('Function: "StartCountdown"')
|
||||
pause = block.split('eventName === "timer_pause"', 1)[1].split('} else if (["timer_stop", "timer_finished"]', 1)[0]
|
||||
assert pause.index('Function: "PauseCountdown"') < pause.index('Function: "SetCountdown"')
|
||||
stop = block.split('["timer_stop", "timer_finished"].includes(eventName)', 1)[1].split('} else if (["timer_reset"', 1)[0]
|
||||
assert stop.index('Function: "StopCountdown"') < stop.index('Function: "SetCountdown"')
|
||||
assert 'vmixCountdownSyncCommands(input, selectedName, valueProvider, "start")' in block
|
||||
assert 'vmixCountdownSyncCommands(input, selectedName, valueProvider, "pause")' in block
|
||||
assert 'vmixCountdownSyncCommands(input, selectedName, valueProvider, "stop")' in block
|
||||
helper = _block("function vmixCountdownSyncCommands", "function buildShortcutRuntimeContext")
|
||||
start_branch = helper.rsplit("return [", 1)[1]
|
||||
assert start_branch.index('Function: "SuspendCountdown"') < start_branch.index('Function: "SetCountdown"')
|
||||
assert start_branch.index('Function: "SetCountdown"') < start_branch.index('Function: "StartCountdown"')
|
||||
|
||||
|
||||
def test_penalty_pause_freezes_before_reseed_and_start_reseeds_before_run():
|
||||
block = _block("async function rebalanceVmixPenaltyTargets", "function finishActionMatchesSource")
|
||||
assert 'stopCommands.push({ Function: "PauseCountdown"' in block
|
||||
assert 'stopCommands.push({ Function: "SuspendCountdown"' in block
|
||||
assert 'setCommands.push({ Function: "SetCountdown"' in block
|
||||
assert 'runCommands.push({ Function: "StartCountdown"' in block
|
||||
assert "Never StartCountdown for a prepared item" in block
|
||||
assert "prepared/paused penalty" in block
|
||||
|
||||
|
||||
def test_combined_shortcut_changes_web_first_then_synchronizes_vmix():
|
||||
@@ -48,8 +49,9 @@ def test_combined_shortcut_changes_web_first_then_synchronizes_vmix():
|
||||
web_game = block.index("if (step.start_web_game)")
|
||||
vmix_game = block.index("if (step.sync_vmix_game && step.game_vmix_input)")
|
||||
assert web_game < vmix_game
|
||||
assert block.index('Function: "PauseCountdown"') < block.index('Function: "SetCountdown"', block.index('Function: "PauseCountdown"'))
|
||||
assert 'Function: "StartCountdown"' in block
|
||||
assert 'vmixCountdownSyncCommands(' in block
|
||||
assert '() => gameTimerState.currentMs' in block
|
||||
assert '() => event.remainingMs' in block
|
||||
|
||||
|
||||
def test_build95_runtime_version():
|
||||
|
||||
67
tests/test_build98_timer_countdown_sync_fixed_pin.py
Normal file
67
tests/test_build98_timer_countdown_sync_fixed_pin.py
Normal file
@@ -0,0 +1,67 @@
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
from ui_builder.auth import EditorAuthManager
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
APP_JS = (ROOT / "ui_builder/static/app.js").read_text(encoding="utf-8")
|
||||
APP = (ROOT / "app.py").read_text(encoding="utf-8")
|
||||
AUTH = (ROOT / "ui_builder/auth.py").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _block(start: str, end: str) -> str:
|
||||
return APP_JS.split(start, 1)[1].split(end, 1)[0]
|
||||
|
||||
|
||||
def test_vmix_queue_resolves_countdown_values_at_actual_send_time():
|
||||
compact = _block("function compactVmixCommand", "function currentRuntimeVmixDeviceId")
|
||||
assert 'typeof rawValue === "function" ? rawValue() : rawValue' in compact
|
||||
sender = _block("async function sendRuntimeVmixSequence", "function splitVmixInputs")
|
||||
assert 'const rawCommands = typeof commands === "function" ? commands() : commands;' in sender
|
||||
assert 'const clean = (rawCommands || []).map(compactVmixCommand)' in sender
|
||||
|
||||
|
||||
def test_countdown_start_is_deterministic_suspend_set_start():
|
||||
helper = _block("function vmixCountdownSyncCommands", "function buildShortcutRuntimeContext")
|
||||
start_branch = helper.split('return [', 3)[-1]
|
||||
assert start_branch.index('Function: "SuspendCountdown"') < start_branch.index('Function: "SetCountdown"')
|
||||
assert start_branch.index('Function: "SetCountdown"') < start_branch.index('Function: "StartCountdown"')
|
||||
assert 'Value: currentValue' in helper
|
||||
|
||||
|
||||
def test_pause_uses_suspend_not_toggle_pausecountdown():
|
||||
helper = _block("function vmixCountdownSyncCommands", "function buildShortcutRuntimeContext")
|
||||
pause = helper.split('if (action === "pause")', 1)[1].split('return [', 1)[1].split('];', 1)[0]
|
||||
assert 'Function: "SuspendCountdown"' in pause
|
||||
assert 'Function: "PauseCountdown"' not in pause
|
||||
assert pause.index('Function: "SuspendCountdown"') < pause.index('Function: "SetCountdown"')
|
||||
|
||||
|
||||
def test_penalty_rebalance_hard_syncs_running_countdown_and_pauses_stably():
|
||||
block = _block("async function rebalanceVmixPenaltyTargets", "function finishActionMatchesSource")
|
||||
assert 'stopCommands.push({ Function: "SuspendCountdown"' in block
|
||||
assert 'Value: () => vmixCountdownValue(entry.event.remainingMs)' in block
|
||||
assert 'runCommands.push({ Function: "StartCountdown"' in block
|
||||
assert 'Function: "PauseCountdown"' not in block
|
||||
|
||||
|
||||
def test_running_penalty_is_preferred_over_paused_shorter_penalty():
|
||||
sorted_block = _block("function sortedPenaltyEntries", "function penaltyLocalAdvantageSide")
|
||||
assert 'const runningOrder = Number(Boolean(b.event?.running)) - Number(Boolean(a.event?.running));' in sorted_block
|
||||
display = _block("function penaltyDisplayEntriesByTargetSide", "function rememberPenaltyAdvantagePlan")
|
||||
assert 'const runningOrder = Number(Boolean(b.event?.running)) - Number(Boolean(a.event?.running));' in display
|
||||
|
||||
|
||||
def test_fixed_pin_1993_replaces_daily_login(tmp_path, monkeypatch):
|
||||
monkeypatch.delenv("EDITOR_FIXED_PIN", raising=False)
|
||||
auth = EditorAuthManager(tmp_path)
|
||||
assert auth.settings["fixed_pin"] == "1993"
|
||||
# The old daily algorithm remains diagnostic-only; for 24.08.2026 its
|
||||
# default result is 6225, but login validation no longer uses it.
|
||||
assert auth.daily_pin(datetime(2026, 8, 24, 12, 0, tzinfo=ZoneInfo("Europe/Moscow"))) == "6225"
|
||||
assert 'valid = hmac.compare_digest(supplied, str(self.settings["fixed_pin"]))' in AUTH
|
||||
|
||||
|
||||
def test_build98_runtime_version():
|
||||
assert 'BUILD_VERSION = "2026.08.24.1"' in APP
|
||||
95
tests/test_build99_prepared_title_editing.py
Normal file
95
tests/test_build99_prepared_title_editing.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import asyncio
|
||||
import json
|
||||
import types
|
||||
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, VmixPreparedTitle
|
||||
from tests.support import LocalTestDatabase
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
APP_JS = (ROOT / "ui_builder/static/app.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_build99_frontend_and_routes_exist():
|
||||
assert 'preparedTitleEditingId: ""' in APP_JS
|
||||
assert 'data-prepared-edit=' in APP_JS
|
||||
assert 'method: "PUT"' in APP_JS
|
||||
assert '@router.put("/api/hockey/prepared-titles/{prepared_id}")' in BRIDGE
|
||||
assert '"Function": "SetInputName"' in BRIDGE
|
||||
assert 'return f"Заготовка {number}"' in BRIDGE
|
||||
assert 'BUILD_VERSION = "2026.08.24.2"' in APP
|
||||
|
||||
|
||||
def test_blank_name_gets_number_and_existing_clone_is_editable(tmp_path):
|
||||
database = LocalTestDatabase(tmp_path / "prepared99.sqlite3")
|
||||
database.create_all()
|
||||
hub = VmixAgentHub(database) # type: ignore[arg-type]
|
||||
user = HockeyUser(id="prep99", login="prep99", display_name="Prep99")
|
||||
source = {
|
||||
"key": "source-key",
|
||||
"number": "12",
|
||||
"title": "COMPARE",
|
||||
"type": "GT",
|
||||
"fields": [{"name": "Name.Text", "type": "text", "index": "0"}],
|
||||
}
|
||||
inventory = {"inputs": [source]}
|
||||
with database.session() as session:
|
||||
session.add(VmixDevice(
|
||||
device_uuid="prep-device",
|
||||
device_secret_hash="x" * 64,
|
||||
name="Prep device",
|
||||
wfl_user_id=user.id,
|
||||
is_active_for_account=True,
|
||||
vmix_connected=True,
|
||||
current_match_external_id="9001",
|
||||
project_inventory_json=json.dumps(inventory),
|
||||
))
|
||||
|
||||
calls = []
|
||||
|
||||
async def fake_sequence(self, _user, commands, *, device_id="", session_token="", timeout=0, **kwargs):
|
||||
calls.append(commands)
|
||||
if commands and commands[0].get("Function") == "CreateVirtualInput":
|
||||
cloned = {"inputs": [source, {**source, "key": "clone-key", "number": "13", "title": "COMPARE"}]}
|
||||
with database.session() as session:
|
||||
device = session.scalar(select(VmixDevice).where(VmixDevice.device_uuid == "prep-device"))
|
||||
device.project_inventory_json = json.dumps(cloned)
|
||||
return {"ok": True, "device_id": device_id or "prep-device", "match_id": "9001"}
|
||||
|
||||
hub.run_vmix_sequence_for_user = types.MethodType(fake_sequence, hub)
|
||||
created = asyncio.run(hub.create_prepared_title(user, SimpleNamespace(
|
||||
name="",
|
||||
device_id="prep-device",
|
||||
session_token="",
|
||||
source_input_key="source-key",
|
||||
source_input_number="12",
|
||||
source_input_title="COMPARE",
|
||||
source_kind="manual",
|
||||
source_ref="",
|
||||
field_values={"Name.Text": {"type": "text", "value": "Иванов"}},
|
||||
)))
|
||||
assert created["name"] == "Заготовка 1"
|
||||
assert created["clone_input"]["title"] == "Заготовка 1"
|
||||
assert any(c.get("Function") == "SetInputName" and c.get("Value") == "Заготовка 1" for c in calls[1])
|
||||
|
||||
updated = asyncio.run(hub.update_prepared_title(created["id"], user, SimpleNamespace(
|
||||
name="Сравнение вратарей",
|
||||
device_id="prep-device",
|
||||
session_token="",
|
||||
field_values={"Name.Text": {"type": "text", "value": "Петров"}},
|
||||
)))
|
||||
assert updated["name"] == "Сравнение вратарей"
|
||||
assert updated["field_values"]["Name.Text"]["value"] == "Петров"
|
||||
assert any(c.get("Function") == "SetInputName" and c.get("Value") == "Сравнение вратарей" for c in calls[-1])
|
||||
with database.session() as session:
|
||||
row = session.get(VmixPreparedTitle, created["id"])
|
||||
assert row is not None
|
||||
assert row.name == "Сравнение вратарей"
|
||||
assert row.clone_input_title == "Сравнение вратарей"
|
||||
@@ -106,7 +106,7 @@ def test_shortcut_editor_layout_fixes_add_step_buttons_and_fullscreen_modal() ->
|
||||
|
||||
def test_hockey_timer_shortcut_supports_native_countdown_and_legacy_text_mirror() -> None:
|
||||
app_js = Path("ui_builder/static/app.js").read_text(encoding="utf-8")
|
||||
assert 'Function: "PauseCountdown"' in app_js
|
||||
assert 'Function: "SuspendCountdown"' in app_js
|
||||
assert 'Function: "StopCountdown"' in app_js
|
||||
assert 'hockey_timer_command' in app_js
|
||||
assert 'game_vmix_mode' in app_js
|
||||
@@ -183,8 +183,11 @@ def test_hockey_timer_targets_persist_selected_names_and_finish_actions(tmp_path
|
||||
def test_hockey_countdown_uses_selected_name_for_game_and_penalty_targets() -> None:
|
||||
app_js = Path("ui_builder/static/app.js").read_text(encoding="utf-8")
|
||||
|
||||
assert 'Function: "SetCountdown", Input: step.game_vmix_input, SelectedName: step.game_vmix_selected_name' in app_js
|
||||
assert 'Function: "SetCountdown", Input: target.input, SelectedName: target.selected_name' in app_js
|
||||
assert 'function vmixCountdownSyncCommands(input, selectedName' in app_js
|
||||
assert 'SelectedName: String(selectedName || "").trim()' in app_js
|
||||
assert 'step.game_vmix_selected_name, () => gameTimerState.currentMs' in app_js
|
||||
assert 'target.input, target.selected_name, () => event.remainingMs' in app_js
|
||||
assert 'Input: target.input, SelectedName: target.selected_name' in app_js
|
||||
assert 'vmixTextSelectedNameOptions' in app_js
|
||||
assert 'data-penalty-target-field="selected_name"' in app_js
|
||||
assert 'data-finish-action-add' in app_js
|
||||
@@ -249,7 +252,7 @@ def test_build90_native_countdown_avoids_per_second_transport() -> None:
|
||||
app_js = Path("ui_builder/static/app.js").read_text(encoding="utf-8")
|
||||
assert 'game_vmix_mode !== "countdown"' in app_js
|
||||
assert 'const countdownMode = step.penalty_vmix_mode === "countdown"' in app_js
|
||||
assert 'Function: "PauseCountdown"' in app_js
|
||||
assert 'Function: "SuspendCountdown"' in app_js
|
||||
assert 'Function: "StopCountdown"' in app_js
|
||||
assert 'signal: controller.signal' in app_js
|
||||
assert 'vmixCommandQueue: Promise.resolve()' in app_js
|
||||
|
||||
Reference in New Issue
Block a user