aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/harness
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-09 14:28:40 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-09 14:28:40 +0200
commit7e2d078fe1870d256ae47781bee6ac4f454edf24 (patch)
tree05689049fd48f01ebbdb9995d5189cba15cee052 /packages/meshbay-hub/tests/harness
parent813d18424ec57963bb56e6f40824a2db0ccce50d (diff)
parente6f895c473a0b19e7b186889c1836d3945bc880b (diff)
downloadmeshbay-7e2d078fe1870d256ae47781bee6ac4f454edf24.tar.gz
Merge branch 'fix/large-download-paths'
Concurrent-transfer limits, with the queue, the pause and the flag day. A node now caps how many transfers it runs at once (8 downloads, 8 uploads, node-wide) and how many one member may run in one group (2 by default, operator-signed). Beyond that the node answers "queued" and the client waits its turn, visibly, in the transfers panel — and a slot that frees starts whatever is next, skipping past a member who is at their own cap rather than letting them stall everyone behind them. Browsing is never subject to a slot: not the poster grid, not the covers, not opening a photo to look at it. That is structural — a transfer is what the transfers widget shows — and the exemption is bounded rather than open, at two files in flight per session, because an exemption with no bound is a leaseless branch under another name. Transfers can be cancelled, and now paused and resumed. A paused one holds nothing: its slot goes back at once and resuming rejoins the queue at the tail. Uploads survive the connection that started them and resume where the node stopped, asked for inside the seal rather than on a clear message. What they leave behind when they are abandoned is reaped, which closes a disk leak that predates this work. MNP 3.0 makes the lease compulsory and refuses 2.x at the handshake, with the desktop client checking `client.minimum` before connecting so an un-updated one says "update" instead of failing every connection in a protocol vocabulary. Fourteen defects were found on the way, eight of them by a person clicking Download and pasting a console — none of which 2075 tests could reach. Section 12 of ~/next/improve-downloads.md is that report, including the three this work introduced itself and the one that turned out to be caused by an instruction to hard-reload after each deployment. Node suite 1209 passed, hub suite 866 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
Diffstat (limited to 'packages/meshbay-hub/tests/harness')
-rw-r--r--packages/meshbay-hub/tests/harness/chat_scroll_probe.py11
-rw-r--r--packages/meshbay-hub/tests/harness/chat_send_probe.py11
-rw-r--r--packages/meshbay-hub/tests/harness/group_tab_probe.py11
-rw-r--r--packages/meshbay-hub/tests/harness/layout_probe.py12
-rw-r--r--packages/meshbay-hub/tests/harness/scroll_probe.py10
-rw-r--r--packages/meshbay-hub/tests/harness/upload_seal_probe.mjs25
6 files changed, 73 insertions, 7 deletions
diff --git a/packages/meshbay-hub/tests/harness/chat_scroll_probe.py b/packages/meshbay-hub/tests/harness/chat_scroll_probe.py
index 7373976..17ec554 100644
--- a/packages/meshbay-hub/tests/harness/chat_scroll_probe.py
+++ b/packages/meshbay-hub/tests/harness/chat_scroll_probe.py
@@ -193,7 +193,15 @@ class H(http.server.BaseHTTPRequestHandler):
def main() -> int:
with socketserver.TCPServer(("127.0.0.1", PORT), H) as srv:
threading.Thread(target=srv.serve_forever, daemon=True).start()
- with tempfile.TemporaryDirectory() as profile:
+ # ignore_cleanup_errors: Chrome's children (zygote, renderer, gpu)
+ # outlive terminate() on the parent by a moment and go on writing into
+ # the profile. rmtree then walks a directory that gains a file between
+ # its readdir and its rmdir and raises "Directory not empty" -- which
+ # failed the probe, which failed every test in the file, intermittently
+ # and for a reason nowhere near the chat code they were testing. A few
+ # bytes left in a throwaway profile are harmless; failing the run is not.
+ with tempfile.TemporaryDirectory(
+ ignore_cleanup_errors=True) as profile:
# Real time, not `--virtual-time-budget`: the defect is a feedback
# loop between layout and an event, and a virtual clock does not
# run it.
@@ -211,6 +219,7 @@ def main() -> int:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
proc.kill()
+ proc.wait()
if not RECORDS:
print(json.dumps({"error": "no measurement"}), file=sys.stderr)
return 1
diff --git a/packages/meshbay-hub/tests/harness/chat_send_probe.py b/packages/meshbay-hub/tests/harness/chat_send_probe.py
index 5f99beb..28635b0 100644
--- a/packages/meshbay-hub/tests/harness/chat_send_probe.py
+++ b/packages/meshbay-hub/tests/harness/chat_send_probe.py
@@ -297,7 +297,15 @@ class H(http.server.BaseHTTPRequestHandler):
def main() -> int:
with socketserver.TCPServer(("127.0.0.1", PORT), H) as srv:
threading.Thread(target=srv.serve_forever, daemon=True).start()
- with tempfile.TemporaryDirectory() as profile:
+ # ignore_cleanup_errors: Chrome's children (zygote, renderer, gpu)
+ # outlive terminate() on the parent by a moment and go on writing into
+ # the profile. rmtree then walks a directory that gains a file between
+ # its readdir and its rmdir and raises "Directory not empty" -- which
+ # failed the probe, which failed every test in the file, intermittently
+ # and for a reason nowhere near the chat code they were testing. A few
+ # bytes left in a throwaway profile are harmless; failing the run is not.
+ with tempfile.TemporaryDirectory(
+ ignore_cleanup_errors=True) as profile:
proc = subprocess.Popen(
["google-chrome", "--headless=new", "--disable-gpu", "--no-sandbox",
f"--user-data-dir={profile}", "--window-size=1100,800",
@@ -312,6 +320,7 @@ def main() -> int:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
proc.kill()
+ proc.wait()
if not RECORDS:
print(json.dumps({"error": "no measurement"}), file=sys.stderr)
return 1
diff --git a/packages/meshbay-hub/tests/harness/group_tab_probe.py b/packages/meshbay-hub/tests/harness/group_tab_probe.py
index 5e4e452..b6d2bcc 100644
--- a/packages/meshbay-hub/tests/harness/group_tab_probe.py
+++ b/packages/meshbay-hub/tests/harness/group_tab_probe.py
@@ -156,7 +156,15 @@ class H(http.server.BaseHTTPRequestHandler):
def main() -> int:
with socketserver.TCPServer(("127.0.0.1", PORT), H) as srv:
threading.Thread(target=srv.serve_forever, daemon=True).start()
- with tempfile.TemporaryDirectory() as profile:
+ # ignore_cleanup_errors: Chrome's children (zygote, renderer, gpu)
+ # outlive terminate() on the parent by a moment and go on writing into
+ # the profile. rmtree then walks a directory that gains a file between
+ # its readdir and its rmdir and raises "Directory not empty" -- which
+ # failed the probe, which failed every test in the file, intermittently
+ # and for a reason nowhere near the chat code they were testing. A few
+ # bytes left in a throwaway profile are harmless; failing the run is not.
+ with tempfile.TemporaryDirectory(
+ ignore_cleanup_errors=True) as profile:
proc = subprocess.Popen(
["google-chrome", "--headless=new", "--disable-gpu", "--no-sandbox",
f"--user-data-dir={profile}", "--window-size=1100,900",
@@ -171,6 +179,7 @@ def main() -> int:
proc.wait(timeout=10)
except subprocess.TimeoutExpired:
proc.kill()
+ proc.wait()
if not RECORDS:
print(json.dumps({"error": "no measurement"}), file=sys.stderr)
return 1
diff --git a/packages/meshbay-hub/tests/harness/layout_probe.py b/packages/meshbay-hub/tests/harness/layout_probe.py
index 530b3f0..65b3083 100644
--- a/packages/meshbay-hub/tests/harness/layout_probe.py
+++ b/packages/meshbay-hub/tests/harness/layout_probe.py
@@ -19,6 +19,7 @@ single pass. Launching Chrome per width put three minutes on the test suite.
"""
import http.server
import json
+import shutil
import socketserver
import subprocess
import sys
@@ -118,16 +119,25 @@ def main() -> int:
srv = S(("127.0.0.1", PORT), H)
threading.Thread(target=srv.serve_forever, daemon=True).start()
+ # mkdtemp left a Chrome profile in /tmp on every run, for ever, and nothing
+ # waited for Chrome to exit. Same cleanup rule as the other probes.
+ profile = tempfile.mkdtemp(prefix="chrome-layout-")
chrome = subprocess.Popen([
"google-chrome", "--headless=new", "--no-sandbox",
"--window-size=1000,900",
- "--user-data-dir=" + tempfile.mkdtemp(prefix="chrome-layout-"),
+ "--user-data-dir=" + profile,
f"http://127.0.0.1:{PORT}/",
], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
deadline = time.time() + 45
while time.time() < deadline and not RECORDS:
time.sleep(0.2)
chrome.terminate()
+ try:
+ chrome.wait(timeout=10)
+ except subprocess.TimeoutExpired:
+ chrome.kill()
+ chrome.wait()
+ shutil.rmtree(profile, ignore_errors=True)
srv.shutdown()
if not RECORDS:
print(json.dumps({"error": "no measurement"}))
diff --git a/packages/meshbay-hub/tests/harness/scroll_probe.py b/packages/meshbay-hub/tests/harness/scroll_probe.py
index 46d8357..ae407b8 100644
--- a/packages/meshbay-hub/tests/harness/scroll_probe.py
+++ b/packages/meshbay-hub/tests/harness/scroll_probe.py
@@ -151,7 +151,15 @@ class H(http.server.BaseHTTPRequestHandler):
def main() -> int:
with socketserver.TCPServer(("127.0.0.1", PORT), H) as srv:
threading.Thread(target=srv.serve_forever, daemon=True).start()
- with tempfile.TemporaryDirectory() as profile:
+ # ignore_cleanup_errors: Chrome's children (zygote, renderer, gpu)
+ # outlive terminate() on the parent by a moment and go on writing into
+ # the profile. rmtree then walks a directory that gains a file between
+ # its readdir and its rmdir and raises "Directory not empty" -- which
+ # failed the probe, which failed every test in the file, intermittently
+ # and for a reason nowhere near the chat code they were testing. A few
+ # bytes left in a throwaway profile are harmless; failing the run is not.
+ with tempfile.TemporaryDirectory(
+ ignore_cleanup_errors=True) as profile:
subprocess.run(
["google-chrome", "--headless", "--disable-gpu", "--no-sandbox",
f"--user-data-dir={profile}", "--window-size=1100,1300",
diff --git a/packages/meshbay-hub/tests/harness/upload_seal_probe.mjs b/packages/meshbay-hub/tests/harness/upload_seal_probe.mjs
index 0b77e42..a6008c2 100644
--- a/packages/meshbay-hub/tests/harness/upload_seal_probe.mjs
+++ b/packages/meshbay-hub/tests/harness/upload_seal_probe.mjs
@@ -72,13 +72,34 @@ tp._nodeVersion = input.node_version;
const frames = [];
let uploadId = null;
+let answered = 0;
tp._send = (msg) => {
frames.push(toHex(msgpack_encode(msg)));
if (msg.upload_id) uploadId = msg.upload_id;
- if (input.mode !== 'receive') return;
+ if (input.mode !== 'receive') {
+ // Nothing answers in this mode -- except the probe, which the client waits
+ // five seconds for. A node that predates it refuses the index, and that
+ // refusal is a plain error rather than a sealed ack, so the harness can
+ // produce it honestly. It is also the degradation path worth exercising.
+ if (msg.chunk_index === -1) {
+ // With `probe_ack`, answer it the way a node holding part of this file
+ // does; without, the way one that predates the probe does.
+ const reply = input.probe_ack
+ ? Object.assign(msgpack_decode(hex(input.probe_ack)),
+ { upload_id: uploadId })
+ : { type: 'error', upload_id: uploadId,
+ code: 'bad_chunk_index', detail: 'Unexpected chunk index' };
+ setImmediate(() => tp._dispatch(reply));
+ }
+ return;
+ }
// Answer as the node did, on the next turn of the loop so the send path
// finishes first — which is also how a real ack arrives.
- const ack = msgpack_decode(hex(input.acks[msg.chunk_index]));
+ //
+ // By position, not by `chunk_index`: the node answers every frame including
+ // the probe, whose index is -1, and the two lists are built from the same
+ // sequence of frames.
+ const ack = msgpack_decode(hex(input.acks[answered++]));
ack.upload_id = uploadId;
// Through the real `_dispatch`, so the routing under test — matching an
// ack to its uploader by `upload_id` — is the shipped one.