diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-09-09 10:59:59 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-09-09 10:59:59 +0200 |
| commit | 99ae7f6955ffc94cd973822d4e2fd5a8c952f563 (patch) | |
| tree | 95337150859a2511b81832ca6f029f723085e9fe /packages/meshbay-hub | |
| parent | 29e93e5e553c94818cd2b4e587b0e54cfe7d8424 (diff) | |
| download | meshbay-99ae7f6955ffc94cd973822d4e2fd5a8c952f563.tar.gz | |
docs(spa): say that a download with no folder cannot be paused
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
Diffstat (limited to 'packages/meshbay-hub')
12 files changed, 81 insertions, 21 deletions
diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js index ae20687..fd63ddf 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/de.js @@ -317,7 +317,7 @@ export default { + 'auch wenn Sie eine Auswahl herunterladen.', 'settings.dl_folder': 'Ordner: {name}', 'settings.dl_no_folder': 'Kein Ordner ausgewählt — Downloads landen dort, wo Ihr ' - + 'Browser sie ablegt', + + 'Browser sie ablegt, und sie lassen sich nicht anhalten', 'settings.dl_choose': 'Ordner auswählen', 'settings.dl_change': 'Ändern', 'settings.dl_forget': 'Verwerfen', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js index 7033778..e9d4072 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/en.js @@ -315,7 +315,7 @@ export default { + 'including when you download a selection.', 'settings.dl_folder': 'Folder: {name}', 'settings.dl_no_folder': 'No folder chosen — downloads go wherever your browser ' - + 'puts them', + + 'puts them, and they cannot be paused', 'settings.dl_choose': 'Choose folder', 'settings.dl_change': 'Change', 'settings.dl_forget': 'Forget', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js index 311ba1f..4fa19e7 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/es.js @@ -315,7 +315,7 @@ export default { + 'también cuando descarga una selección.', 'settings.dl_folder': 'Carpeta: {name}', 'settings.dl_no_folder': 'Ninguna carpeta elegida — las descargas van adonde las ' - + 'ponga su navegador', + + 'ponga su navegador, y no se pueden pausar', 'settings.dl_choose': 'Elegir carpeta', 'settings.dl_change': 'Cambiar', 'settings.dl_forget': 'Olvidar', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js index 106e3cd..6fba91d 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js @@ -316,7 +316,7 @@ export default { + 'un par fichier, y compris lorsque vous téléchargez une sélection.', 'settings.dl_folder': 'Dossier : {name}', 'settings.dl_no_folder': 'Aucun dossier choisi — les téléchargements vont là où ' - + 'votre navigateur les place', + + 'votre navigateur les place, et ne peuvent pas être suspendus', 'settings.dl_choose': 'Choisir un dossier', 'settings.dl_change': 'Changer', 'settings.dl_forget': 'Oublier', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js index a610683..76b3101 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/it.js @@ -316,7 +316,7 @@ export default { + 'file, anche quando scarica una selezione.', 'settings.dl_folder': 'Cartella: {name}', 'settings.dl_no_folder': 'Nessuna cartella scelta — i download finiscono dove li ' - + 'colloca il browser', + + 'colloca il browser, e non si possono sospendere', 'settings.dl_choose': 'Scegli una cartella', 'settings.dl_change': 'Cambia', 'settings.dl_forget': 'Dimentica', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js index aed4ba0..cfc1125 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js @@ -313,7 +313,7 @@ export default { + 'まとめてダウンロードする場合も 1 ファイルにつき 1 回です。', 'settings.dl_folder': 'フォルダー:{name}', 'settings.dl_no_folder': 'フォルダーが選ばれていません。ダウンロードは' - + 'ブラウザーが決めた場所に保存されます', + + 'ブラウザーが決めた場所に保存され、一時停止できません', 'settings.dl_choose': 'フォルダーを選択', 'settings.dl_change': '変更', 'settings.dl_forget': '解除', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js index fe10e12..29cc566 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js @@ -317,7 +317,7 @@ export default { + 'ook wanneer u een selectie downloadt.', 'settings.dl_folder': 'Map: {name}', 'settings.dl_no_folder': 'Geen map gekozen — downloads komen terecht waar uw browser ' - + 'ze neerzet', + + 'ze neerzet, en ze kunnen niet worden gepauzeerd', 'settings.dl_choose': 'Map kiezen', 'settings.dl_change': 'Wijzigen', 'settings.dl_forget': 'Vergeten', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js index c2ba35c..38fe714 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js @@ -328,7 +328,7 @@ export default { + 'także przy pobieraniu zaznaczonych pozycji.', 'settings.dl_folder': 'Folder: {name}', 'settings.dl_no_folder': 'Nie wybrano folderu — pobrane pliki trafiają tam, gdzie ' - + 'umieszcza je przeglądarka', + + 'umieszcza je przeglądarka, i nie można ich wstrzymać', 'settings.dl_choose': 'Wybierz folder', 'settings.dl_change': 'Zmień', 'settings.dl_forget': 'Zapomnij', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js index 83d179b..5942b77 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js @@ -317,7 +317,7 @@ export default { + 'arquivo, inclusive quando você baixa uma seleção.', 'settings.dl_folder': 'Pasta: {name}', 'settings.dl_no_folder': 'Nenhuma pasta escolhida — os downloads vão para onde o ' - + 'seu navegador os colocar', + + 'seu navegador os colocar, e não podem ser pausados', 'settings.dl_choose': 'Escolher pasta', 'settings.dl_change': 'Alterar', 'settings.dl_forget': 'Esquecer', diff --git a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js index 9acfb17..56af6c8 100644 --- a/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js +++ b/packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js @@ -309,7 +309,7 @@ export default { 'settings.dl_ask_hint': '每个文件弹出一次“另存为”对话框——每个文件一次,' + '批量下载时也是如此。', 'settings.dl_folder': '文件夹:{name}', - 'settings.dl_no_folder': '未选择文件夹——下载内容会保存到浏览器指定的位置', + 'settings.dl_no_folder': '未选择文件夹——下载内容会保存到浏览器指定的位置,且无法暂停', 'settings.dl_choose': '选择文件夹', 'settings.dl_change': '更改', 'settings.dl_forget': '忘记', 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 diff --git a/packages/meshbay-hub/tests/test_streamed_download_reliability.py b/packages/meshbay-hub/tests/test_streamed_download_reliability.py index 355fbff..da745d0 100644 --- a/packages/meshbay-hub/tests/test_streamed_download_reliability.py +++ b/packages/meshbay-hub/tests/test_streamed_download_reliability.py @@ -501,3 +501,22 @@ def test_a_download_waits_for_the_self_test(tmp_path): """) assert out["target"] is True assert out["ms"] >= 1, "the download did not wait for priming at all" + + +def test_the_streamed_target_says_it_cannot_be_paused(tmp_path): + """The value the widget's pause button is drawn from, read off the real + module rather than a stub of it. + + It is false for a reason that is not about this code: the browser is already + writing an HTTP response into its own download folder, so not feeding the + stream stalls a download we can neither see nor resume, and an idle worker + is terminated within seconds. Firefox and Safari therefore get cancel and no + pause; Chrome gets one as soon as a download folder has been granted, which + yields a held-open file instead of this. + """ + out = _run(tmp_path, """ + const target = await M.openStreamedDownload('film.mkv', 20e9, FAST); + out.pausable = target && target.pausable; + if (target) await target.writable.close(); + """) + assert out["pausable"] is False |