aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_upload_seal_client.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-09 12:01:06 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-09 12:01:06 +0200
commit4f5d3d4ac151874f03c6fcc451d6b1d5bb1efb78 (patch)
tree4de7dbb57b7e4342c0ee41f26268aab90117f7df /packages/meshbay-hub/tests/test_upload_seal_client.py
parentd62b6a4e8985d0504e1825f6f8f663ccd64489ae (diff)
downloadmeshbay-4f5d3d4ac151874f03c6fcc451d6b1d5bb1efb78.tar.gz
feat: resume an interrupted upload, and pause one
Stage 8 of ~/next/improve-downloads.md, second half, plus the gap it exposed in stage 7. **Asking where to resume.** The node identifies an upload by (member, directory, filename), so a client resuming one has to name the file — and `transfer_open`, the obvious place to ask, travels in clear. Naming it there would undo exactly what sealing this path bought in MNP 2.0: before it, the same file was ciphertext leaving a node and plaintext arriving at one. So the question is asked inside the seal that already exists, as an ordinary `file_upload` with no bytes and `chunk_index: -1`. The node writes nothing, creates no state, reserves no name, and answers with `resume_from` in the sealed ack. A node that predates it refuses the index, which the client reads as "start from the beginning" — the behaviour it had anyway — and the wait is bounded so one that answers neither does not strand an upload. The probe is answered after every check the write path makes, so it cannot ask questions about a directory the caller may not write to, and it answers only about the member who asks: otherwise one member could measure another's progress on a file they never sent, and worse, resume it. **Pausing an upload.** Reported: no pause button on an upload, even in the desktop app. Stage 7 built pause around the download path — a target declares whether it can be stopped — and an upload has no local target to ask. It was also refused by design, since a transfer handed a lease it cannot re-create must not be offered a button that would drop its slot for good. Uploads now ask for their slot rather than being handed one, and say they are pausable outright: a File is seekable and the node keeps the position. Resuming re-probes rather than trusting the client's own memory, so it works across a reconnect too. **And the slot they hold.** `_do_file_upload` never called `slots.touch(tr)`. Chunks are not gated by the lease, so the file arrived — but the node reclaimed a grant nobody appeared to be using after thirty seconds, twice, then abandoned it, and the widget follows the lease. Measured from the journal: a 3.5 GB upload read "waiting, 0 ahead" for a minute and a half while it was transferring. The download twin of this was fixed on 2026-09-08; the same omission was still here, invisible until uploads took a real lease. `test_the_upload_itself_is_sealed` now checks every message `uploadFile` sends rather than the first. Adding the probe put a second one in front of the one it was written for, and it would have kept passing while guarding nothing. Node suite 1202 passed, hub suite 850 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/test_upload_seal_client.py')
-rw-r--r--packages/meshbay-hub/tests/test_upload_seal_client.py38
1 files changed, 38 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_upload_seal_client.py b/packages/meshbay-hub/tests/test_upload_seal_client.py
index d6f9156..2e4bfb5 100644
--- a/packages/meshbay-hub/tests/test_upload_seal_client.py
+++ b/packages/meshbay-hub/tests/test_upload_seal_client.py
@@ -167,3 +167,41 @@ def test_the_client_refuses_an_older_node_before_sending_a_chunk(_gek):
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
+ is missing.
+
+ Both halves are the shipped ones: the frames come from the real
+ `uploadFile`, the answer comes from the real node handler. What is asserted
+ is the thing that used to be impossible — an upload interrupted at chunk two
+ of five that sends three chunks instead of five.
+ """
+ body = bytes(range(256)) * ((CHUNK * 5) // 256 + 1)
+ body = body[:CHUNK * 5]
+ first = _run_probe(_probe_input(_gek, "send",
+ file={"name": "film.mkv", "data": body.hex()}))
+ frames = [msgpack.unpackb(bytes.fromhex(f), raw=False)
+ for f in first["frames"]]
+ assert [f["chunk_index"] for f in frames] == [-1, 0, 1, 2, 3, 4]
+
+ # The link drops after two chunks.
+ session = _node_session(tmp_path, _gek)
+ for frame in frames[1:3]:
+ session._do_file_upload(frame)
+ assert not [m for m in session.sent if m.get("type") == "error"]
+
+ # It comes back and asks.
+ session.sent.clear()
+ session._do_file_upload(frames[0])
+ probe_ack = msgpack.packb(session.sent[-1], use_bin_type=True).hex()
+
+ second = _run_probe(_probe_input(
+ _gek, "send", file={"name": "film.mkv", "data": body.hex()},
+ probe_ack=probe_ack))
+ resumed = [msgpack.unpackb(bytes.fromhex(f), raw=False)["chunk_index"]
+ for f in second["frames"]]
+ assert resumed == [-1, 2, 3, 4], (
+ f"sent {resumed} — the answer to the probe was not used")