aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-09 15:05:37 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-09 15:05:37 +0200
commit78b309d18efdd227c0efa42464988eaaf1483c16 (patch)
tree0ee3f3485ca9c963a11e139d7a615a656bcf181b /packages/meshbay-node/tests
parent7e2d078fe1870d256ae47781bee6ac4f454edf24 (diff)
downloadmeshbay-78b309d18efdd227c0efa42464988eaaf1483c16.tar.gz
fix(node): the leaseless bound refused the music player
Reported the day MNP 3.0 shipped: playing a track answered "Too many files open at once without a transfer. Download this one instead of previewing it." §3.4.1's bound of two was reasoned about *viewers* — a photo viewer shows one photo, a preview modal one document, and the second is for prefetching the next. It forgot the music player, which warms a read-ahead window: `prefetchDepth()` returns 5 on Wi-Fi and 3 otherwise, so playing an album has six files in flight and the fourth was refused. Browsing a group is never subject to a transfer slot — that is a stated requirement, not a tuning parameter — and a constant nobody had checked against the client broke it. Twelve now: six for the music read-ahead at its widest, two for a photo viewer and its own prefetch in the same session, the rest as headroom. Generosity is cheap here and refusal is not — this is a fairness control among cooperating clients, not a security boundary, so a client that lies gets twelve files at a time instead of its member cap, bounded and audited, while refusing a legitimate read breaks the requirement outright. And the number is now derived rather than chosen: a test reads `prefetchDepth()` out of the shipped player and fails if the node's bound no longer covers it, so widening the client's read-ahead breaks the build instead of reaching a person. Checked by widening it: "the music player reads 21 files ahead and the node admits only 12". Three cases that hard-coded "two then refuse" now set their own limit — they are about the mechanism, and the shipped number moves with the client. Node suite 1210 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
Diffstat (limited to 'packages/meshbay-node/tests')
-rw-r--r--packages/meshbay-node/tests/test_leaseless_reads.py68
1 files changed, 57 insertions, 11 deletions
diff --git a/packages/meshbay-node/tests/test_leaseless_reads.py b/packages/meshbay-node/tests/test_leaseless_reads.py
index 70fd24f..64aed3d 100644
--- a/packages/meshbay-node/tests/test_leaseless_reads.py
+++ b/packages/meshbay-node/tests/test_leaseless_reads.py
@@ -19,10 +19,18 @@ is a download, and no size threshold separates them. What separates them is
which function asked.
"""
+import re
+from pathlib import Path
+
+import pytest
+
from meshbay_node.transfers import (
LEASELESS_IDLE_SECS, MAX_LEASELESS_IN_FLIGHT, LeaselessReads,
)
+SPA = (Path(__file__).resolve().parents[2] / "meshbay-hub" / "src"
+ / "meshbay_hub" / "static")
+
def test_a_viewer_looking_at_one_file_is_never_refused():
reads = LeaselessReads()
@@ -33,13 +41,16 @@ def test_a_viewer_looking_at_one_file_is_never_refused():
def test_a_second_file_is_allowed_so_prefetching_stays_possible():
"""One is what a viewer needs; two is so the photo viewer can fetch the
next one while showing this one."""
- reads = LeaselessReads()
+ reads = LeaselessReads(limit=2)
assert reads.admit("photo-1", now=0.0) is True
assert reads.admit("photo-2", now=0.0) is True
-def test_a_third_file_is_refused():
- reads = LeaselessReads()
+def test_a_file_past_the_limit_is_refused():
+ """The mechanism, at a limit this test sets itself: the shipped number is
+ derived from the client and moves, and a test that hard-codes it is a test
+ that breaks every time the client legitimately reads further ahead."""
+ reads = LeaselessReads(limit=2)
reads.admit("a", now=0.0)
reads.admit("b", now=0.0)
assert reads.admit("c", now=0.0) is False
@@ -49,7 +60,7 @@ def test_a_file_already_being_read_is_never_cut_off():
"""Even once the limit is reached. Refusing a chunk halfway through a photo
because the count moved would be worse than never having admitted it — the
viewer would show half an image and no error anyone can act on."""
- reads = LeaselessReads()
+ reads = LeaselessReads(limit=2)
reads.admit("a", now=0.0)
reads.admit("b", now=0.0)
assert reads.admit("c", now=0.0) is False
@@ -60,7 +71,7 @@ def test_finishing_one_frees_it_at_once():
"""The last chunk is the only "close" a leaseless read has. Waiting for the
idle timeout instead would mean somebody who looked at two photos cannot
look at a third for a minute."""
- reads = LeaselessReads()
+ reads = LeaselessReads(limit=2)
reads.admit("a", now=0.0)
reads.admit("b", now=0.0)
reads.finish("a")
@@ -71,15 +82,50 @@ def test_a_viewer_closed_mid_file_does_not_hold_its_place_for_ever():
"""It stops asking and says nothing — there is no message for "I closed the
tab". Without the idle expiry the session would carry two dead entries and
refuse every later preview, which is the bound turning into a bug."""
- reads = LeaselessReads()
+ reads = LeaselessReads(limit=2)
reads.admit("a", now=0.0)
reads.admit("b", now=0.0)
assert reads.admit("c", now=1.0) is False
assert reads.admit("c", now=LEASELESS_IDLE_SECS + 2) is True
-def test_the_bound_is_two():
- """Stated here so that changing it is a decision rather than a typo: it is
- the number §3.4.1 argues for, and the argument is about viewers, not about
- tuning."""
- assert MAX_LEASELESS_IN_FLIGHT == 2
+def test_the_bound_covers_what_the_music_player_actually_reads_ahead():
+ """The number is derived, not chosen — and choosing it is how it went wrong.
+
+ §3.4.1 argued for two by reasoning about viewers: one photo, one document,
+ plus one for prefetching the next. It forgot the music player, which warms a
+ read-ahead window; playing an album on Wi-Fi therefore has six files in
+ flight and the fourth was refused with `transfer_required`. Reported the day
+ MNP 3.0 shipped as "I try to play a track and it tells me to download it
+ instead" — a stated requirement (browsing is never subject to a slot) broken
+ by a constant nobody had checked against the client.
+
+ So this reads `prefetchDepth()` out of the shipped player and fails if the
+ node's bound no longer covers it. Raising the client's read-ahead without
+ raising the node's bound now breaks the build instead of reaching a person.
+ """
+ player = SPA / "music-player.js"
+ if not player.exists():
+ pytest.skip("the SPA sources are not present next to the node package")
+ body = player.read_text()
+ fn = body[body.index("function prefetchDepth()"):]
+ fn = fn[:fn.index("\n}\n")]
+ depths = [int(n) for n in re.findall(r"return (\d+);", fn)]
+ assert depths, "prefetchDepth() no longer returns a number this can read"
+
+ # The track playing, plus the widest read-ahead it will warm.
+ music = max(depths) + 1
+ # And a photo viewer with its own prefetch, in the same session: somebody
+ # can look at photos while an album plays.
+ viewer = 2
+ assert MAX_LEASELESS_IN_FLIGHT >= music + viewer, (
+ f"the music player reads {music} files ahead and the node admits only "
+ f"{MAX_LEASELESS_IN_FLIGHT} leaseless — playing an album would be "
+ "refused")
+
+
+def test_the_bound_is_still_a_bound():
+ """Generosity is cheap and refusal is not, but "unbounded" is the thing this
+ exists to prevent: a client that omits `tr` must not have a download channel
+ with no ceiling at all."""
+ assert MAX_LEASELESS_IN_FLIGHT < 64