phase: 07_kiosk_layout_state
Build and Push Containers / test (push) Successful in 39s
Build and Push Containers / build-and-push-app (push) Successful in 12s

Every AGENTS.md rule claim checks out against the code — no stale statements. All verification is complete.

**Phase 07 — final verification pass report**

- **Verified, not re-implemented:** all 3 tasks already in `complete/` — `Arrange`/`layout` frames + `hello.kiosk` (protocol), `Layout`/`set_layout`/`layout_dto` policy (registry), `arrange` dispatch + `GET /api/kiosk` (app).
- **Live check:** idle `/api/kiosk` → `{"staged": null, "fullscreen": false, "followers": 0}`; drove `arrange` over a real WebSocket (websocat absent → python `websockets` client): state reflected in `/api/kiosk`, kiosk join bumped `followers` to 1 and pushed to other viewers, sharer teardown reset `staged` to `null` in exactly one `layout` frame — no stale id.
- **Gates (all 4):** `uv run ruff check . && uv run ruff format --check .` → clean; `uv run pyright` → 0 errors; `uv run pytest` → **319 passed, 5 skipped** (realdisplay, by design); `MIRROR_REALDISPLAY=1 uv run pytest -m realdisplay` → **4 passed, 1 skipped** (documented host has no audio source).
- **Criteria 1/4/5:** targeted suite 154 passed incl. all new layout cases; stale-fallback unit tests cover unpublish, disconnect, and stream-replacement; `git diff` of `src/mirror/static/` and `tests/e2e/` is empty, e2e green within the full run.
- **Criterion 6:** full suite 100% green; coverage 95% total — new/modified code: protocol 100%, registry 99%, app 94% (all >90%).
- **Criterion 7:** `PLAN.md` untouched.
- **Documentation:** README.md reviewed in full and cross-checked against code (routes, CLI flags, config table incl. 25 Mbit/s default, quality-preset table, watch/share key tables, docs references, the phase-07 shared-state paragraph) — consistent, no fixes needed. AGENTS.md — verified, no stale statements, no changes.
- **Note:** `PLAN.md` §6 says the default `MIRROR_MAX_BITRATE` is "12 Mbit/s" but code/README say 25 — pre-existing, in a LOCKED file, left as-is (README is correct).
- **Next pending phase:** `08_kiosk_page`.
This commit is contained in:
2026-11-03 09:53:38 -05:00
parent 1f60a6b40c
commit 736da1c1ef
28 changed files with 826 additions and 11 deletions
+71 -1
View File
@@ -48,6 +48,49 @@ class TestInbound:
with pytest.raises(ValidationError):
protocol.parse_client_message({"type": "subscribe"})
def test_arrange_with_no_fields_changes_nothing(self):
message = protocol.parse_client_message({"type": "arrange"})
assert message.staged is None
assert message.fullscreen is None
# Nothing was mentioned, so nothing may be applied: this is the whole
# reason the fields are read with ``model_fields_set`` rather than
# defaults.
assert "staged" not in message.model_fields_set
assert "fullscreen" not in message.model_fields_set
def test_arrange_with_an_explicit_null_staged_returns_the_displays_to_the_wall(self):
message = protocol.parse_client_message({"type": "arrange", "staged": None})
assert message.staged is None
# Explicit ``null`` is a different request from "absent": it says
# "clear the stage", so ``staged`` must be in ``model_fields_set``.
assert "staged" in message.model_fields_set
assert "fullscreen" not in message.model_fields_set
def test_arrange_carries_its_values_and_marks_only_what_was_mentioned(self):
message = protocol.parse_client_message(
{"type": "arrange", "staged": "p1", "fullscreen": True}
)
assert message.staged == "p1"
assert message.fullscreen is True
assert message.model_fields_set == {"type", "staged", "fullscreen"}
def test_arrange_accepts_an_explicit_false_fullscreen(self):
message = protocol.parse_client_message(
{"type": "arrange", "staged": "p1", "fullscreen": False}
)
assert message.fullscreen is False
assert "fullscreen" in message.model_fields_set
def test_hello_defaults_kiosk_to_false(self):
message = protocol.parse_client_message({"type": "hello", "role": "viewer"})
assert message.kiosk is False
def test_hello_accepts_a_kiosk_display(self):
message = protocol.parse_client_message(
{"type": "hello", "role": "viewer", "name": "Wall", "kiosk": True}
)
assert message.kiosk is True
def test_dimensions_are_bounded(self):
for bad in (-1, 16385, "wide"):
with pytest.raises(ValidationError):
@@ -93,9 +136,35 @@ class TestOutbound:
}
assert dto["displaySurface"] == "window"
def test_the_layout_frame_has_exactly_the_contract_keys(self):
frame = protocol.layout(staged="p1", fullscreen=True, followers=2)
# The outbound dicts are hand-written, so there is no alias generator to
# catch a typo here; the key set is the guard.
assert set(frame) == {"type", "staged", "fullscreen", "followers"}
assert frame["type"] == "layout"
assert frame["staged"] == "p1"
assert frame["fullscreen"] is True
assert frame["followers"] == 2
def test_a_layout_with_no_stage_says_null_not_absent(self):
frame = protocol.layout(staged=None, fullscreen=False, followers=0)
assert frame["staged"] is None
def test_welcome_carries_the_layout_it_was_given(self):
state = protocol.layout(staged="p1", fullscreen=True, followers=3)
frame = protocol.welcome(peer_id="p1", role="viewer", streams=[], layout=state)
assert frame["layout"] == state
assert set(frame) == {"type", "peerId", "role", "streams", "layout"}
def test_every_server_message_advertises_its_type(self):
messages = [
protocol.welcome(peer_id="p1", role="viewer", streams=[]),
protocol.welcome(
peer_id="p1",
role="viewer",
streams=[],
layout=protocol.layout(staged=None, fullscreen=False, followers=0),
),
protocol.layout(staged=None, fullscreen=False, followers=0),
protocol.stream_added(stream={"id": "p1"}),
protocol.stream_updated(stream={"id": "p1"}),
protocol.stream_removed(stream_id="p1"),
@@ -107,6 +176,7 @@ class TestOutbound:
]
assert [m["type"] for m in messages] == [
"welcome",
"layout",
"stream_added",
"stream_updated",
"stream_removed",