summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/tests/test_memory_ceiling.py
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-09 01:54:10 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-09 01:54:10 +0200
commitd6c4808d9a3ef6bc740cda7892508f15ee9ea030 (patch)
treeb9420bd6cc86ac0a72aef09d5ca0ee7e25a3533f /packages/meshbay-hub/tests/test_memory_ceiling.py
parenta0b070e5fd382bb4a4262637836cb8d4b268fbf7 (diff)
downloadmeshbay-d6c4808d9a3ef6bc740cda7892508f15ee9ea030.tar.gz
fix(spa): one save dialog per batch, not one per file
Selecting four files on Chrome produced a Save As dialog for the first, then one for the second only after that file had finished, while the last two timed out; on a later attempt the three remaining transfers appeared frozen. Two things were going on. Opening the target inside `prepare` had removed the accidental serialisation that `for (…) await downloadFile(e)` used to provide, so `_openTargetInTurn` now queues the openings — but a queue whose head is an unanswered dialog is a head-of-line block, which is what the "freeze" was. The code already recovered from a picker with no gesture behind it by streaming instead, on the `SecurityError` Chrome throws. That branch was never reached: Chrome does not throw, it shows the dialog anyway and waits for a human. So anything that has to wait its turn is now marked `batched`, and a batched opening prefers the streamed path whatever the download mode says. The first file of a batch — the one actually holding the gesture — still gets its dialog, so the preference is honoured where it can be. For the rest there is no gesture left to spend and nothing is lost by streaming: the file still lands on disk, in the browser's own download folder. Only the choice of folder goes, and it was not on offer. If the worker does not answer, a batched download falls back to the dialog rather than failing. Also logs which path led to a dialog. A dialog is the one outcome nobody can diagnose after the fact — it looks the same whether it was asked for or fallen back to — and the report this fixes needed three test cycles to narrow. The two harnesses that lift `_openDownloadTarget` as text now route console.info to stderr, since they parse stdout as JSON. Measured against the deployed hub in Chrome 152: the streamed path serves the hidden iframe in 2-3 ms on a normal load, after a hard reload (via the `mbdl-claim` recovery already in `_claimController`), and twice in the same document. Hub suite 824 passed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HCGdheDLxGReuKHga3BtST
Diffstat (limited to 'packages/meshbay-hub/tests/test_memory_ceiling.py')
-rw-r--r--packages/meshbay-hub/tests/test_memory_ceiling.py53
1 files changed, 51 insertions, 2 deletions
diff --git a/packages/meshbay-hub/tests/test_memory_ceiling.py b/packages/meshbay-hub/tests/test_memory_ceiling.py
index 8430627..d46cae3 100644
--- a/packages/meshbay-hub/tests/test_memory_ceiling.py
+++ b/packages/meshbay-hub/tests/test_memory_ceiling.py
@@ -66,12 +66,16 @@ def target_fn():
def _run(target_fn, tmp_path, *, size, native=False, granted=False,
- streamed=False, picker=False, mode="auto"):
+ streamed=False, picker=False, mode="auto", batched=False):
"""Drive the real function against one browser shape."""
script = tmp_path / "case.mjs"
script.write_text(f"""
// Stubs for everything the lifted function reaches. `formatSize` and `t` only
// build the message; the assertions are about which branch was taken.
+//
+// stdout carries the outcome and nothing else, so the function's own logging
+// goes to stderr -- where it is still shown when a case fails.
+console.info = (...a) => console.error(...a);
const formatSize = (n) => `${{n}} B`;
const t = (key, vars) => key + ' ' + JSON.stringify(vars);
const platform = {{
@@ -107,7 +111,8 @@ if ({json.dumps(picker)}) {{
let outcome;
try {{
- const r = await _openDownloadTarget('film.mkv', {size});
+ const r = await _openDownloadTarget('film.mkv', {size}, {{}}, {size},
+ {{ batched: {json.dumps(batched)} }});
outcome = r === null ? {{ kind: 'memory' }}
: r === false ? {{ kind: 'cancelled' }}
: {{ kind: 'stream', name: r.name }};
@@ -180,6 +185,50 @@ def test_a_browser_with_a_picker_is_offered_one_instead_of_being_refused(
assert out == {"kind": "stream", "name": "p"}
+# ── One dialog per gesture, not one per file ────────────────────────────────
+
+def test_the_first_of_a_batch_still_asks_where_to_save(target_fn, tmp_path):
+ """The preference is not being taken away. Someone who asked to choose the
+ 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"}
+
+
+def test_the_rest_of_a_batch_stream_instead_of_asking(target_fn, tmp_path):
+ """A browser grants one picker per user gesture and selecting four files is
+ one gesture. Chrome showed the dialog for the second file anyway and then
+ waited for a human, so the third and fourth sat behind it until they timed
+ out — reported as three downloads frozen.
+
+ There is no gesture left to spend, so nothing is lost by streaming: the file
+ still lands on disk, in the browser's own download folder. Only the choice
+ of folder goes, and it was not on offer.
+ """
+ out = _run(target_fn, tmp_path, size=20 * GB, mode="ask", picker=True,
+ streamed=True, batched=True)
+ assert out == {"kind": "stream", "name": "s"}
+
+
+def test_a_batched_download_falls_back_to_the_dialog_rather_than_failing(
+ target_fn, tmp_path):
+ """When the worker does not answer, asking is better than refusing: a
+ dialog that has to be answered is still a download, and the alternative
+ here is losing the file. A preference must not cost a capability, and
+ 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"}
+
+
+def test_batching_never_pushes_a_large_file_into_memory(target_fn, tmp_path):
+ """Firefox shape — no picker at all. Nothing about the batch flag may reach
+ the memory floor above the ceiling."""
+ out = _run(target_fn, tmp_path, size=20 * GB, mode="ask", picker=False,
+ streamed=False, batched=True)
+ assert out["kind"] == "refused", out
+
+
# ── The one that outlives today's branches ──────────────────────────────────
def test_no_unguarded_memory_floor(target_fn):