aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_transport_contracts.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_transport_contracts.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_transport_contracts.py')
-rw-r--r--packages/meshbay-hub/tests/test_transport_contracts.py33
1 files changed, 23 insertions, 10 deletions
diff --git a/packages/meshbay-hub/tests/test_transport_contracts.py b/packages/meshbay-hub/tests/test_transport_contracts.py
index 879062b..fe550f9 100644
--- a/packages/meshbay-hub/tests/test_transport_contracts.py
+++ b/packages/meshbay-hub/tests/test_transport_contracts.py
@@ -349,16 +349,29 @@ def test_the_upload_itself_is_sealed(transport):
"the upload must be sealed under the group key")
assert "openGroup(" in body and "'file_upload_ack'" in body, (
"the ack carries the stored name and must be opened, not read")
- # The message the node actually receives: everything between `this._send({`
- # and its close. Read on its own, because the same field names appear a few
- # lines above inside `msgpack_encode({...})`, which is the sealed half.
- sent = body[body.index("this._send({"):]
- sent = sent[:sent.index("});")]
- assert "filename" not in sent, "the filename is on the message in clear"
- assert "data" not in sent, "the bytes are on the message in clear"
- assert "dir" not in sent and "root" not in sent, (
- "the destination is on the message in clear")
- assert "...sealed," in sent, "the message must carry the sealed pair"
+ # The messages the node actually receives: everything between each
+ # `this._send({` and its close. Read on their own, because the same field
+ # names appear a few lines above inside `msgpack_encode({...})`, which is
+ # the sealed half.
+ #
+ # Every one of them, not the first: `uploadFile` sends a probe chunk before
+ # the file ("where am I?", UPLOAD_PROBE_INDEX) and it names the file too, so
+ # a check that stopped at the first message would have moved off the one it
+ # was written for the day the second appeared.
+ sends = []
+ rest = body
+ while "this._send({" in rest:
+ rest = rest[rest.index("this._send({"):]
+ sends.append(rest[:rest.index("});")])
+ rest = rest[len("this._send({"):]
+ assert len(sends) >= 2, "the probe and the chunks are both sent from here"
+ for sent in sends:
+ assert "filename" not in sent, "the filename is on the message in clear"
+ assert "data" not in sent, "the bytes are on the message in clear"
+ assert "dir" not in sent and "root" not in sent, (
+ "the destination is on the message in clear")
+ assert "...sealed," in sent or "...probeSealed," in sent, (
+ "the message must carry the sealed pair")
assert "supportsSealedUpload" in body, (
"an older node must be refused before a chunk is sent, not after")