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
+154
View File
@@ -315,6 +315,160 @@ class TestTeardown:
registry.signal(v1, sharer, {})
class TestLayout:
def test_the_default_is_the_wall_not_fullscreen_with_zero_followers(self, registry):
registry.join(role="viewer")
assert registry.layout.staged is None
assert registry.layout.fullscreen is False
assert registry.layout_dto() == {
"type": "layout",
"staged": None,
"fullscreen": False,
"followers": 0,
}
def test_staging_broadcasts_to_viewers_and_the_sender_without_doubling(self, mesh):
sharer, v1, v2, registry = mesh
outbox = registry.set_layout(sharer, staged=sharer)
assert {t for t, _ in outbox} == {sharer, v1, v2}
for peer_id in (sharer, v1, v2):
assert types_for(outbox, peer_id) == ["layout"]
expected = {"type": "layout", "staged": sharer, "fullscreen": False, "followers": 0}
assert all(m == expected for _t, m in outbox)
assert len(outbox) == 3 # one frame per target; nobody receives it twice
def test_a_partial_update_touches_only_the_named_fields(self, mesh):
sharer, _v1, _v2, registry = mesh
registry.set_layout(sharer, staged=sharer)
outbox = registry.set_layout(sharer, fullscreen=True)
dto = message_for(outbox, sharer, "layout")
assert registry.layout.staged == sharer # absent field left alone
assert registry.layout.fullscreen is True
assert dto == {"type": "layout", "staged": sharer, "fullscreen": True, "followers": 0}
def test_an_explicit_null_clears_and_an_opinionless_arrange_is_ack_only(self, mesh):
sharer, v1, v2, registry = mesh
registry.set_layout(sharer, staged=sharer, fullscreen=True)
outbox = registry.set_layout(v1, staged=None)
assert registry.layout.staged is None
assert registry.layout.fullscreen is True # absent field left alone
# The clearing is a real change: viewers plus the sender (deduped) are told.
assert {t for t, _ in outbox} == {v1, v2}
# No opinion at all: the sender is acked and nobody else hears anything.
outbox = registry.set_layout(v2)
assert outbox == [
(v2, {"type": "layout", "staged": None, "fullscreen": True, "followers": 0})
]
def test_an_unknown_staged_mirror_is_refused_and_changes_nothing(self, mesh):
sharer, _v1, _v2, registry = mesh
registry.set_layout(sharer, staged=sharer, fullscreen=True)
with pytest.raises(RegistryError) as excinfo:
registry.set_layout(sharer, staged="ghost", fullscreen=False)
assert excinfo.value.code == "unknown_stream"
assert registry.layout.staged == sharer
assert registry.layout.fullscreen is True
def test_repeating_the_same_state_replies_with_the_ack_only(self, mesh):
sharer, _v1, _v2, registry = mesh
registry.set_layout(sharer, staged=sharer)
outbox = registry.set_layout(sharer, staged=sharer, fullscreen=False)
assert outbox == [
(sharer, {"type": "layout", "staged": sharer, "fullscreen": False, "followers": 0})
]
def test_unpublishing_the_staged_mirror_falls_back_to_the_wall(self, mesh):
sharer, v1, v2, registry = mesh
registry.set_layout(sharer, staged=sharer)
outbox = registry.unpublish(sharer)
for peer_id in (v1, v2):
# Exactly one layout frame, riding the same envelope set as the removal.
assert types_for(outbox, peer_id) == ["stream_removed", "layout"]
assert message_for(outbox, peer_id, "layout")["staged"] is None
assert registry.layout.staged is None
def test_a_sharer_disconnecting_falls_back_to_the_wall(self, mesh):
sharer, v1, v2, registry = mesh
registry.set_layout(sharer, staged=sharer)
outbox = registry.leave(sharer)
for peer_id in (v1, v2):
assert types_for(outbox, peer_id) == ["stream_removed", "layout"]
assert message_for(outbox, peer_id, "layout")["staged"] is None
assert registry.layout.staged is None
def test_a_replaced_stream_leaves_the_stage_cleared(self, mesh):
sharer, v1, _v2, registry = mesh
registry.set_layout(sharer, staged=sharer)
registry.leave(sharer) # the old mirror goes with its socket
fresh, _ = registry.join(role="sharer", name="Linux Box")
outbox = registry.publish(fresh, label="Terminal", width=1280, height=720)
# The replacement has a new id; the stage must not still point at the
# old one, and nothing auto-stages the newcomer.
assert fresh != sharer
assert registry.layout.staged is None
assert "layout" not in types_for(outbox, v1)
assert [s["id"] for s in registry.roster()] == [fresh]
def test_the_stage_is_said_once_when_a_kiosk_sharer_leaves(self, registry):
kiosk_sharer, _ = registry.join(role="sharer", kiosk=True)
viewer, _ = registry.join(role="viewer")
registry.publish(kiosk_sharer, label="Terminal")
registry.set_layout(kiosk_sharer, staged=kiosk_sharer)
outbox = registry.leave(kiosk_sharer)
# The fallback and the follower change must not double the layout frame.
assert types_for(outbox, viewer) == ["stream_removed", "layout"]
def test_followers_counts_only_kiosk_peers(self, registry):
registry.join(role="viewer")
registry.join(role="viewer")
registry.join(role="viewer", kiosk=True)
registry.join(role="sharer")
assert registry.layout_dto()["followers"] == 1
def test_a_kiosk_joining_pushes_the_new_follower_count(self, mesh):
_sharer, v1, v2, registry = mesh
_id, outbox = registry.join(role="viewer", kiosk=True)
# Told to the other viewers; the joiner got the state in its welcome,
# not as a layout frame.
assert {t for t, m in outbox if m["type"] == "layout"} == {v1, v2}
assert types_for(outbox, _id) == ["welcome"]
assert message_for(outbox, v1, "layout")["followers"] == 1
assert message_for(outbox, v1, "layout") == message_for(outbox, v2, "layout")
assert registry.layout_dto()["followers"] == 1
def test_a_non_kiosk_join_says_nothing_about_the_layout(self, mesh):
_sharer, v1, _v2, registry = mesh
_id, outbox = registry.join(role="viewer")
assert "layout" not in types_for(outbox, v1)
def test_a_kiosk_leaving_pushes_the_new_follower_count(self, mesh):
_sharer, v1, v2, registry = mesh
kiosk, _ = registry.join(role="viewer", kiosk=True)
assert registry.layout_dto()["followers"] == 1
outbox = registry.leave(kiosk)
assert {t for t, _ in outbox} == {v1, v2}
assert message_for(outbox, v1, "layout")["followers"] == 0
assert registry.layout_dto()["followers"] == 0
def test_a_peer_joined_after_an_arrange_arrives_already_arranged(self, mesh):
sharer, _v1, _v2, registry = mesh
registry.set_layout(sharer, staged=sharer, fullscreen=True)
late, outbox = registry.join(role="viewer", kiosk=True)
welcome = message_for(outbox, late, "welcome")
assert welcome["layout"] == {
"type": "layout",
"staged": sharer,
"fullscreen": True,
"followers": 1,
}
def test_a_dead_peer_cannot_arrange_the_wall(self, mesh):
_sharer, _v1, _v2, registry = mesh
with pytest.raises(RegistryError) as excinfo:
registry.set_layout("ghost", staged="ghost")
assert excinfo.value.code == "unknown_peer"
class TestRenameAndStats:
def test_rename_refreshes_the_mirrors_that_show_it(self, mesh):
sharer, v1, _v2, registry = mesh