diff options
Diffstat (limited to 'packages/meshbay-hub/tests/test_downloads.py')
| -rw-r--r-- | packages/meshbay-hub/tests/test_downloads.py | 68 |
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 |