тест 2
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
|
||||
|
||||
BASE_DIR = Path(__file__).resolve().parent
|
||||
BUILD_VERSION = "2026.08.24.6"
|
||||
BUILD_VERSION = "2026.08.24.7"
|
||||
# compatibility: BUILD_VERSION = "2026.08.24.6"
|
||||
# compatibility: BUILD_VERSION = "2026.08.24.5"
|
||||
# compatibility: BUILD_VERSION = "2026.08.24.4"
|
||||
# compatibility: BUILD_VERSION = "2026.08.24.2"
|
||||
|
||||
@@ -22,7 +22,8 @@ def test_timer_start_stop_protocol_is_minimal():
|
||||
helper = _block("function vmixCountdownSyncCommands", "function buildShortcutRuntimeContext")
|
||||
assert 'if (action === "stop" || action === "pause")' in helper
|
||||
stop_branch = helper.split('if (action === "stop" || action === "pause")', 1)[1].split('return [', 1)[1].split('];', 1)[0]
|
||||
assert stop_branch.index('Function: "StopCountdown"') < stop_branch.index('Function: "SetCountdown"')
|
||||
assert 'Function: "StopCountdown"' in stop_branch
|
||||
assert 'Function: "SetCountdown"' not in stop_branch
|
||||
start_branch = helper.rsplit('return [', 1)[1]
|
||||
assert start_branch.index('Function: "SetCountdown"') < start_branch.index('Function: "StartCountdown"')
|
||||
assert 'SuspendCountdown' not in helper
|
||||
|
||||
@@ -89,7 +89,6 @@ def test_timer_fast_backend_uses_one_no_ack_batch_and_bypasses_mapping_lock(tmp_
|
||||
user,
|
||||
[
|
||||
{"Function": "StopCountdown", "Input": "SCORE", "SelectedName": "TIME.Text"},
|
||||
{"Function": "SetCountdown", "Input": "SCORE", "SelectedName": "TIME.Text", "Value": "00:18:41"},
|
||||
],
|
||||
delivery_mode="timer-fast",
|
||||
),
|
||||
@@ -97,16 +96,21 @@ def test_timer_fast_backend_uses_one_no_ack_batch_and_bypasses_mapping_lock(tmp_
|
||||
)
|
||||
|
||||
batches = [item for item in ws.sent if item.get("type") == "vmix.batch"]
|
||||
assert len(batches) == 2
|
||||
singles = [item for item in ws.sent if item.get("type") == "vmix.command"]
|
||||
assert len(batches) == 1
|
||||
assert [item["Function"] for item in batches[0]["commands"]] == ["SetCountdown", "StartCountdown"]
|
||||
assert [item["Function"] for item in batches[1]["commands"]] == ["StopCountdown", "SetCountdown"]
|
||||
assert not [item for item in ws.sent if item.get("type") == "vmix.command"]
|
||||
for payload in (result, stop_result):
|
||||
assert payload["ok"] is True
|
||||
assert payload["transport"] == "timer-batch-no-ack"
|
||||
assert payload["confirmation"] == "not_waited"
|
||||
assert payload["applied"] == 2
|
||||
assert all(row["ack_waited"] is False for row in payload["results"])
|
||||
assert len(singles) == 1
|
||||
assert singles[0]["command"]["Function"] == "StopCountdown"
|
||||
assert result["ok"] is True
|
||||
assert result["transport"] == "timer-batch-no-ack"
|
||||
assert result["confirmation"] == "not_waited"
|
||||
assert result["applied"] == 2
|
||||
assert all(row["ack_waited"] is False for row in result["results"])
|
||||
assert stop_result["ok"] is True
|
||||
assert stop_result["transport"] == "timer-no-ack"
|
||||
assert stop_result["confirmation"] == "not_waited"
|
||||
assert stop_result["applied"] == 1
|
||||
assert all(row["ack_waited"] is False for row in stop_result["results"])
|
||||
assert not hub._pending_commands
|
||||
|
||||
asyncio.run(scenario())
|
||||
|
||||
37
tests/test_build104_timer_stop_no_rollback.py
Normal file
37
tests/test_build104_timer_stop_no_rollback.py
Normal file
@@ -0,0 +1,37 @@
|
||||
from pathlib import Path
|
||||
|
||||
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")
|
||||
|
||||
|
||||
def _block(start: str, end: str) -> str:
|
||||
return APP_JS.split(start, 1)[1].split(end, 1)[0]
|
||||
|
||||
|
||||
def test_pause_stop_sends_only_stopcountdown():
|
||||
helper = _block("function vmixCountdownSyncCommands", "function buildShortcutRuntimeContext")
|
||||
branch = helper.split('if (action === "stop" || action === "pause")', 1)[1].split('if (action === "set")', 1)[0]
|
||||
assert 'Function: "StopCountdown"' in branch
|
||||
assert 'Function: "SetCountdown"' not in branch
|
||||
|
||||
|
||||
def test_explicit_edit_still_can_seed_stopped_clock():
|
||||
helper = _block("function vmixCountdownSyncCommands", "function buildShortcutRuntimeContext")
|
||||
branch = helper.split('if (action === "set")', 1)[1].split('return [', 2)[1]
|
||||
assert 'Function: "StopCountdown"' in branch
|
||||
assert 'Function: "SetCountdown"' in branch
|
||||
sync = _block("async function syncActiveVmixGameCountdown", "function penaltyMirrorKey")
|
||||
assert 'timerState.running ? "start" : "set"' in sync
|
||||
|
||||
|
||||
def test_penalty_pause_preserves_native_vmix_frozen_value():
|
||||
rebalance = _block("async function rebalanceVmixPenaltyTargets", "function finishActionMatchesSource")
|
||||
assert "preservePausedCountdown = false" in rebalance
|
||||
assert "if (!preservePausedCountdown || assignmentChanged)" in rebalance
|
||||
control = _block("function controlHockeyPenalty", "function formatHockeyPenaltyTime")
|
||||
assert 'preservePausedCountdown: command === "pause"' in control
|
||||
|
||||
|
||||
def test_build104_runtime_version():
|
||||
assert 'BUILD_VERSION = "2026.08.24.7"' in APP
|
||||
@@ -30,12 +30,11 @@ def test_countdown_start_is_simple_set_start():
|
||||
assert 'Value: currentValue' in helper
|
||||
|
||||
|
||||
def test_pause_and_stop_use_stop_then_set():
|
||||
def test_pause_and_stop_freeze_without_reseed():
|
||||
helper = _block("function vmixCountdownSyncCommands", "function buildShortcutRuntimeContext")
|
||||
branch = helper.split('if (action === "stop" || action === "pause")', 1)[1].split('return [', 1)[1].split('];', 1)[0]
|
||||
assert 'Function: "StopCountdown"' in branch
|
||||
assert 'Function: "SetCountdown"' in branch
|
||||
assert branch.index('Function: "StopCountdown"') < branch.index('Function: "SetCountdown"')
|
||||
assert 'Function: "SetCountdown"' not in branch
|
||||
assert 'Function: "SuspendCountdown"' not in branch
|
||||
assert 'Function: "PauseCountdown"' not in branch
|
||||
|
||||
|
||||
@@ -4163,6 +4163,16 @@ function startCustomTooltips() {
|
||||
};
|
||||
if (!target.Input || !target.SelectedName) return [];
|
||||
if (action === "stop" || action === "pause") {
|
||||
// BUILD104: never reseed immediately after StopCountdown. vMix has already
|
||||
// frozen its native countdown at the exact frame it received StopCountdown;
|
||||
// a following SetCountdown from the browser can be slightly older and makes
|
||||
// the on-air clock visibly jump backwards.
|
||||
return [
|
||||
{ Function: "StopCountdown", ...target },
|
||||
];
|
||||
}
|
||||
if (action === "set") {
|
||||
// Explicit edits/reset still need to write the Runtime value while stopped.
|
||||
return [
|
||||
{ Function: "StopCountdown", ...target },
|
||||
{ Function: "SetCountdown", ...target, Value: currentValue },
|
||||
@@ -4749,7 +4759,7 @@ function startCustomTooltips() {
|
||||
commands.push(...vmixCountdownSyncCommands(input, selectedName, valueProvider, "stop"));
|
||||
} else if (["timer_reset", "timer_set_time", "timer_add_time", "timer_subtract_time"].includes(eventName)) {
|
||||
commands.push(...vmixCountdownSyncCommands(
|
||||
input, selectedName, valueProvider, timerState.running ? "start" : "pause"
|
||||
input, selectedName, valueProvider, timerState.running ? "start" : "set"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -5019,7 +5029,7 @@ function startCustomTooltips() {
|
||||
return `${String(step?.id || "")}:${side}:${String(target?.id || "")}`;
|
||||
}
|
||||
|
||||
async function rebalanceVmixPenaltyTargets({ force = false, hideUnused = true } = {}) {
|
||||
async function rebalanceVmixPenaltyTargets({ force = false, hideUnused = true, preservePausedCountdown = false } = {}) {
|
||||
const outCommands = [];
|
||||
const stopCommands = [];
|
||||
const setCommands = [];
|
||||
@@ -5077,10 +5087,13 @@ function startCustomTooltips() {
|
||||
runCommands.push({ Function: "StartCountdown", Input: target.input, SelectedName: target.selected_name });
|
||||
}
|
||||
} else if (force || assignmentChanged || previous?.running !== false) {
|
||||
// Freeze the title and seed the exact Runtime value, but never run a
|
||||
// prepared/paused penalty until the web event itself is running.
|
||||
// BUILD104: a normal Pause of a prepared/paused penalty freezes the native vMix countdown only.
|
||||
// Re-seeding after StopCountdown can visibly roll the clock back.
|
||||
// Explicit reset/set-time/assignment changes may still seed a value.
|
||||
stopCommands.push({ Function: "StopCountdown", Input: target.input, SelectedName: target.selected_name });
|
||||
setCommands.push({ Function: "SetCountdown", Input: target.input, SelectedName: target.selected_name, Value: () => vmixCountdownValue(entry.event.remainingMs) });
|
||||
if (!preservePausedCountdown || assignmentChanged) {
|
||||
setCommands.push({ Function: "SetCountdown", Input: target.input, SelectedName: target.selected_name, Value: () => vmixCountdownValue(entry.event.remainingMs) });
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assignedMirrorKeys.add(eventKey);
|
||||
@@ -7220,8 +7233,11 @@ function openTimerQuickEditor(focusActionId = "") {
|
||||
persistHockeyBoard(component, board, true);
|
||||
refreshHockeyBoardNodes(component);
|
||||
if (options.syncVmix !== false && state.activeHockeyVmixTimerSteps.size && ["start", "pause", "reset", "set_time"].includes(command)) {
|
||||
rebalanceVmixPenaltyTargets({ force: true, hideUnused: true })
|
||||
.catch((error) => console.error("Penalty countdown state sync error", error));
|
||||
rebalanceVmixPenaltyTargets({
|
||||
force: true,
|
||||
hideUnused: true,
|
||||
preservePausedCountdown: command === "pause",
|
||||
}).catch((error) => console.error("Penalty countdown state sync error", error));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -13852,7 +13868,7 @@ function renderHockeyPenaltyDashboard(node, component, runtime) {
|
||||
<div class="shortcut-finish-action-list">${timerFinishActionRows(step)}</div>
|
||||
|
||||
<div class="shortcut-vmix-inventory-note"><span>${escapeHtml(shortcutInventoryLabel())}</span><small>Для каждого countdown теперь обязательно выбирается конкретный Text / SelectedName. Это исключает отправку времени в первый текстовый элемент по умолчанию.</small></div>
|
||||
<p class="shortcut-step-note">Режим <b>Countdown vMix</b>: веб-таймер является главным. Start сразу меняет состояние Runtime и независимо отправляет <code>SetCountdown → StartCountdown</code> в vMix; Pause/Stop сразу останавливают Runtime и независимо отправляют <code>StopCountdown → SetCountdown</code>. Ошибка Agent не блокирует управление таймером. Каждую секунду значение в vMix не передаётся. <b>Text mirror</b> оставлен только для совместимости со старыми титрами. Для верхнего счёта используется одна penalty-плашка: при реальном большинстве она показывает ближайшее изменение численного состава; при чистом равном обоюдном удалении сама по себе не появляется.</p>
|
||||
<p class="shortcut-step-note">Режим <b>Countdown vMix</b>: веб-таймер является главным. Start сразу меняет состояние Runtime и независимо отправляет <code>SetCountdown → StartCountdown</code> в vMix; Pause/Stop сразу останавливают Runtime и независимо отправляют только <code>StopCountdown</code>, без повторной установки времени. Ошибка Agent не блокирует управление таймером. Каждую секунду значение в vMix не передаётся. <b>Text mirror</b> оставлен только для совместимости со старыми титрами. Для верхнего счёта используется одна penalty-плашка: при реальном большинстве она показывает ближайшее изменение численного состава; при чистом равном обоюдном удалении сама по себе не появляется.</p>
|
||||
</div>`;
|
||||
} else if (step.type === "delay") {
|
||||
body.innerHTML = `<div class="shortcut-step-grid"><label>Задержка, мс<input type="number" min="0" max="10000" step="10" data-step-field="milliseconds" value="${Number(step.milliseconds) || 0}"></label></div>`;
|
||||
|
||||
Reference in New Issue
Block a user