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`.
193 lines
7.5 KiB
Python
193 lines
7.5 KiB
Python
"""The wire contract, in both directions.
|
|
|
|
These exist because the client speaks camelCase and Python speaks snake_case,
|
|
and a silently-dropped field is the kind of bug that only shows up as a
|
|
"cosmetic" column being blank three features later.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from mirror import protocol
|
|
|
|
|
|
class TestInbound:
|
|
def test_publish_accepts_the_camel_case_wire_format(self):
|
|
message = protocol.parse_client_message(
|
|
{
|
|
"type": "publish",
|
|
"label": "proj.gl — zsh",
|
|
"displaySurface": "window",
|
|
"width": 1920,
|
|
"height": 1080,
|
|
"audio": True,
|
|
}
|
|
)
|
|
assert message.display_surface == "window"
|
|
assert (message.width, message.height) == (1920, 1080)
|
|
assert message.audio is True
|
|
|
|
def test_snake_case_is_accepted_too_so_server_side_tools_can_replay_frames(self):
|
|
message = protocol.parse_client_message(
|
|
{"type": "publish", "display_surface": "monitor", "width": 800, "height": 600}
|
|
)
|
|
assert message.display_surface == "monitor"
|
|
|
|
def test_subscribe_carries_a_camel_case_stream_id(self):
|
|
assert (
|
|
protocol.parse_client_message({"type": "subscribe", "streamId": "p1"}).stream_id == "p1"
|
|
)
|
|
assert (
|
|
protocol.parse_client_message({"type": "unsubscribe", "streamId": "p1"}).stream_id
|
|
== "p1"
|
|
)
|
|
|
|
def test_a_missing_stream_id_is_a_validation_error_not_a_none(self):
|
|
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):
|
|
protocol.parse_client_message({"type": "publish", "width": bad})
|
|
|
|
def test_unknown_message_types_are_rejected(self):
|
|
with pytest.raises(ValidationError):
|
|
protocol.parse_client_message({"type": "teleport"})
|
|
|
|
def test_extra_keys_are_tolerated_for_forward_compatibility(self):
|
|
message = protocol.parse_client_message({"type": "ping", "someDayWeAddThis": True})
|
|
assert message.type == "ping"
|
|
|
|
def test_a_signal_payload_is_carried_verbatim(self):
|
|
payload = {"sdp": {"type": "offer", "sdp": "v=0\r\n"}}
|
|
message = protocol.parse_client_message({"type": "signal", "to": "p2", "payload": payload})
|
|
assert message.payload == payload
|
|
|
|
|
|
class TestOutbound:
|
|
def test_the_stream_dto_is_camel_case(self):
|
|
dto = protocol.stream_dto(
|
|
stream_id="p1",
|
|
sharer_id="p1",
|
|
name="Tower",
|
|
label="Terminal",
|
|
display_surface="window",
|
|
width=1280,
|
|
height=720,
|
|
audio=False,
|
|
viewers=2,
|
|
)
|
|
assert set(dto) == {
|
|
"id",
|
|
"sharerId",
|
|
"name",
|
|
"label",
|
|
"displaySurface",
|
|
"width",
|
|
"height",
|
|
"audio",
|
|
"viewers",
|
|
}
|
|
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=[],
|
|
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"),
|
|
protocol.peer_joined(stream_id="p1", viewer_id="p2", name="V"),
|
|
protocol.peer_left(stream_id="p1", viewer_id="p2"),
|
|
protocol.relayed_signal(frm="p1", to="p2", payload={}),
|
|
protocol.error(code="x", message="y"),
|
|
protocol.pong(),
|
|
]
|
|
assert [m["type"] for m in messages] == [
|
|
"welcome",
|
|
"layout",
|
|
"stream_added",
|
|
"stream_updated",
|
|
"stream_removed",
|
|
"peer_joined",
|
|
"peer_left",
|
|
"signal",
|
|
"error",
|
|
"pong",
|
|
]
|
|
|
|
def test_an_error_says_whether_the_connection_survives_it(self):
|
|
assert protocol.error(code="x", message="y")["fatal"] is False
|
|
assert protocol.error(code="x", message="y", fatal=True)["fatal"] is True
|