diff options
| author | Christophe Besson <cbesson@gmail.com> | 2026-10-17 10:11:07 +0200 |
|---|---|---|
| committer | Christophe Besson <cbesson@gmail.com> | 2026-10-17 10:11:07 +0200 |
| commit | 7e61d16f0ca6aeca6adb1dd909a7e7b0376719f9 (patch) | |
| tree | 08466d6c6879efd1f2c21a118919cb99835fb31f /packages/meshbay-hub/tests/test_network_change.py | |
| parent | 6329beaca34e31749ff0343d02e27db2826157e7 (diff) | |
| download | meshbay-7e61d16f0ca6aeca6adb1dd909a7e7b0376719f9.tar.gz | |
fix(spa): follow the device to another network
Moving between mobile data and the node's Wi-Fi left the connection on
the network it had left until ICE gave up, and a group that had failed
was never tried again: the Android application had to be killed. A
change of network or an ICE disconnection now pings the session and
reconnects one that does not answer, wakes a reconnect backing off, and
retries a group page left on an error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-hub/tests/test_network_change.py')
| -rw-r--r-- | packages/meshbay-hub/tests/test_network_change.py | 90 |
1 files changed, 90 insertions, 0 deletions
diff --git a/packages/meshbay-hub/tests/test_network_change.py b/packages/meshbay-hub/tests/test_network_change.py new file mode 100644 index 00000000..2298e8e6 --- /dev/null +++ b/packages/meshbay-hub/tests/test_network_change.py @@ -0,0 +1,90 @@ +""" +A connection follows the device to another network. + +A phone moving between mobile data and the Wi-Fi the node is on keeps a +connection whose candidate pair runs over the network it left. ICE says so only +once its consent checks lapse, about thirty seconds, and on Android not before +the old network has lingered. Reported on the Android application: the group +stopped answering and the application had to be killed; in Chrome, a group that +had failed on Wi-Fi opened on mobile data only after a pull-to-refresh. + +Now a change of network asks the open connection for a ping, and a connection +that does not answer is reconnected; a reconnect that is backing off tries again +at once; and a group page left on an error tries again (group-page.js). + +These run the shipped transport: harness/network_change_harness.mjs. +""" + +import json +import re +import shutil +import subprocess +from pathlib import Path + +import pytest +from spa_source import transport_argv + +STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" +HARNESS = Path(__file__).parent / "harness" / "network_change_harness.mjs" + +pytestmark = pytest.mark.skipif(shutil.which("node") is None, reason="node is not available") + +# Connected at 5 s, the network changes at 5 s: settled 1.5 s later, a ping +# unanswered for 5 s, the first reconnect attempt 1 s after that. +CHANGED_AT_MS = 5000 + + +def _run(scenario: str) -> dict: + proc = subprocess.run(["node", str(HARNESS), transport_argv(), + json.dumps({"scenario": scenario})], + capture_output=True, text=True, encoding="utf-8", timeout=120) + assert proc.returncode == 0, proc.stderr + return json.loads(proc.stdout) + + +def test_a_connection_left_on_the_old_network_is_reconnected(): + out = _run("change-dead") + assert out["connected"] + assert out["connections"] == 2, out + # Long before ICE's own thirty seconds would have declared it failed. + assert out["reconnectedAt"] - CHANGED_AT_MS < 10000, out + + +def test_a_connection_that_still_answers_is_kept(): + out = _run("change-alive") + assert out["connections"] == 1 and out["reconnectedAt"] is None, out + assert out["pings"] == 1 + + +def test_an_ice_disconnection_is_checked_rather_than_waited_out(): + dead = _run("ice-dead") + assert dead["connections"] == 2 and dead["reconnectedAt"] - CHANGED_AT_MS < 10000, dead + alive = _run("ice-alive") + assert alive["connections"] == 1 and alive["reconnectedAt"] is None, alive + + +def test_a_reconnect_backing_off_tries_again_on_the_new_network(): + """Without the wake-up, the attempt after 30 s of failures waits 16 s more.""" + out = _run("backoff") + assert out["reconnectedAt"] is not None, out + assert out["reconnectedAt"] - out["backAt"] < 3000, out + + +def test_only_a_change_of_network_counts_and_once(): + """A bandwidth estimate is not news; Wi-Fi against mobile data, `online` and + the application's own event are, and a burst of them is one change.""" + out = _run("events") + assert out["calls"] == [0, 1, 2, 3, 4, 4], out + assert out["leftListening"] == 0, out + + +def test_a_closed_transport_stops_listening(): + out = _run("closed") + assert out["connections"] == 1 and out["leftListening"] == 0, out + + +def test_a_group_left_on_an_error_tries_again_when_the_network_changes(): + source = (STATIC / "group-page.js").read_text(encoding="utf-8") + block = re.search(r"onNetworkChange\(.*?\)", source, re.S) + assert block, "the group page does not listen for another network" + assert "setRetryKey" in source[block.start():block.start() + 200] |