""" The message that says something changed has to be able to say what. A group's directory table travelled on `index_sync` alone — a *full* index, which the node only ever sends on request. Every ongoing change went out as an `index_delta`, which carried files and nothing else. So a root added, removed, ejected or plugged by the operator reached every other client's screen only when somebody happened to reload the page. It was hidden by the acks: `root_add_ack` and friends broadcast the new table to whoever was connected, so the common cases looked fine. What that could not cover is a client connecting mid-change, one whose ack was lost, or — the one that surfaced it — the operator's own client, where the ack landed and was then overwritten by an index fetched before the node had rebuilt anything. Additive on the wire (MNP 1.1): a 1.0 client sees a field it does not read. """ from pathlib import Path from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey from meshbay_common.crypto import generate_gek from meshbay_common.groupbox import PURPOSE_INDEX, unseal from meshbay_node.indexer.group_index import GroupIndex from meshbay_node.roots import RootSet from meshbay_node.transport.wire import index_delta_message, index_sync_message GROUP = "g" * 32 def _roots(tmp_path: Path) -> RootSet: for name in ("Films", "Albums"): (tmp_path / name).mkdir() return RootSet.build([ {"path": str(tmp_path / "Films"), "writable": True}, {"path": str(tmp_path / "Albums"), "removable": True}, ]) def _index() -> GroupIndex: return GroupIndex(group_id=GROUP, sk_node=Ed25519PrivateKey.generate(), gek=generate_gek()) def _payload(msg: dict, index: GroupIndex) -> dict: """What a member actually reads, through the seal rather than around it.""" return unseal(index.gek, PURPOSE_INDEX, msg["type"], GROUP, msg) class _Delta: base_version = 1 version = 2 additions: list = [] deletions: list = [] updates: list = [] def test_a_delta_carries_the_directory_table(tmp_path): index = _index() msg = index_delta_message(index, _Delta(), _roots(tmp_path)) payload = _payload(msg, index) assert [r["name"] for r in payload["roots"]] == ["Films", "Albums"] assert payload["roots"][0]["writable"] is True assert payload["roots"][1]["removable"] is True def test_the_table_is_sealed_with_the_rest(tmp_path): """ It is group content, not routing. Only `type`, `v` and `group_id` stay in clear, because a receiver has to route and authenticate before it would trust a decryption. """ index = _index() msg = index_delta_message(index, _Delta(), _roots(tmp_path)) assert set(msg) - {"type", "v", "group_id"}, "nothing was sealed" assert "roots" not in msg, "the directory table is outside the envelope" def test_a_delta_still_works_without_a_table(tmp_path): """ The argument is optional, so an older caller — or a path that has no root set to hand — produces a message a client reads exactly as before. """ index = _index() payload = _payload(index_delta_message(index, _Delta()), index) assert "roots" not in payload assert payload["version"] == 2 def test_the_table_says_the_same_thing_on_both_messages(tmp_path): """ Two encodings of one idea is the drift `wire.py` exists to prevent — it already happened twice, for `index_sync` and for `file_chunk`. """ index = _index() roots = _roots(tmp_path) delta = _payload(index_delta_message(index, _Delta(), roots), index) sync = _payload(index_sync_message(index, roots), index) assert delta["roots"] == sync["roots"] def test_the_table_never_carries_a_path(tmp_path): """ This message goes to every member. Where a directory lives on the operator's disk is theirs — see test_root_paths_are_operator_only.py. """ index = _index() payload = _payload(index_delta_message(index, _Delta(), _roots(tmp_path)), index) assert not any("path" in r for r in payload["roots"]) def test_an_ejected_root_is_visible_in_the_delta(tmp_path): """ The case this was written for. An eject changes no file — the entries freeze — so the delta it produces is empty of additions, deletions and updates. Without the table it says literally nothing, which is how a library disappearing from under the group's feet went unannounced. """ index = _index() roots = _roots(tmp_path) roots.roots[1].ejected = True roots.roots[1].available = False payload = _payload(index_delta_message(index, _Delta(), roots), index) assert payload["additions"] == [] and payload["deletions"] == [] assert payload["roots"][1]["ejected"] is True assert payload["roots"][1]["available"] is False