Push a state snapshot only for events that change it

The hub sent every open tab a full snapshot on every StateEvent, LED writes
included - placing and removing a figure was 13 snapshots, 9 of them for LED
changes the snapshot does not even contain. List the events that matter, and make
a test fail when a new StateEvent is sorted into neither list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-04 00:23:06 +02:00
parent b791beed9c
commit 7a7ad9f0a7
2 changed files with 62 additions and 2 deletions

View File

@@ -21,15 +21,20 @@ from fastapi import FastAPI
from musicmouse.app import ParentTimer
from musicmouse.config import WebConfig, load_config
from musicmouse.effects import OFF
from musicmouse.events import (
CancelTimerRequested,
ClockStateChanged,
LedEffectChanged,
SetTimerRequested,
StateEvent,
ToggleClockRequested,
ToggleScreenRequested,
)
from musicmouse.hardware import LedZone
from musicmouse.library.analysis import BeatGrid, TrackCurves
from musicmouse.library.models import track_key
from musicmouse.services.web.hub import IGNORED_EVENTS, SNAPSHOT_EVENTS
from musicmouse.services.web.service import build_app
from musicmouse.simulator.harness import Simulation, build_simulation
from tests.conftest import VALID_CONFIG, write_config
@@ -442,6 +447,30 @@ async def test_a_state_change_reaches_every_client(api: FastAPI, sim: Simulation
assert message["state"]["active_figure"] == "fuchs"
async def test_led_writes_are_not_pushed_to_the_clients(api: FastAPI, sim: Simulation) -> None:
"""The snapshot has no LED state, so an LED write is no reason to send one."""
async with websocket_connect(api, "/api/ws") as socket:
await socket.next_json()
await sim.bus.emit_and_wait(
LedEffectChanged(zone=LedZone.RING, effect=OFF(), origin="mqtt")
)
assert socket.from_app.empty()
def test_every_state_event_is_either_pushed_or_deliberately_ignored() -> None:
"""A new StateEvent has to be sorted into SNAPSHOT_EVENTS or IGNORED_EVENTS."""
def subclasses(kind: type) -> set[type]:
found = set(kind.__subclasses__())
for sub in list(found):
found |= subclasses(sub)
return found
assert subclasses(StateEvent) == set(SNAPSHOT_EVENTS) | set(IGNORED_EVENTS)
async def test_switching_albums_while_playing_reaches_every_client(
api: FastAPI, client: Client, sim: Simulation
) -> None: