summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-19 01:02:05 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-19 01:49:56 +0200
commitc2c49182e6d4f1f1ef3ed3ba5e8699912f9a27bd (patch)
tree026a7e4347d4580ed4914605019db50c5794a49b /packages/meshbay-node/tests
parent345a59fc128e5e4f253c277c8cee69a2b1062a4f (diff)
downloadmeshbay-c2c49182e6d4f1f1ef3ed3ba5e8699912f9a27bd.tar.gz
feat(node): per-file upload ceiling is an operator setting, default 8 GB
Was a 4 GB constant in webrtc_server.py, the same on a small board and on a machine holding a library. Now max_upload_gb in node.toml, on the Node page and via `meshbay-node transfers max-size`, read from the transport context per chunk so a change reaches an upload already running. MESHBAY_DESIGN.md §6.4; §15.3 records a defect found beside it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/tests')
-rw-r--r--packages/meshbay-node/tests/test_chat_is_bounded.py2
-rw-r--r--packages/meshbay-node/tests/test_cli_dispatch.py5
-rw-r--r--packages/meshbay-node/tests/test_transfer_settings.py6
-rw-r--r--packages/meshbay-node/tests/test_upload_size_cap.py152
4 files changed, 162 insertions, 3 deletions
diff --git a/packages/meshbay-node/tests/test_chat_is_bounded.py b/packages/meshbay-node/tests/test_chat_is_bounded.py
index 1af72b4..4e767bc 100644
--- a/packages/meshbay-node/tests/test_chat_is_bounded.py
+++ b/packages/meshbay-node/tests/test_chat_is_bounded.py
@@ -13,7 +13,7 @@ it in `chat.db` on the operator's disk, where nothing expires it — retention i
a manual CLI command (§6.6) — relays it to every other connected member, and
has the hub write a notification for every member of the group. Uploads, the
other member-supplied write, have carried a filename allowlist, strict chunk
-ordering, a no-overwrite rule and a 4 GB cap since C5a. Chat carried nothing:
+ordering, a no-overwrite rule and a per-file size cap since C5a. Chat carried nothing:
the only ceiling was the DataChannel frame, 64 MB once the handshake is done.
One member in a loop filled the operator's disk and saturated everyone else's
connection, and the node's own answer to each message was `ack`.
diff --git a/packages/meshbay-node/tests/test_cli_dispatch.py b/packages/meshbay-node/tests/test_cli_dispatch.py
index 64f96f1..ce661b5 100644
--- a/packages/meshbay-node/tests/test_cli_dispatch.py
+++ b/packages/meshbay-node/tests/test_cli_dispatch.py
@@ -51,6 +51,11 @@ VERBS = [
["transfers", "set", "4", "2"],
["transfers", "set", "4"], # only one number: usage, then exit
["transfers", "set", "0", "2"], # zero is not "unlimited": refused
+ ["transfers", "max-size", "8"],
+ ["transfers", "max-size", "0.5"], # a fraction of a GB is legitimate
+ ["transfers", "max-size"], # no size: usage, then exit
+ ["transfers", "max-size", "0"], # zero is not "unlimited": refused
+ ["transfers", "max-size", "huge"], # not a number: refused
["transfers", "per-member", "4", "2"],
["transfers", "per-member", "4"], # only one number: usage, then exit
["transfers", "per-member", "0", "2"], # zero is refused here too
diff --git a/packages/meshbay-node/tests/test_transfer_settings.py b/packages/meshbay-node/tests/test_transfer_settings.py
index 79502e7..7f3719d 100644
--- a/packages/meshbay-node/tests/test_transfer_settings.py
+++ b/packages/meshbay-node/tests/test_transfer_settings.py
@@ -138,15 +138,17 @@ async def test_the_node_wide_caps_round_trip_through_the_roster(roster):
**{k: None for k in ("invite_ttl_hours", "pair_ttl_hours",
"device_request_ttl_minutes",
"max_concurrent_streams",
- "transcode_incompatible_video")},
+ "transcode_incompatible_video",
+ "max_upload_gb")},
"stun_servers": [], "ice_interfaces": [],
}
await roster.set_node_setting(roster.SETTING_MAX_DOWNLOADS, "3", "op")
assert (await roster.node_settings(defaults))["max_concurrent_downloads"] == 3
-def test_node_toml_carries_both_keys():
+def test_node_toml_carries_the_upload_keys():
"""The template is what an operator reads before they read any document."""
from meshbay_node.config import EXAMPLE_CONFIG as tpl
assert "max_concurrent_downloads" in tpl
assert "max_concurrent_uploads" in tpl
+ assert "max_upload_gb" in tpl
diff --git a/packages/meshbay-node/tests/test_upload_size_cap.py b/packages/meshbay-node/tests/test_upload_size_cap.py
new file mode 100644
index 0000000..dca6e15
--- /dev/null
+++ b/packages/meshbay-node/tests/test_upload_size_cap.py
@@ -0,0 +1,152 @@
+"""
+How large a single upload may be, and who decides.
+
+The cap used to be a constant: 4 GB, in `webrtc_server.py`, the same on a Pi
+with a 32 GB card and on a machine holding a film library. It is the operator's
+disk that fills, so the number is theirs — `max_upload_gb` under [node] in
+node.toml, the Node page, and `meshbay-node transfers max-size`, with the
+constant as the default when they have said nothing.
+
+These follow the value along the whole path rather than checking that the field
+parses, for the reason `test_stream_capacity_config.py` gives: every join in
+such a path has been wrong at least once, and a ceiling read from the wrong
+place fails only when somebody sends a large file.
+"""
+
+import textwrap
+from pathlib import Path
+
+import pytest
+
+from meshbay_node.config import load_config
+from meshbay_node.roots import RootSet
+from meshbay_node.transport.webrtc_server import (
+ GB_BYTES,
+ MAX_UPLOAD_BYTES,
+ WebRTCPeerSession,
+ WebRTCTransport,
+)
+
+
+def _cfg(tmp_path: Path, body: str):
+ p = tmp_path / "node.toml"
+ p.write_text(textwrap.dedent(body))
+ return load_config(p)
+
+
+class _FakePC:
+ def on(self, *a, **k):
+ return lambda f: f
+
+
+def _session(gb):
+ t = WebRTCTransport(
+ sk_node=None, hub_pk_pem=b"", gek=b"\0" * 32,
+ roots=RootSet(), index=None, max_upload_gb=gb)
+ return t, WebRTCPeerSession(_FakePC(), t._ctx, peer_id="p")
+
+
+# ── What the operator writes ─────────────────────────────────────────────────
+
+def test_the_default_is_eight_gb():
+ assert MAX_UPLOAD_BYTES == 8 * GB_BYTES
+
+
+def test_the_operator_sets_it(tmp_path):
+ cfg = _cfg(tmp_path, """
+ [node]
+ max_upload_gb = 20
+ """)
+ assert cfg.node.max_upload_gb == 20
+
+
+def test_a_fraction_of_a_gigabyte_is_legitimate(tmp_path):
+ """Not a count, so it does not get `_positive`'s floor of one.
+
+ A node on a small disk may well want to stop at half a gigabyte, and
+ rounding that to zero would refuse every upload.
+ """
+ cfg = _cfg(tmp_path, """
+ [node]
+ max_upload_gb = 0.5
+ """)
+ assert cfg.node.max_upload_gb == 0.5
+ _t, s = _session(0.5)
+ assert s._max_upload_bytes() == GB_BYTES // 2
+
+
+def test_saying_nothing_gets_the_default(tmp_path):
+ cfg = _cfg(tmp_path, """
+ [node]
+ quic_port = 19010
+ """)
+ assert cfg.node.max_upload_gb * GB_BYTES == MAX_UPLOAD_BYTES, (
+ "the config default and the source default disagree, so the ceiling "
+ "depends on whether a node.toml happens to mention it")
+
+
+@pytest.mark.parametrize("value", ["0", "-2", '"lots"', "true"])
+def test_a_value_that_would_refuse_every_upload_is_refused(tmp_path, value, caplog):
+ cfg = _cfg(tmp_path, f"""
+ [node]
+ max_upload_gb = {value}
+ """)
+ assert cfg.node.max_upload_gb * GB_BYTES == MAX_UPLOAD_BYTES
+ assert "max_upload_gb" in caplog.text, (
+ "the value was silently discarded — the operator has no way to learn "
+ "their setting is not in effect")
+
+
+# ── That the number reaches the check it governs ─────────────────────────────
+
+@pytest.mark.parametrize("gb,expect", [
+ (None, MAX_UPLOAD_BYTES),
+ (2, 2 * GB_BYTES),
+ (16, 16 * GB_BYTES),
+])
+def test_the_configured_size_is_what_the_handler_enforces(gb, expect):
+ _t, s = _session(gb)
+ assert s._max_upload_bytes() == expect
+
+
+def test_the_handler_reads_it_rather_than_the_constant():
+ """A ceiling captured once is one the operator cannot change.
+
+ The check runs per chunk, so raising the cap has to reach an upload that
+ is already in flight — which it does only if the handler asks the context
+ each time instead of closing over the constant.
+ """
+ src = (Path(__file__).resolve().parents[1] / "src" / "meshbay_node"
+ / "transport" / "webrtc_server.py").read_text(encoding="utf-8")
+ i = src.index("Upload exceeds size limit")
+ check = src[src.rindex("if state.bytes", 0, i):i]
+ assert "_max_upload_bytes()" in check, (
+ "the upload check reads the module constant, so node.toml, the Node "
+ "page and the CLI are all read and then ignored")
+
+
+def test_raising_it_reaches_an_upload_already_running():
+ t, s = _session(2)
+ assert s._max_upload_bytes() == 2 * GB_BYTES
+ t.set_capacity(max_upload_gb=10)
+ assert s._max_upload_bytes() == 10 * GB_BYTES, (
+ "the session kept the ceiling it started with, so the setting only "
+ "takes effect on a restart"
+ )
+
+
+def test_zero_is_refused_at_the_transport_too():
+ t, _s = _session(4)
+ with pytest.raises(ValueError):
+ t.set_capacity(max_upload_gb=0)
+
+
+def test_the_daemon_passes_it():
+ """The join that syntax checking cannot see."""
+ daemon = (Path(__file__).resolve().parents[1] / "src" / "meshbay_node"
+ / "daemon.py").read_text(encoding="utf-8")
+ i = daemon.index("WebRTCTransport(")
+ call = daemon[i:daemon.index(")", daemon.index("denylist=denylist", i))]
+ assert "max_upload_gb=self._config.node.max_upload_gb" in call, (
+ "the daemon builds the transport without the operator's ceiling, so "
+ "node.toml is read and then ignored")