aboutsummaryrefslogtreecommitdiffstats
path: root/packages
diff options
context:
space:
mode:
Diffstat (limited to 'packages')
-rwxr-xr-xpackages/meshbay-node/tests/transfer_probe.py114
1 files changed, 114 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/transfer_probe.py b/packages/meshbay-node/tests/transfer_probe.py
index fb14533..bf18a5e 100755
--- a/packages/meshbay-node/tests/transfer_probe.py
+++ b/packages/meshbay-node/tests/transfer_probe.py
@@ -241,6 +241,113 @@ async def pull_together(client, entries, CHUNK) -> int:
return 1 if failures else 0
+def _cli(*argv) -> str:
+ """Run `meshbay-node …` the way the operator does, and return its output."""
+ import subprocess
+ out = subprocess.run(["meshbay-node", *argv], capture_output=True,
+ text=True, timeout=30)
+ if out.returncode != 0:
+ raise RuntimeError(f"meshbay-node {' '.join(argv)}: {out.stderr.strip()}")
+ return out.stdout
+
+
+async def operator_checks(client, ack, group, node_id) -> int:
+ """The two things only the operator's side can answer.
+
+ Both are about a promise made elsewhere: draft-v6 §2.11 says these settings
+ apply without a restart, and §5 of the transfer-slots plan says a lost peer
+ gives its slots back through a hook rather than a timeout. Neither can be
+ checked from inside the client, and both were wrong at some point today —
+ the live cap because the hot-swap wrote to an attribute that never existed,
+ and the operator's view because it reported the module defaults.
+ """
+ import asyncio as _a
+ import json as _json
+ import re as _re
+
+ failures = 0
+ member_cap = int((ack.get("transfer_limits") or {}).get("download") or 0)
+
+ # The queue has to be held by the *node* cap, not by this member's own.
+ #
+ # With both at 2, one account holding two transfers hits both at once, and
+ # raising the node-wide cap then correctly changes nothing — per-member is
+ # checked first, by design. An earlier version of this check set it up that
+ # way and reported the design working as a failure. So: node cap to 1, well
+ # under the member cap, and the third transfer is waiting on the machine.
+ was = _cli("transfers", "show")
+ prior = int(_re.search(r"download\s+\d+/(\d+)", was).group(1))
+ _cli("transfers", "set", "1", "1")
+
+ granted_tr, first = await _open_transfer(client)
+ tr_waiting, waiting = await _open_transfer(client)
+ ok = first.get("state") == "granted" and waiting.get("state") == "queued"
+ _line(ok, "with the node cap at 1, the second transfer waits on the machine "
+ f"rather than on this member's own cap of {member_cap}")
+ failures += not ok
+
+ # ── 1. the operator can see it ────────────────────────────────────────
+ shown = _cli("transfers", "show")
+ # The lease table only: "queued" also appears in each pool's summary line,
+ # and counting those reported three queues where there was one.
+ rows = [ln for ln in shown.splitlines() if " download " in ln
+ or " upload " in ln]
+ seen = sum(1 for ln in rows if " granted " in ln)
+ queued = sum(1 for ln in rows if " queued " in ln)
+ ok = seen == 1 and queued == 1
+ _line(ok, f"`transfers show` lists {seen} granted and {queued} queued lease(s)")
+ failures += not ok
+ if not ok:
+ print(" the operator's only window into a stuck queue is wrong")
+ print(" " + shown.replace("\n", "\n "))
+
+ # ── 2. raising the cap live starts what was waiting ───────────────────
+ _cli("transfers", "set", "4", "4")
+ try:
+ started = await _a.wait_for(
+ _wait_for_grant(client, tr_waiting), timeout=20)
+ except _a.TimeoutError:
+ started = False
+ _line(started, "raising the cap started the waiting transfer, with no "
+ "restart and no reconnection")
+ failures += not started
+ if not started:
+ print(" draft-v6 §2.11 promises this applies live; the setting "
+ "was accepted and nothing moved")
+
+ # ── 3. a vanished peer's slots are back before anyone asks ────────────
+ await client.close()
+ await _a.sleep(1.0)
+ after = _cli("transfers", "show")
+ ok = "nothing transferring" in after
+ _line(ok, "every slot came back when the peer vanished, with no timeout")
+ failures += not ok
+ if not ok:
+ print(" " + after.replace("\n", "\n "))
+
+ # Put the operator's cap back where it was found — not at a default, at
+ # whatever this node was running before the probe touched it.
+ _cli("transfers", "set", str(prior), str(prior))
+ print(f"\n (node cap restored to {prior})")
+
+ print()
+ print("all operator checks passed" if not failures
+ else f"{failures} operator check(s) failed")
+ return 1 if failures else 0
+
+
+async def _wait_for_grant(client, tr: str) -> bool:
+ """The node pushes the grant; nothing here polls for it.
+
+ A grant that is decided and never sent is the "stuck at waiting" report the
+ whole design exists to prevent, and it looks perfectly correct in the pool.
+ """
+ while True:
+ msg = await client.recv_type("transfer_state", timeout=30)
+ if msg.get("tr") == tr and msg.get("state") == "granted":
+ return True
+
+
async def probe(args) -> int:
cfg = env()
hub = cfg["HUB_URL"]
@@ -285,6 +392,9 @@ async def probe(args) -> int:
cap = int(limits.get("download") or 0)
print(f"node reports this member may run {cap} download(s) at once\n")
+ if args.operator:
+ return await operator_checks(alice, ack, group, node_id)
+
if args.pull:
return await pull(alice, ack, args.pull, args.parallel)
@@ -406,6 +516,10 @@ def main() -> int:
ap.add_argument("--pull", type=int, default=0, metavar="N",
help="actually download N files to completion, under a "
"lease, and report where they stop")
+ ap.add_argument("--operator", action="store_true",
+ help="the two checks that need the operator's CLI: a cap "
+ "raised live starts what was waiting, and a vanished "
+ "peer's slots are back before anyone asks")
ap.add_argument("--parallel", action="store_true",
help="pull them at the same time on one connection, the "
"way a browser does — which is when it goes wrong")