aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_downloads.py
diff options
context:
space:
mode:
Diffstat (limited to 'packages/meshbay-hub/tests/test_downloads.py')
-rw-r--r--packages/meshbay-hub/tests/test_downloads.py68
1 files changed, 68 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_downloads.py b/packages/meshbay-hub/tests/test_downloads.py
index dfb1433..41ae5d3 100644
--- a/packages/meshbay-hub/tests/test_downloads.py
+++ b/packages/meshbay-hub/tests/test_downloads.py
@@ -489,3 +489,71 @@ out.push(asked);
assert batched == [False, True, True], (
"the stuck one is still the only holder of the gesture, so the released "
"openings must not try for a dialog of their own")
+
+
+def test_a_pause_falls_between_chunks_and_resumes_at_one(tmp_path):
+ """What makes resuming exact rather than approximate.
+
+ Everything written is a whole number of chunks, because the loop checks for
+ a pause between two of them and never inside one. So `fromChunk` is a
+ position, not an estimate, and a resumed download is never appended to at an
+ offset nobody verified — the failure mode being avoided is a file that looks
+ complete and is quietly corrupt.
+
+ The real `pipelinedDownload` is lifted out and run against stubs, on the
+ rule this repo follows for the video player: model the environment, never
+ the code under test.
+ """
+ src = (STATIC / "file-utils.js").read_text()
+ fn = src[src.index("async function pipelinedDownload"):]
+ fn = fn[:fn.index("\n}\n") + 2]
+
+ script = tmp_path / "pipeline.mjs"
+ script.write_text("""
+const CHUNK_SIZE = 8;
+const PIPELINE_WINDOW = 4;
+const written = [];
+// `ct` has to be truthy: chunk 0 with a falsy body is refused as undecryptable,
+// which is the guard working, not the harness.
+const _fetchChunkResilient = async (transport, fileId, i) =>
+ ({ ct: new Uint8Array([i & 0xff]), nonce: new Uint8Array(12) });
+const _writeOrStall = async (w, bytes, index) => { written.push(index); };
+globalThis.window = { MeshBayCrypto: {
+ // The plaintext carries its own index, so what lands where can be checked.
+ decryptChunkBin: async (k, id, index) => ({ byteLength: CHUNK_SIZE, index }),
+} };
+""" + fn + """
+const out = {};
+const signal = { aborted: false, paused: false };
+// Stop it part way, the way the store does.
+let seen = 0;
+const onChunk = () => { if (++seen === 3) signal.paused = true; };
+try {
+ await pipelinedDownload({}, 'k', 'file', 10, onChunk, {}, signal, '', 0);
+ out.threw = 'no';
+} catch (err) {
+ out.threw = err.name;
+}
+out.resumeFrom = signal.resumeFrom;
+out.writtenBeforePause = written.slice();
+
+// And again, from where it said.
+signal.paused = false;
+written.length = 0;
+await pipelinedDownload({}, 'k', 'file', 10, () => {}, {}, signal, '',
+ out.resumeFrom);
+out.writtenAfterResume = written.slice();
+console.log(JSON.stringify(out));
+""")
+ proc = subprocess.run(["node", str(script)], capture_output=True, text=True)
+ assert proc.returncode == 0, proc.stderr
+ out = json.loads(proc.stdout)
+
+ assert out["threw"] == "PausedError", out
+ # Whole chunks only, in order, with nothing skipped.
+ assert out["writtenBeforePause"] == list(range(len(out["writtenBeforePause"])))
+ assert out["resumeFrom"] == len(out["writtenBeforePause"]), (
+ f"stopped after {len(out['writtenBeforePause'])} chunks but asked to "
+ f"resume at {out['resumeFrom']} — that gap is a hole in the file")
+ # The resumed run covers exactly the rest, and repeats nothing.
+ assert out["writtenAfterResume"] == list(range(out["resumeFrom"], 10)), out