summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-10 17:29:50 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-10 17:29:50 +0200
commit4753c67816c774323e3ab4efc76d3259e8ded40d (patch)
tree0ac0b8a3013aa483111ea249e3aa37ff49988891 /packages/meshbay-hub/tests
parent1dcedc77083b908b7b3b431bad679a4813884355 (diff)
downloadmeshbay-4753c67816c774323e3ab4efc76d3259e8ded40d.tar.gz
refactor(spa): stop asking a node what version it is
`MNP_MIN_SUPPORTED` is the version this build speaks, so `check_version` refuses everything below it at the handshake. Every capability the client was gating on the node's version is therefore true of every peer it can reach: * `supportsSealedUpload` — an upload is sealed or it is not sent; * `supportsAppOps` — one `app_directories` op, and no `setVideoRoot` / `setAudioRoot` / `setPhotoRoots` wrappers behind it; * `supportsTransferSlots` and `Lease._skip()` — a lease is always real, so there is no branch where a transfer runs without one; * `legacyNode`, the read-only shared-directories table, and the two hints telling an operator their node is too old to configure an app. The version the node declares is still recorded, for diagnostics. Nothing branches on it, and the comment says so, because a field kept "just in case" is how the branches came back last time. `test_mnp_1_0_node_compat.py` goes with them: it existed to hold the fallbacks in place, and holding a fallback that cannot execute is how a suite starts lying. The two locale strings for those hints are removed from all ten catalogues. Hub suite 872 passed (test_sticky_header deselected — failing before this). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AsoWC3GmhNdwVFomW3QjH3
Diffstat (limited to 'packages/meshbay-hub/tests')
-rw-r--r--packages/meshbay-hub/tests/harness/upload_seal_probe.mjs1
-rw-r--r--packages/meshbay-hub/tests/test_app_settings_plugin.py6
-rw-r--r--packages/meshbay-hub/tests/test_mnp_1_0_node_compat.py144
-rw-r--r--packages/meshbay-hub/tests/test_transport_contracts.py7
-rw-r--r--packages/meshbay-hub/tests/test_upload_seal_client.py15
5 files changed, 9 insertions, 164 deletions
diff --git a/packages/meshbay-hub/tests/harness/upload_seal_probe.mjs b/packages/meshbay-hub/tests/harness/upload_seal_probe.mjs
index a6008c2..0fe7554 100644
--- a/packages/meshbay-hub/tests/harness/upload_seal_probe.mjs
+++ b/packages/meshbay-hub/tests/harness/upload_seal_probe.mjs
@@ -68,7 +68,6 @@ tp._channel = { readyState: 'open', bufferedAmount: 0, send() {}, close() {} };
tp._pc = { close() {} };
tp._gekRaw = hex(input.gek);
tp._connectArgs = { groupId: input.group_id };
-tp._nodeVersion = input.node_version;
const frames = [];
let uploadId = null;
diff --git a/packages/meshbay-hub/tests/test_app_settings_plugin.py b/packages/meshbay-hub/tests/test_app_settings_plugin.py
index b8afc23..892fd09 100644
--- a/packages/meshbay-hub/tests/test_app_settings_plugin.py
+++ b/packages/meshbay-hub/tests/test_app_settings_plugin.py
@@ -226,9 +226,9 @@ def test_the_page_performs_exactly_one_app_specific_operation():
# The page's own settings, which belong to no app: which apps are enabled
# at all, and how hard the node works watching its disk.
page_level = {"setAppsEnabled", "setScanSettings"}
- # Both are the same generic operation; the second is what a node too old
- # for it understands, chosen by version rather than by app.
- generic = {"setAppDirectories", "setAppDirectoriesLegacy"}
+ # One generic operation, keyed by the app's own name: adding an app adds
+ # no message type and no call site here.
+ generic = {"setAppDirectories"}
assert calls - page_level == generic, (
f"the settings page performs app-specific operations: "
f"{sorted(calls - page_level - generic)}")
diff --git a/packages/meshbay-hub/tests/test_mnp_1_0_node_compat.py b/packages/meshbay-hub/tests/test_mnp_1_0_node_compat.py
deleted file mode 100644
index b15018b..0000000
--- a/packages/meshbay-hub/tests/test_mnp_1_0_node_compat.py
+++ /dev/null
@@ -1,144 +0,0 @@
-"""
-The page ships before the nodes do.
-
-The SPA is served by the hub, so deploying the hub puts this version of the
-client in front of *every* node, including the ones still running MNP 1.0. That
-window is not a corner case — it is the normal state for as long as it takes an
-operator to update, and for a node someone else runs it may be indefinite.
-
-The failure mode is specific and quiet: a node logs an unknown message type and
-sends **nothing back**, so a control that speaks MNP 1.1 to it produces a
-thirty-second wait ending in a timeout, with nothing on screen to say the node
-simply cannot do this. Three of them were like that before these tests:
-
-* The shared-directories toggles, eject and plug have no older equivalent at
- all.
-* The per-app folder pickers spoke `app_directories`, where a 1.0 node
- understands `video_root` / `audio_root` / `photo_roots`.
-
-Source-reading, like the other SPA guards. What it cannot check is that the
-degraded path is pleasant; what it does check is that each of the three exists.
-"""
-
-import re
-from pathlib import Path
-
-import pytest
-
-STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"
-TRANSPORT = STATIC / "transport.js"
-FILES_APP = STATIC / "files-app.js"
-GROUP_PAGE = STATIC / "group-page.js"
-GROUP_SETTINGS = STATIC / "group-settings.js"
-
-pytestmark = pytest.mark.skipif(not TRANSPORT.exists(),
- reason="SPA sources unavailable")
-
-
-def _component(source: str, name: str) -> str:
- start = source.index(f"\nfunction {name}(")
- end = source.find("\nfunction ", start + 1)
- return source[start:end if end != -1 else len(source)]
-
-
-# ── Knowing which node you are talking to ───────────────────────────────────
-
-def test_the_client_keeps_the_version_it_checked():
- """
- `_checkNodeVersion` parsed the node's version and threw it away, so nothing
- downstream could ask. Refusing to connect is not the only thing a version
- is good for.
- """
- source = TRANSPORT.read_text(encoding="utf-8")
- assert "this._nodeVersion = String(reply.v" in source
- assert "get supportsAppOps()" in source
-
-
-def test_the_capability_reads_the_version_rather_than_guessing():
- """
- Inferring it from whether some field happens to be present is how two
- unrelated things end up coupled — the flag would flip because a payload
- changed shape for another reason entirely.
- """
- source = TRANSPORT.read_text(encoding="utf-8")
- getter = source[source.index("get supportsAppOps()"):]
- getter = getter[:getter.index("\n }") + 4]
- assert "_nodeVersion" in getter
- assert "1" in getter, "no version comparison in the capability check"
-
-
-# ── The three degraded paths ────────────────────────────────────────────────
-
-def test_app_directories_fall_back_to_the_three_older_messages():
- """
- Videos, Music and Photos each had their own message before the generic op,
- and those still work — so an operator on an un-upgraded node keeps the
- ability they had rather than being handed a control that times out.
- """
- source = TRANSPORT.read_text(encoding="utf-8")
- legacy = source[source.index("async setAppDirectoriesLegacy("):]
- legacy = legacy[:legacy.index("\n async ", 1)]
- for call in ("setPhotoRoots", "setVideoRoot", "setAudioRoot"):
- assert call in legacy, f"{call} is not reachable on the older path"
-
- panel = _component(GROUP_SETTINGS.read_text(encoding="utf-8"),
- "GroupSettingsPanel")
- assert "transport.supportsAppOps" in panel, (
- "the settings page sends the 1.1 message unconditionally")
-
-
-def test_the_older_path_refuses_what_it_cannot_carry():
- """
- `video_root` and `audio_root` hold one folder. Sending several would store
- the first and drop the rest silently, which is worse than refusing — the
- operator would see a saved setting that is not what they chose.
- """
- source = TRANSPORT.read_text(encoding="utf-8")
- legacy = source[source.index("async setAppDirectoriesLegacy("):]
- legacy = legacy[:legacy.index("\n async ", 1)]
- assert "clean.length > 1" in legacy
- assert "throw new Error" in legacy
-
-
-def test_root_management_is_read_only_against_an_older_node():
- """
- Unlike the app directories, `writable`, `removable`, eject and plug have no
- older equivalent to route to. The controls are shown without being
- offered, with the reason, rather than accepting a click that goes nowhere.
- """
- panel = _component(GROUP_SETTINGS.read_text(encoding="utf-8"),
- "GroupSettingsPanel")
- table_call = panel[panel.index("<${SharedDirectoriesTable}"):]
- table_call = table_call[:table_call.index("/>")]
- assert "readOnly=" in table_call
- assert "nodeSupportsAppOps" in table_call
- assert "settings_node.roots_node_too_old" in panel, (
- "nothing says why the controls are inert")
-
-
-def test_chat_settings_are_hidden_rather_than_routed():
- """
- Chat's directory and link-preview switch are new in 1.1 with nothing
- before them, so there is no older message to fall back to.
- """
- panel = _component(GROUP_SETTINGS.read_text(encoding="utf-8"),
- "GroupSettingsPanel")
- assert "settings_node.app_node_too_old" in panel
-
-
-# ── Reading an older node's handshake ───────────────────────────────────────
-
-def test_the_ack_is_read_in_both_shapes(app=None):
- """
- A 1.0 ack has `video_root` and no `video_directories`, and no
- `chat_link_preview` at all. Reading a missing plural as "nothing
- configured" empties a working Videos tab; reading a missing switch as off
- silently changes what a group's chat does.
- """
- page = GROUP_PAGE.read_text(encoding="utf-8")
- block = page[page.index("setAppDirectories({"):]
- block = block[:block.index("setNodeSupportsAppOps")]
- for legacy in ("ack.video_root", "ack.audio_root", "ack.photo_roots"):
- assert legacy in block, f"{legacy} is not read as a fallback"
- assert "ack.chat_link_preview !== false" in page, (
- "an absent link-preview switch must read as on, not off")
diff --git a/packages/meshbay-hub/tests/test_transport_contracts.py b/packages/meshbay-hub/tests/test_transport_contracts.py
index fe550f9..f2f4372 100644
--- a/packages/meshbay-hub/tests/test_transport_contracts.py
+++ b/packages/meshbay-hub/tests/test_transport_contracts.py
@@ -372,8 +372,11 @@ def test_the_upload_itself_is_sealed(transport):
"the destination is on the message in clear")
assert "...sealed," in sent or "...probeSealed," in sent, (
"the message must carry the sealed pair")
- assert "supportsSealedUpload" in body, (
- "an older node must be refused before a chunk is sent, not after")
+ # And no branch that sends anything else: an upload is sealed or it is not
+ # sent. A fallback here is a fallback the node would have to keep opening.
+ assert "filename: file.name" not in body.replace(
+ "msgpack_encode({ filename: file.name", ""), (
+ "a filename reaches the message outside the seal")
# ── MNP 1.0: the sealed handshake ack ────────────────────────────────────────
diff --git a/packages/meshbay-hub/tests/test_upload_seal_client.py b/packages/meshbay-hub/tests/test_upload_seal_client.py
index 2e4bfb5..d233ccd 100644
--- a/packages/meshbay-hub/tests/test_upload_seal_client.py
+++ b/packages/meshbay-hub/tests/test_upload_seal_client.py
@@ -80,7 +80,7 @@ def _node_session(tmp_path: Path, gek: bytes) -> WebRTCPeerSession:
def _probe_input(gek: bytes, mode: str, **extra) -> dict:
return {
"mode": mode, "gek": gek.hex(), "group_id": GROUP,
- "node_version": "2.0", "chunk_size": CHUNK, "dir": "library",
+ "chunk_size": CHUNK, "dir": "library",
"root": "library",
"file": {"name": "holiday.jpg", "data": BODY.hex()},
**extra,
@@ -156,19 +156,6 @@ def test_the_caller_is_told_the_name_the_node_chose(tmp_path, _gek, _sent):
assert result["stored"]["dir"] == "library"
-def test_the_client_refuses_an_older_node_before_sending_a_chunk(_gek):
- """
- A 1.x node would answer "Missing filename or data" — an error about the
- wrong thing, naming no upload, which fails every upload in flight. Asked
- first instead, and nothing goes on the wire.
- """
- result = _run_probe(_probe_input(_gek, "receive", acks=[],
- node_version="1.1"))
- assert result["state"] == "rejected"
- assert "older MeshBay" in result["message"]
- assert result["frames"] == [], "a chunk was sent to a node that cannot open it"
-
-
def test_an_interrupted_upload_resumes_where_the_node_stopped(tmp_path, _gek):
"""
The browser asks, the node answers, and the second attempt sends only what