From 99ae7f6955ffc94cd973822d4e2fd5a8c952f563 Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Wed, 9 Sep 2026 10:59:59 +0200 Subject: docs(spa): say that a download with no folder cannot be paused MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported from testing 7a: pause worked in the desktop app and no button appeared in Chrome. That is the design working — without a granted download folder, "save automatically" means the service worker, and that target is a download the browser already owns — but nothing anywhere said so, and choosing a folder looked like a question of where files land. So the Settings line now says what it costs not to choose one, in all ten catalogues. It renders only where a folder can be chosen at all, which is exactly the browsers the advice applies to. Also pins the tier table the pause button is drawn from: a granted folder, a save dialog and the desktop sink can be paused, a service-worker stream cannot. Four cases through the real `_openDownloadTarget`, and one more that reads the value off the real `downloads.js` rather than a stub of it -- the first version of these stubs did not carry the field at all, so the cases would have passed while checking nothing. Hub suite 847 passed. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST --- packages/meshbay-hub/tests/test_memory_ceiling.py | 63 +++++++++++++++++++---- 1 file changed, 52 insertions(+), 11 deletions(-) (limited to 'packages/meshbay-hub/tests/test_memory_ceiling.py') diff --git a/packages/meshbay-hub/tests/test_memory_ceiling.py b/packages/meshbay-hub/tests/test_memory_ceiling.py index d46cae3..1654825 100644 --- a/packages/meshbay-hub/tests/test_memory_ceiling.py +++ b/packages/meshbay-hub/tests/test_memory_ceiling.py @@ -90,9 +90,12 @@ const downloads = {{ // wrong failure entirely. lastStreamFailure: () => 'stubbed: no streamed target in this harness', getMode: () => {json.dumps(mode)}, - openTarget: async () => ({json.dumps(granted)} ? {{ name: 'g', writable: {{}} }} : null), + // `pausable` mirrors the real modules: a granted folder is a held-open file + // handle, a service-worker stream is a download the browser already owns. + openTarget: async () => + ({json.dumps(granted)} ? {{ name: 'g', writable: {{}}, pausable: true }} : null), openStreamedDownload: async () => - ({json.dumps(streamed)} ? {{ name: 's', writable: {{}} }} : null), + ({json.dumps(streamed)} ? {{ name: 's', writable: {{}}, pausable: false }} : null), }}; globalThis.window = {{}}; if ({json.dumps(picker)}) {{ @@ -115,7 +118,7 @@ try {{ {{ batched: {json.dumps(batched)} }}); outcome = r === null ? {{ kind: 'memory' }} : r === false ? {{ kind: 'cancelled' }} - : {{ kind: 'stream', name: r.name }}; + : {{ kind: 'stream', name: r.name, pausable: !!r.pausable }}; }} catch (err) {{ outcome = {{ kind: 'refused', name: err.name, message: err.message }}; }} @@ -164,17 +167,17 @@ def test_the_boundary_is_the_ceiling_itself(target_fn, tmp_path): def test_a_granted_folder_streams_whatever_the_size(target_fn, tmp_path): out = _run(target_fn, tmp_path, size=20 * GB, granted=True) - assert out == {"kind": "stream", "name": "g"} + assert (out["kind"], out["name"]) == ("stream", "g") def test_the_service_worker_streams_whatever_the_size(target_fn, tmp_path): out = _run(target_fn, tmp_path, size=20 * GB, streamed=True) - assert out == {"kind": "stream", "name": "s"} + assert (out["kind"], out["name"]) == ("stream", "s") def test_the_desktop_app_streams_whatever_the_size(target_fn, tmp_path): out = _run(target_fn, tmp_path, size=20 * GB, native=True) - assert out == {"kind": "stream", "name": "n"} + assert (out["kind"], out["name"]) == ("stream", "n") def test_a_browser_with_a_picker_is_offered_one_instead_of_being_refused( @@ -182,7 +185,7 @@ def test_a_browser_with_a_picker_is_offered_one_instead_of_being_refused( """Chrome/Edge: the file is large, nothing streamed yet, but Save As does. A refusal here would be this fix breaking a path that was never broken.""" out = _run(target_fn, tmp_path, size=20 * GB, picker=True) - assert out == {"kind": "stream", "name": "p"} + assert (out["kind"], out["name"]) == ("stream", "p") # ── One dialog per gesture, not one per file ──────────────────────────────── @@ -192,7 +195,7 @@ def test_the_first_of_a_batch_still_asks_where_to_save(target_fn, tmp_path): folder chooses it, for the download they actually clicked.""" out = _run(target_fn, tmp_path, size=20 * GB, mode="ask", picker=True, streamed=True) - assert out == {"kind": "stream", "name": "p"} + assert (out["kind"], out["name"]) == ("stream", "p") def test_the_rest_of_a_batch_stream_instead_of_asking(target_fn, tmp_path): @@ -207,7 +210,7 @@ def test_the_rest_of_a_batch_stream_instead_of_asking(target_fn, tmp_path): """ out = _run(target_fn, tmp_path, size=20 * GB, mode="ask", picker=True, streamed=True, batched=True) - assert out == {"kind": "stream", "name": "s"} + assert (out["kind"], out["name"]) == ("stream", "s") def test_a_batched_download_falls_back_to_the_dialog_rather_than_failing( @@ -218,7 +221,7 @@ def test_a_batched_download_falls_back_to_the_dialog_rather_than_failing( neither must the fix for one.""" out = _run(target_fn, tmp_path, size=20 * GB, mode="ask", picker=True, streamed=False, batched=True) - assert out == {"kind": "stream", "name": "p"} + assert (out["kind"], out["name"]) == ("stream", "p") def test_batching_never_pushes_a_large_file_into_memory(target_fn, tmp_path): @@ -277,7 +280,7 @@ def test_a_lost_gesture_streams_instead_of_failing(target_fn, tmp_path): """ out = _run(target_fn, tmp_path, size=20 * GB, picker="no-gesture", streamed=True, mode="ask") - assert out == {"kind": "stream", "name": "s"}, out + assert (out["kind"], out["name"]) == ("stream", "s"), out def test_a_lost_gesture_with_nothing_to_stream_to_still_refuses(target_fn, tmp_path): @@ -286,3 +289,41 @@ def test_a_lost_gesture_with_nothing_to_stream_to_still_refuses(target_fn, tmp_p out = _run(target_fn, tmp_path, size=20 * GB, picker="no-gesture", streamed=False, mode="ask") assert out["kind"] == "refused", out + + +# ── Which targets can be paused ───────────────────────────────────────────── +# +# `pausable` travels with the target rather than with the platform, because the +# same browser yields both answers on the same page: a granted folder is a +# held-open file, and a service-worker stream is a download the browser already +# owns. The widget draws its button from this and nothing else. + + +def test_a_granted_folder_can_be_paused(tmp_path, target_fn): + out = _run(target_fn, tmp_path, size=20 * GB, granted=True) + assert out["pausable"] is True + + +def test_a_save_dialog_can_be_paused(tmp_path, target_fn): + out = _run(target_fn, tmp_path, size=20 * GB, picker=True) + assert out["pausable"] is True + + +def test_the_desktop_sink_can_be_paused(tmp_path, target_fn): + out = _run(target_fn, tmp_path, size=20 * GB, native=True) + assert out["pausable"] is True + + +def test_a_service_worker_stream_cannot_be_paused(tmp_path, target_fn): + """Not a shortcoming of this code. The browser is already writing an HTTP + response into its own download folder: not feeding the stream stalls that + download where we can neither see nor resume it, and an idle worker is + terminated within seconds. Firefox and Safari have no other target, so they + get cancel and no pause — the browser's own download manager is where a + pause lives there, for as long as it works. + + This is also why Chrome shows no pause button until a download folder has + been granted: without one, "save automatically" means the service worker. + """ + out = _run(target_fn, tmp_path, size=20 * GB, streamed=True) + assert out["pausable"] is False -- cgit v1.2.3