1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
|
"""
No download above the ceiling is ever collected in the page.
`pipelinedDownload` with no `writable` allocates `new Array(totalChunks)` and
keeps every decrypted chunk, so whatever `_openDownloadTarget` returns `null`
for is a file held whole in RAM. That floor had no upper bound: the
`!window.showSaveFilePicker` branch returned `null` at any size, so on a browser
without the File System Access API a 20 GB film went to memory whenever the
service-worker path did not answer — which happens for ordinary reasons. The
symptom was the tab dying, with nothing in the source to lead back here.
The real `_openDownloadTarget` is lifted out of `file-utils.js` **as text** and
executed against stubbed browsers, on the rule this repo already follows for the
video player: model the environment, never the code under test. A test that
transcribed the decision tree would agree with a broken version of it by
construction.
`test_no_unguarded_memory_floor` is the one that outlives today's branches: it
reads the function and fails if a `return null` appears in it that does not go
through the guard — which is what a fourth fallback added in a hurry would look
like.
"""
import json
import re
import shutil
import subprocess
from pathlib import Path
import pytest
STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static"
FILE_UTILS = STATIC / "file-utils.js"
pytestmark = pytest.mark.skipif(
shutil.which("node") is None or not FILE_UTILS.exists(),
reason="node or the SPA sources are not available")
CEILING = 100 * 1024 * 1024
GB = 1024 * 1024 * 1024
def _lift(name, source):
"""The text of one top-level declaration, from its opening line to the
column-0 brace that closes it. Nothing is re-typed into this test."""
start = source.index(name)
end = source.index("\n}\n", start) + len("\n}\n")
return source[start:end]
@pytest.fixture(scope="module")
def target_fn():
"""The ceiling, its error and the real function — read, never re-typed."""
src = FILE_UTILS.read_text()
ceiling = re.search(r"^const MEMORY_CEILING = .*?;$", src, re.M)
assert ceiling, "MEMORY_CEILING is gone from file-utils.js"
# The test's own CEILING constant must agree with the source's, or every
# boundary case below is asserting against a number nothing uses.
assert str(CEILING) in ceiling.group(0).replace(" ", "") or \
eval(ceiling.group(0).split("=")[1].strip(" ;")) == CEILING
return "\n".join([
ceiling.group(0),
_lift("class TooLargeForMemoryError", src),
_lift("async function _openDownloadTarget", src),
])
def _run(target_fn, tmp_path, *, size, native=False, granted=False,
streamed=False, picker=False, mode="auto"):
"""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.
const formatSize = (n) => `${{n}} B`;
const t = (key, vars) => key + ' ' + JSON.stringify(vars);
const platform = {{
capabilities: {{ nativeSave: {json.dumps(native)} }},
nativeSave: async () => ({{ name: 'n', writable: {{}} }}),
bridgeMessage: (e) => String(e),
}};
const downloads = {{
BLOB_LIMIT: 512 * 1024 * 1024,
// Called by the refusal to name why the streamed path declined -- absent
// from this stub, the error constructor threw TypeError and the test saw the
// wrong failure entirely.
lastStreamFailure: () => 'stubbed: no streamed target in this harness',
getMode: () => {json.dumps(mode)},
openTarget: async () => ({json.dumps(granted)} ? {{ name: 'g', writable: {{}} }} : null),
openStreamedDownload: async () =>
({json.dumps(streamed)} ? {{ name: 's', writable: {{}} }} : null),
}};
globalThis.window = {{}};
if ({json.dumps(picker)}) {{
window.showSaveFilePicker = async () => {{
if ({json.dumps(picker)} === 'no-gesture') {{
const e = new Error("Failed to execute 'showSaveFilePicker' on 'Window': "
+ "Must be handling a user gesture to show a file picker.");
e.name = 'SecurityError';
throw e;
}}
return {{ name: 'p', createWritable: async () => ({{}}) }};
}};
}}
{target_fn}
let outcome;
try {{
const r = await _openDownloadTarget('film.mkv', {size});
outcome = r === null ? {{ kind: 'memory' }}
: r === false ? {{ kind: 'cancelled' }}
: {{ kind: 'stream', name: r.name }};
}} catch (err) {{
outcome = {{ kind: 'refused', name: err.name, message: err.message }};
}}
console.log(JSON.stringify(outcome));
""")
proc = subprocess.run(["node", str(script)], capture_output=True, text=True)
assert proc.returncode == 0, proc.stderr
return json.loads(proc.stdout)
# ── The hole this was written for ───────────────────────────────────────────
def test_a_film_is_refused_rather_than_collected_in_memory(target_fn, tmp_path):
"""Firefox/Safari shape: no picker, no granted folder, the worker did not
answer. This returned null — 20 GB into a tab."""
out = _run(target_fn, tmp_path, size=20 * GB)
assert out["kind"] == "refused", out
assert out["name"] == "TooLargeForMemoryError"
def test_the_refusal_says_how_big_and_what_the_limit_is(target_fn, tmp_path):
out = _run(target_fn, tmp_path, size=20 * GB)
assert "download.too_large_for_memory" in out["message"]
assert str(20 * GB) in out["message"]
assert str(CEILING) in out["message"]
def test_the_same_browser_in_ask_mode_is_refused_too(target_fn, tmp_path):
"""'ask' skips the service-worker block entirely, so it reached the
unguarded branch without even trying to stream."""
out = _run(target_fn, tmp_path, size=20 * GB, mode="ask")
assert out["kind"] == "refused", out
# ── What must keep working ──────────────────────────────────────────────────
def test_something_small_still_uses_the_memory_floor(target_fn, tmp_path):
out = _run(target_fn, tmp_path, size=4 * 1024 * 1024)
assert out["kind"] == "memory", out
def test_the_boundary_is_the_ceiling_itself(target_fn, tmp_path):
assert _run(target_fn, tmp_path, size=CEILING)["kind"] == "memory"
assert _run(target_fn, tmp_path, size=CEILING + 1)["kind"] == "refused"
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"}
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"}
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"}
def test_a_browser_with_a_picker_is_offered_one_instead_of_being_refused(
target_fn, tmp_path):
"""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"}
# ── The one that outlives today's branches ──────────────────────────────────
def test_no_unguarded_memory_floor(target_fn):
"""Every `return null` in the function goes through the guard.
A fourth fallback appended to the chain — which is exactly how the third one
got here — is caught by this even though no case above covers it.
"""
body = target_fn[target_fn.index("async function _openDownloadTarget"):]
lines = body.splitlines()
# The guard's own `return null` is the one legitimate instance, so cut its
# definition out before looking. Comments go too — the branch that used to
# be the bug is now described in one, and a test that reads prose is the
# mistake already recorded in CLAUDE.md for the packaged systemd unit.
start = next(n for n, l in enumerate(lines) if "const _memoryFloor" in l)
end = next(n for n in range(start, len(lines)) if lines[n].strip() == "};")
rest = lines[:start] + lines[end + 1:]
code = [re.sub(r"//.*$", "", l) for l in rest]
bare = [l.strip() for l in code if re.search(r"\breturn null\b", l)]
assert bare == [], (
"an unguarded in-memory fallback was added to _openDownloadTarget; "
"return _memoryFloor() instead: " + "; ".join(bare))
def test_the_guard_is_what_the_preview_uses_too(target_fn):
"""`FilePreview` decrypts a whole entry with no writable at all, so it needs
the same ceiling — and must import it rather than keep a second number."""
files_app = (STATIC / "files-app.js").read_text()
assert "MEMORY_CEILING" in files_app
assert re.search(r"entry\.size\s*>\s*MEMORY_CEILING", files_app), (
"the preview modal must refuse an oversized entry before fetching it")
assert not re.search(r"100\s*\*\s*1024\s*\*\s*1024", files_app), (
"the ceiling is defined once, in file-utils.js")
def test_a_lost_gesture_streams_instead_of_failing(target_fn, tmp_path):
"""
A browser grants one file picker per user gesture, and downloading three
files is one gesture — so the second and third throw "Must be handling a
user gesture". The person sees a failed transfer, with a message from Chrome
about gestures, for having done something entirely reasonable.
The streamed path needs no gesture, so it is the right answer rather than a
consolation: the file lands on disk either way, and the only thing lost is
the choice of folder, which there was no picker to make anyway.
"""
out = _run(target_fn, tmp_path, size=20 * GB,
picker="no-gesture", streamed=True, mode="ask")
assert out == {"kind": "stream", "name": "s"}, out
def test_a_lost_gesture_with_nothing_to_stream_to_still_refuses(target_fn, tmp_path):
"""And the ceiling still holds underneath: no gesture and no stream is not
a reason to put twenty gigabytes in the page."""
out = _run(target_fn, tmp_path, size=20 * GB,
picker="no-gesture", streamed=False, mode="ask")
assert out["kind"] == "refused", out
|