aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_index_seal_client.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-07 18:03:52 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-07 18:03:52 +0200
commite1383e1d545b994f4ad61694f868339defb0bdef (patch)
tree67a3b1933f2a9c5caf107d01d9ff91c44a975df6 /packages/meshbay-hub/tests/test_index_seal_client.py
parent36cebf25d0e0f24cf63be4380ccb5d03da726a74 (diff)
parent8980a8e42d94ab7c0bc9739283d39f938f8402b0 (diff)
downloadmeshbay-e1383e1d545b994f4ad61694f868339defb0bdef.tar.gz
Merge origin/main into the chat encryption work
Both sides landed a breaking MNP change and both called it 2.0, which is right: the sealed upload, the removal of `stream_seg` and mandatory chat encryption share one flag day. They are recorded as one version in `__init__.py` rather than as a race between two. The resolutions that were decisions rather than mechanics: * **`MNP_MIN_SUPPORTED` moves to "2.0".** The sealed upload alone was a *confined* break — a 1.x peer could still connect, browse, download, stream and chat, with only its uploads refused by `upload_not_sealed` — so the floor deliberately stayed at "1.0". Mandatory chat encryption ends that confinement: a 1.x peer can neither produce a sealed chat message nor read one, so it would connect, look fine, and be unable to say anything. Refusing it at the handshake is the honest form. The per-message `upload_not_sealed` path is untouched and still right if the floor is ever lowered. * **`sendChat` throws on an `error` reply**, from origin, applied to the sealed send. It matters more after this change, not less: the node now refuses a stale epoch, a malformed envelope and a device claim that is not the connection's own, so there are three new ways for a message to be rejected and none of them may look like a message that was sent. * **`req_id` supersedes the per-type routing** this branch added for `chat_keys_resp` and `device_hello_ack`. Both blocks are kept beside the existing `chat_hist_resp` one, for the same stated reason — a node too old to stamp — and their comments no longer claim to be the mechanism that closes the class. `req_id` is. * **`chat_send_probe.py` is rebuilt on origin's structure**, not beside it: two scenarios, a stub that stamps `req_id`, `music_meta_req` as the older pending request. The encrypted path is layered on — a real Ed25519 device key generated in the page, and a `chat_keys_resp` sealed by the shipped Python, because a payload the page built itself would prove only that the page agrees with the page. * **`test_reply_correlation.py` now sends a sealed message.** Its subject is which of the two messages leaving that handler carries the id; plaintext chat was only the fixture, and the node refuses one now. * `groupbox` keeps both new purposes (`upload`, `chat_keys`); `protocol.py` keeps origin's removal of `STREAM_SEGMENT` and this branch's correction of the "Double Ratchet message" comment on `CHAT_MESSAGE`, which was wrong when it was written and is wrong differently now. Full suite on the merged tree: 1993 passed, 11 failed — the same 11 that fail on a pristine checkout (2 Windows service tests, 1 apps-enabled policy, 7 transcode tests that pass in isolation, and the WebRTC invite test that hangs on its own). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TZZxYjz8YeWRz13xDi8LJr
Diffstat (limited to 'packages/meshbay-hub/tests/test_index_seal_client.py')
-rw-r--r--packages/meshbay-hub/tests/test_index_seal_client.py30
1 files changed, 29 insertions, 1 deletions
diff --git a/packages/meshbay-hub/tests/test_index_seal_client.py b/packages/meshbay-hub/tests/test_index_seal_client.py
index ca2c7a2..e1135da 100644
--- a/packages/meshbay-hub/tests/test_index_seal_client.py
+++ b/packages/meshbay-hub/tests/test_index_seal_client.py
@@ -45,10 +45,15 @@ def _frame(msg: dict) -> str:
return (struct.pack(">I", len(body)) + body).hex()
-def _sync_frame(gek: bytes, *names: str, version: int = 3) -> str:
+def _sync_frame(gek: bytes, *names: str, version: int = 3,
+ req_id: int | None = None) -> str:
payload = {"version": version, "entries": [_entry(n) for n in names],
"dirs": ["library"], "roots": [{"name": "library"}]}
+ # `req_id` is what a current node stamps on a *reply*; the push it sends a
+ # newly connected peer answers no request and carries none. Both shapes
+ # arrive here, and only one of them may resolve a waiting fetchIndex.
return _frame({"type": "index_sync", "v": "1.0", "group_id": GROUP,
+ **({"req_id": req_id} if req_id is not None else {}),
**seal(gek, PURPOSE_INDEX, "index_sync", GROUP, payload)})
@@ -141,3 +146,26 @@ def test_deltas_are_applied_in_arrival_order():
assert [e["additions"][0] for e in out["events"][1:]] == [
"added-0.mkv", "added-1.mkv", "added-2.mkv"]
assert [e["base_version"] for e in out["events"][1:]] == [3, 4, 5]
+
+
+def test_a_sealed_reply_is_opened_before_it_reaches_its_caller():
+ """A stamped index_sync must not be short-circuited by its `req_id`.
+
+ Every other reply a node stamps is resolved straight out of the pending
+ map, which is the whole point of the id. An index message cannot be: it is
+ sealed, opening it is asynchronous, and `_dispatch` is not. Handing it over
+ on the strength of the id alone gives `fetchIndex` the envelope — nonce and
+ ciphertext, no entries — and never calls `onIndexSync` at all.
+
+ `req_id` is 0 here because it is the transport's first request, and a
+ falsy id is exactly the one a presence check gets wrong.
+ """
+ out = _run([_sync_frame(GEK, "a-film.mkv", req_id=0)])
+
+ assert [e["event"] for e in out["events"]] == ["index_sync"], (
+ "the consumer was never told about an index that arrived as a reply")
+ assert out["events"][0]["entries"] == ["a-film.mkv"]
+ assert out["events"][0]["hasCiphertext"] is False
+ assert out["fetchIndex"]["state"] == "resolved"
+ assert out["fetchIndex"]["entries"] == ["a-film.mkv"], (
+ "the caller was handed the sealed envelope instead of the index")