""" Joining a group must fail, or succeed — never wait forever. Reported: a first attempt to connect hung with nothing on screen, and the same account connected a few minutes later. That is the shape of a network wait with no deadline, not of a refusal, and there were two of them. **ICE gathering.** Signaling here is non-trickle — the offer carries its candidates, so it is not sent until gathering says it is done. A STUN server that is slow, filtered, or resolved through a DNS that is not answering means `icegatheringstatechange` never reaches `complete`, and `connect()` never returns. Same shape as the `fullscreen` denial: a promise that never settles produces no error to find. **The hub call in the desktop client.** Node's `fetch` has no default timeout, so a host that accepts a connection and then says nothing holds the request for as long as the OS allows. `hub:probe` had a deadline; `hub:fetch`, which carries signaling, did not. """ import re from pathlib import Path import pytest from spa_source import transport_source STATIC = Path(__file__).resolve().parents[1] / "src" / "meshbay_hub" / "static" TRANSPORT = STATIC / "transport.js" MAIN = (Path(__file__).resolve().parents[2] / "meshbay-client" / "src" / "main.js") pytestmark = pytest.mark.skipif(not TRANSPORT.exists(), reason="SPA sources unavailable") def _gathering_block() -> str: """The wait on ICE gathering, and only it.""" source = transport_source() start = source.index("iceGatheringState") return source[max(0, start - 900):start + 700] def test_ice_gathering_has_a_deadline(): block = _gathering_block() assert "setTimeout" in block, ( "the wait on ICE gathering can never end, and connect() with it") assert "ICE_GATHER_TIMEOUT_MS" in block def test_the_deadline_is_long_enough_for_a_stun_round_trip(): """Cutting gathering off too early drops the reflexive candidate and breaks every connection that is not on the same network.""" source = transport_source() match = re.search(r"const ICE_GATHER_TIMEOUT_MS = (\d+);", source) assert match, "the constant is gone or was renamed" assert 2000 <= int(match.group(1)) <= 10000 def test_a_timed_out_gathering_still_sends_the_offer(): """Host candidates are already gathered, which is enough on a LAN. Giving up instead would turn a slow STUN server into a refusal to connect.""" source = transport_source() block = _gathering_block() # The deadline resolves the promise; it does not reject it. assert "reject" not in block.split("setTimeout", 1)[1][:300] # And the offer is still posted afterwards. assert "webrtc/offer" in source[source.index("iceGatheringState"):] @pytest.mark.skipif(not MAIN.exists(), reason="the desktop client is not present") def test_every_hub_call_from_the_client_has_a_deadline(): source = MAIN.read_text(encoding="utf-8") block = source.split("ipcMain.handle('hub:fetch'", 1)[1].split("ipcMain.handle", 1)[0] assert "AbortSignal.timeout" in block, ( "a hub that accepts the connection and says nothing holds this for " "as long as the OS allows") @pytest.mark.skipif(not MAIN.exists(), reason="the desktop client is not present") def test_the_deadline_outlasts_the_hubs_own_longest_call(): """Signaling waits fifteen seconds for a node to answer an offer. A client deadline under that would abort calls that were about to succeed.""" source = MAIN.read_text(encoding="utf-8") match = re.search(r"const HUB_FETCH_TIMEOUT_MS = (\d+);", source) assert match, "the constant is gone or was renamed" assert int(match.group(1)) > 15000 @pytest.mark.skipif(not MAIN.exists(), reason="the desktop client is not present") def test_a_timeout_says_so_rather_than_saying_fetch_failed(): source = MAIN.read_text(encoding="utf-8") block = source.split("ipcMain.handle('hub:fetch'", 1)[1].split("ipcMain.handle", 1)[0] assert "TimeoutError" in block and "did not answer" in block