summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-09 10:59:59 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-09 10:59:59 +0200
commit99ae7f6955ffc94cd973822d4e2fd5a8c952f563 (patch)
tree95337150859a2511b81832ca6f029f723085e9fe /packages/meshbay-hub
parent29e93e5e553c94818cd2b4e587b0e54cfe7d8424 (diff)
downloadmeshbay-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')
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/de.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/en.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/es.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/fr.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/it.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/ja.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/nl.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pl.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/pt-BR.js2
-rw-r--r--packages/meshbay-hub/src/meshbay_hub/static/locales/zh-CN.js2
-rw-r--r--packages/meshbay-hub/tests/test_memory_ceiling.py63
-rw-r--r--packages/meshbay-hub/tests/test_streamed_download_reliability.py19
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