summaryrefslogtreecommitdiffstats
path: root/packages/meshbay-node/tests
diff options
context:
space:
mode:
authorChristophe Besson <cbesson@gmail.com>2026-09-24 16:45:29 +0200
committerChristophe Besson <cbesson@gmail.com>2026-09-24 16:45:29 +0200
commit92ea222b7aca6a3eb5f04330f0af6755e6e434e3 (patch)
treec66b6560377c39f882bf1618b8b85853b1248171 /packages/meshbay-node/tests
parentc30b3233c725fcd60feb4bec79230b434ef0f7f8 (diff)
downloadmeshbay-92ea222b7aca6a3eb5f04330f0af6755e6e434e3.tar.gz
fix(node): a connection that ends leaves its group's peer set
connectionstatechange dropped a closed or failed session from the transport and stopped its tasks, but only close() took it out of the group's peer set, and nothing on that path called it. Every broadcast then went to closed channels, and each reconnect left a dead session held until restart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Diffstat (limited to 'packages/meshbay-node/tests')
-rw-r--r--packages/meshbay-node/tests/test_peer_session_limits.py63
1 files changed, 63 insertions, 0 deletions
diff --git a/packages/meshbay-node/tests/test_peer_session_limits.py b/packages/meshbay-node/tests/test_peer_session_limits.py
index d5c909b..493612a 100644
--- a/packages/meshbay-node/tests/test_peer_session_limits.py
+++ b/packages/meshbay-node/tests/test_peer_session_limits.py
@@ -123,6 +123,69 @@ async def test_handle_offer_arms_the_reaper(monkeypatch):
await tp.close_all()
+class _StatefulPC:
+ """An RTCPeerConnection that keeps the handlers the transport registers, so
+ a test can drive the connection through its states."""
+
+ def __init__(self, *a, **kw):
+ self.localDescription = MagicMock(sdp="v=0\r\n")
+ self.remoteDescription = None
+ self.connectionState = "new"
+ self.handlers = {}
+
+ def on(self, event):
+ def register(fn):
+ self.handlers[event] = fn
+ return fn
+ return register
+
+ async def setRemoteDescription(self, _d): pass
+ async def createAnswer(self): return MagicMock()
+ async def setLocalDescription(self, _d): pass
+ async def close(self): pass
+
+
+@pytest.mark.asyncio
+@pytest.mark.parametrize("state", ["closed", "failed"])
+async def test_a_connection_that_ends_leaves_its_groups_peer_set(monkeypatch, state):
+ """
+ A closed tab, a phone gone to sleep and a dead network all arrive at
+ `connectionstatechange`, which dropped the session from the transport and
+ stopped its tasks but left it in its group's peer set: only `close()` took
+ it out, and nothing on this path called it. Every broadcast to the group
+ was then written to a closed channel ("WebRTC send skipped", thirteen times
+ per settings change on a node up for a day), and every reconnect left one
+ more dead session held until the node restarted.
+ """
+ pcs = []
+
+ def make_pc(*a, **kw):
+ pcs.append(_StatefulPC())
+ return pcs[-1]
+
+ monkeypatch.setattr(ws_mod, "RTCPeerConnection", make_pc)
+ monkeypatch.setattr(ws_mod, "RTCSessionDescription",
+ lambda **kw: MagicMock(**kw))
+
+ tp = _transport()
+ tp._ctx["groups"] = {"g1": {"name": "g1"}}
+ try:
+ await tp.handle_offer("v=0\r\n", "peer-1")
+ session = tp._sessions["peer-1"]
+ session._user_id, session._group_id = "a-member", "g1" # handshake done
+ session._register_peer()
+ peers = tp._ctx["groups"]["g1"]["_peers"]
+ assert list(peers.values()) == [session]
+
+ pcs[0].connectionState = state
+ await pcs[0].handlers["connectionstatechange"]()
+
+ assert "peer-1" not in tp._sessions
+ assert not peers, "the group still broadcasts to a connection that is gone"
+ finally:
+ await tp.close_all()
+
+
@pytest.mark.asyncio
async def test_the_reaper_is_held_and_cancelled_with_the_transport():
"""asyncio keeps only a weak reference to a task, and a reaper collected