шорткаты

This commit is contained in:
2026-08-24 15:59:05 +03:00
parent 0ef8378dd1
commit 79c59eeeed
5 changed files with 153 additions and 115 deletions

View File

@@ -37,8 +37,8 @@ def test_build100_frontend_shortcut_edges_and_pending_press_are_guarded() -> Non
assert 'BUILD_VERSION = "2026.08.24.4"' in APP
def test_build100_interactive_shortcut_is_single_command_acked_and_continues_after_failure(tmp_path: Path) -> None:
database = LocalTestDatabase(tmp_path / "build100-shortcut.sqlite3")
def test_build102_interactive_shortcut_is_dispatched_without_ack_or_mapping_lock(tmp_path: Path) -> None:
database = LocalTestDatabase(tmp_path / "build102-shortcut.sqlite3")
database.create_all()
hub = VmixAgentHub(database) # type: ignore[arg-type]
ws = FakeWebSocket()
@@ -50,8 +50,8 @@ def test_build100_interactive_shortcut_is_single_command_acked_and_continues_aft
{
"device_id": "GFX-BUILD100",
"device_secret": "z" * 40,
"device_name": "Build100 GFX",
"hostname": "BUILD100-PC",
"device_name": "Build102 GFX",
"hostname": "BUILD102-PC",
"agent_version": "1.4.0",
"vmix": {"connected": True, "url": "http://127.0.0.1:8088/api/"},
},
@@ -64,55 +64,44 @@ def test_build100_interactive_shortcut_is_single_command_acked_and_continues_aft
)
assert assigned is not None
task = asyncio.create_task(
hub.run_vmix_sequence_for_user(
user,
[
{"Function": "OverlayInput1In", "Input": "SCORE"},
{"Function": "OverlayInput2In", "Input": "BROKEN"},
{"Function": "OverlayInput3In", "Input": "LINEUP"},
],
sequence_id="shortcut-build100",
sequence_name="Titles",
# Simulate Mapping currently waiting for its own ACK lock. Operator shortcuts
# must bypass that lock completely in BUILD102.
mapping_lock = hub._device_vmix_send_lock("GFX-BUILD100")
await mapping_lock.acquire()
try:
result = await asyncio.wait_for(
hub.run_vmix_sequence_for_user(
user,
[
{"Function": "OverlayInput1In", "Input": "SCORE"},
{"Function": "OverlayInput2In", "Input": "PENALTY"},
{"Function": "OverlayInput3In", "Input": "LINEUP"},
],
sequence_id="shortcut-build102",
sequence_name="Titles",
),
timeout=0.5,
)
)
finally:
mapping_lock.release()
seen: list[dict] = []
for index, ok in enumerate([True, False, True]):
command = None
for _ in range(100):
await asyncio.sleep(0)
commands = [item for item in ws.sent if item.get("type") == "vmix.command"]
if len(commands) > index:
command = commands[index]
break
assert command is not None
seen.append(command)
await hub.receive_command_ack(
"GFX-BUILD100",
{
"type": "command.ack",
"request_id": command["request_id"],
"ok": ok,
"reason": "bad input" if not ok else "",
},
)
assert not [item for item in ws.sent if item.get("type") == "vmix.batch"]
assert [item["command"]["Input"] for item in seen] == ["SCORE", "BROKEN", "LINEUP"]
result = await task
assert result["transport"] == "shortcut-sequential"
commands = [item for item in ws.sent if item.get("type") == "vmix.command"]
assert [item["command"]["Input"] for item in commands] == ["SCORE", "PENALTY", "LINEUP"]
assert result["transport"] == "shortcut-no-ack"
assert result["confirmation"] == "not_waited"
assert result["requested"] == 3
assert result["attempted"] == 3
assert result["applied"] == 2
assert result["failed"] == 1
assert result["ok"] is False
assert result["results"][2]["ok"] is True
assert result["applied"] == 3
assert result["failed"] == 0
assert result["ok"] is True
assert all(row["ack_waited"] is False for row in result["results"])
assert not hub._pending_commands
asyncio.run(scenario())
def test_build100_backend_marks_interactive_shortcuts_sequential() -> None:
assert 'interactive_shortcut = bool' in BRIDGE
assert 'transport = "batch" if use_batch else ("shortcut-sequential"' in BRIDGE
assert 'if not item["ok"] and not interactive_shortcut' in BRIDGE
def test_build102_backend_marks_interactive_shortcuts_no_ack() -> None:
assert 'transport = "shortcut-no-ack" if interactive_shortcut' in BRIDGE
assert 'async with self._device_vmix_shortcut_send_lock(target_device_id)' in BRIDGE
assert '"ack_waited": False' in BRIDGE
assert '"confirmation": "not_waited" if interactive_shortcut else "ack"' in BRIDGE

View File

@@ -36,8 +36,9 @@ def test_quick_panel_selector_is_configurable_and_sent_before_sequence():
assert "hockeyCommitQuickPanelButtonContext" in JS
click = JS.index('el.runtimeButtonDock.querySelectorAll("[data-quick-command-button]")')
snippet = JS[click:click + 2600]
assert "await hockeyCommitQuickPanelButtonContext(button, attachedSelectors)" in snippet
assert "await hockeyCommitQuickPanelButtonContext(button, attachedSelectors, { refreshMapping: false })" in snippet
assert snippet.index("await hockeyCommitQuickPanelButtonContext") < snippet.index("runShortcutSequence")
assert "full Mapping ACK round-trip" in snippet
assert '"/control/values"' in JS
assert '@router.put("/games/{external_id}/control/values")' in ROUTER
assert 'quick_panel_selectors' in APP