diff options
Diffstat (limited to 'packages/meshbay-hub/tests')
| -rw-r--r-- | packages/meshbay-hub/tests/test_spa_ordering.py | 32 | ||||
| -rw-r--r-- | packages/meshbay-hub/tests/test_transfers.py | 169 |
2 files changed, 201 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_spa_ordering.py b/packages/meshbay-hub/tests/test_spa_ordering.py index 1329b74..04c5159 100644 --- a/packages/meshbay-hub/tests/test_spa_ordering.py +++ b/packages/meshbay-hub/tests/test_spa_ordering.py @@ -167,3 +167,35 @@ def test_no_caller_waits_for_one_chunk_at_a_time(): app = APP.read_text() assert "uploadChunk(" not in app, ( "a per-chunk await is back in the SPA; use transport.uploadFile()") + + +# ── Transfers outlive the page ────────────────────────────────────────────── +# +# Downloads and uploads used to be state inside GroupPage, so leaving a group +# unmounted the component, its cleanup closed the DataChannel, and a half-written +# file was all you had. The store in transfers.js owns them now; these check the +# two ends of that, since neither shows up in any Python test. + +def test_leaving_a_group_hands_the_transport_over_rather_than_closing_it(): + app = APP.read_text() + cleanup = app[app.index(" return () => {\n cancelled = true;"):] + cleanup = cleanup[:cleanup.index("\n }, [groupId")] + assert "releaseWhenIdle" in cleanup, ( + "the group page closes its transport directly again — a running " + "download would die with the page") + assert ".close()" not in cleanup + + +def test_signing_out_stops_them(): + app = APP.read_text() + logout = app[app.index(" logout: () => {"):] + logout = logout[:logout.index("navigate('/login')")] + assert "transfers.reset()" in logout, ( + "logout must cancel transfers: they run on tokens that stop being ours") + + +def test_the_files_panel_no_longer_carries_its_own_progress_bars(): + """They moved next to the bell, where they stay visible across the app.""" + app = APP.read_text() + for gone in ("setUlState", "setDlState", "dl-bar"): + assert gone not in app, f"{gone} survived the move to the transfer widget" diff --git a/packages/meshbay-hub/tests/test_transfers.py b/packages/meshbay-hub/tests/test_transfers.py new file mode 100644 index 0000000..08743f6 --- /dev/null +++ b/packages/meshbay-hub/tests/test_transfers.py @@ -0,0 +1,169 @@ +""" +The transfer store, which is what keeps a download alive after you leave a group. + +Run under Node, because the behaviour worth pinning is timing and lifetime: +that a cancel actually stops the work rather than only greying out a row, that a +stalled transfer reads as slow instead of reporting its historical average, and +that a transport handed over by a departing page is closed by the last transfer +using it — not before, and not never. +""" + +import json +import shutil +import subprocess +from pathlib import Path + +import pytest + +STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" +TRANSFERS = STATIC / "transfers.js" + +pytestmark = pytest.mark.skipif( + shutil.which("node") is None or not TRANSFERS.exists(), + reason="node or the SPA sources are not available") + + +def _run(body, tmp_path): + module = tmp_path / "transfers.mjs" + module.write_text(TRANSFERS.read_text()) + script = tmp_path / "case.mjs" + script.write_text( + f"import {{ TransferStore, formatSpeed }} from '{module.as_posix()}';\n" + "const out = [];\n" + "const say = (...a) => out.push(...a);\n" + f"{body}\n" + "console.log(JSON.stringify(out));\n") + proc = subprocess.run(["node", str(script)], capture_output=True, text=True) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_a_cancel_actually_stops_the_work(tmp_path): + """ + The flag has to be read by the thing doing the work. A store that only + marks a row cancelled gives you a button that lies. + """ + result = _run(""" +const store = new TransferStore(); +let chunksWritten = 0; +const id = store.start({ + kind: 'download', name: 'big.mp4', total: 100, + run: async ({ signal, onProgress }) => { + for (let i = 0; i < 100; i++) { + if (signal.aborted) throw Object.assign(new Error('stop'), { name: 'AbortError' }); + chunksWritten++; + onProgress(i + 1, 100); + await new Promise(r => setTimeout(r, 1)); + } + }, +}); +await new Promise(r => setTimeout(r, 20)); +store.cancel(id); +await new Promise(r => setTimeout(r, 40)); +say(chunksWritten < 100, store.list()[0].status, store.active); +""", tmp_path) + assert result[0] is True, "the work ran to completion despite being cancelled" + assert result[1] == "cancelled" + assert result[2] == 0 + + +def test_speed_is_measured_over_a_window_not_since_the_start(tmp_path): + """A transfer that stalls should read as slow now, not as its own average.""" + result = _run(""" +let clock = 0; +const store = new TransferStore(() => clock); +let report; +store.start({ + kind: 'download', name: 'x', total: 100_000_000, + run: async ({ onProgress }) => { report = onProgress; await new Promise(() => {}); }, +}); +await new Promise(r => setTimeout(r, 1)); // start() defers run() by a tick +// A fast megabyte a second, for six seconds. +for (let i = 1; i <= 6; i++) { clock = i * 1000; report(i * 1_000_000, 100_000_000); } +say(Math.round(store.list()[0].speed)); +// Then it stalls: the clock moves, the bytes do not. +for (let i = 7; i <= 12; i++) { clock = i * 1000; report(6_000_000, 100_000_000); } +say(Math.round(store.list()[0].speed)); +""", tmp_path) + assert 900_000 <= result[0] <= 1_100_000, f"expected ~1 MB/s, got {result[0]}" + assert result[1] == 0, f"a stalled transfer still reports {result[1]} B/s" + + +def test_a_transport_is_closed_by_the_last_transfer_that_needed_it(tmp_path): + """ + Leaving a group page must not kill a running download, and must not leak + the connection either. + """ + result = _run(""" +const store = new TransferStore(); +let closed = 0; +const transport = { close: () => { closed++; }, onIndexSync: () => {} }; +let done1, done2; +store.start({ kind: 'download', name: 'a', transport, + run: () => new Promise(r => { done1 = r; }) }); +store.start({ kind: 'download', name: 'b', transport, + run: () => new Promise(r => { done2 = r; }) }); + +store.releaseWhenIdle(transport); // the page goes away +await new Promise(r => setTimeout(r, 5)); +say(closed); // still working: must stay open + +done1(); await new Promise(r => setTimeout(r, 5)); +say(closed); // one left: still open + +done2(); await new Promise(r => setTimeout(r, 5)); +say(closed); // now it can go +""", tmp_path) + assert result == [0, 0, 1], f"close() calls after each step: {result}" + + +def test_an_idle_transport_is_closed_straight_away(tmp_path): + result = _run(""" +const store = new TransferStore(); +let closed = 0; +store.releaseWhenIdle({ close: () => { closed++; } }); +say(closed); +""", tmp_path) + assert result == [1], "leaving a group with nothing running should close it" + + +def test_signing_out_cancels_everything_and_lets_go(tmp_path): + """ + Navigating is not signing out. The second one has to stop transfers running + on tokens that are about to stop being ours. + """ + result = _run(""" +const store = new TransferStore(); +let closed = 0; +const transport = { close: () => { closed++; } }; +store.start({ kind: 'upload', name: 'a', transport, + run: () => new Promise(() => {}) }); +store.reset(); +await new Promise(r => setTimeout(r, 5)); +say(store.list().length, closed, store.active); +""", tmp_path) + assert result == [0, 1, 0] + + +def test_a_failure_is_kept_and_named(tmp_path): + """A transfer that dies silently is one the user retries at random.""" + result = _run(""" +const store = new TransferStore(); +store.start({ kind: 'download', name: 'gone.mp4', + run: async () => { throw new Error('File not found'); } }); +await new Promise(r => setTimeout(r, 5)); +const row = store.list()[0]; +say(row.status, row.error); +store.clearFinished(); +say(store.list().length); +""", tmp_path) + assert result[0] == "failed" + assert result[1] == "File not found" + assert result[2] == 0 + + +def test_the_rate_is_readable(tmp_path): + result = _run(""" +say(formatSpeed(0), formatSpeed(2048), formatSpeed(5 * 1024 * 1024)); +""", tmp_path) + assert result == ["", "2 KB/s", "5.0 MB/s"] |