"""Tests for RadicleLink (pure logic — no RNS networking required).""" import threading import time from unittest.mock import MagicMock, patch import pytest from radicle_reticulum.link import RadicleLink, LinkState def make_link(state: LinkState = LinkState.ACTIVE) -> tuple[RadicleLink, MagicMock]: """Create a RadicleLink with a mock RNS.Link.""" mock_rns_link = MagicMock() mock_rns_link.rtt = None link = RadicleLink(rns_link=mock_rns_link, state=state) return link, mock_rns_link class TestRadicleLinkState: def test_active_link_is_active(self): link, _ = make_link(LinkState.ACTIVE) assert link.is_active assert link.state == LinkState.ACTIVE def test_pending_link_is_not_active(self): link, _ = make_link(LinkState.PENDING) assert not link.is_active def test_on_established_sets_active(self): link, _ = make_link(LinkState.PENDING) link._on_established(MagicMock()) assert link.state == LinkState.ACTIVE assert link.is_active def test_on_closed_sets_closed(self): link, _ = make_link(LinkState.ACTIVE) link._on_closed(MagicMock()) assert link.state == LinkState.CLOSED assert not link.is_active def test_close_calls_teardown(self): link, mock_rns = make_link(LinkState.ACTIVE) link.close() mock_rns.teardown.assert_called_once() assert link.state == LinkState.CLOSED def test_close_on_inactive_link_is_noop(self): link, mock_rns = make_link(LinkState.CLOSED) link.close() mock_rns.teardown.assert_not_called() def test_repr(self): link, _ = make_link(LinkState.ACTIVE) r = repr(link) assert "active" in r assert "RadicleLink" in r class TestRadicleLinkSend: def test_send_on_active_link_succeeds(self): link, _ = make_link(LinkState.ACTIVE) with patch("radicle_reticulum.link.RNS.Packet") as mock_packet_cls: mock_packet = MagicMock() mock_packet_cls.return_value = mock_packet result = link.send(b"hello world") assert result is True mock_packet.send.assert_called_once() def test_send_on_inactive_link_returns_false(self): link, _ = make_link(LinkState.CLOSED) result = link.send(b"data") assert result is False def test_send_on_pending_link_returns_false(self): link, _ = make_link(LinkState.PENDING) result = link.send(b"data") assert result is False def test_send_exception_returns_false(self): link, _ = make_link(LinkState.ACTIVE) with patch("radicle_reticulum.link.RNS.Packet") as mock_packet_cls: mock_packet_cls.side_effect = RuntimeError("send failed") result = link.send(b"data") assert result is False class TestRadicleLinkRecv: def test_recv_returns_buffered_data(self): link, _ = make_link() link._on_packet(b"buffered", MagicMock()) result = link.recv(timeout=0.1) assert result == b"buffered" def test_recv_returns_in_order(self): link, _ = make_link() for i in range(3): link._on_packet(f"msg{i}".encode(), MagicMock()) assert link.recv(timeout=0.1) == b"msg0" assert link.recv(timeout=0.1) == b"msg1" assert link.recv(timeout=0.1) == b"msg2" def test_recv_timeout_returns_none(self): link, _ = make_link() start = time.time() result = link.recv(timeout=0.05) elapsed = time.time() - start assert result is None assert elapsed >= 0.04 def test_recv_woken_by_data(self): link, _ = make_link() results = [] def delayed_send(): time.sleep(0.05) link._on_packet(b"delayed", MagicMock()) t = threading.Thread(target=delayed_send, daemon=True) t.start() result = link.recv(timeout=1.0) t.join() assert result == b"delayed" def test_recv_returns_none_when_closed(self): link, _ = make_link() link._on_closed(MagicMock()) result = link.recv(timeout=0.1) assert result is None def test_recv_woken_by_close(self): """recv() unblocks when link closes while waiting.""" link, _ = make_link() result_holder = [] def close_after_delay(): time.sleep(0.05) link._on_closed(MagicMock()) t = threading.Thread(target=close_after_delay, daemon=True) t.start() result = link.recv(timeout=2.0) t.join() assert result is None def test_on_packet_calls_on_data_callback(self): link, _ = make_link() received = [] link.on_data = received.append link._on_packet(b"callback data", MagicMock()) assert received == [b"callback data"] def test_on_closed_calls_on_close_callback(self): link, _ = make_link() called = [] link.on_close = lambda: called.append(True) link._on_closed(MagicMock()) assert called == [True] class TestRadicleLinkProperties: def test_rtt_returns_none_when_unavailable(self): link, mock_rns = make_link() mock_rns.rtt = None assert link.rtt is None def test_rtt_returns_value_when_available(self): link, mock_rns = make_link() mock_rns.rtt = 0.15 assert link.rtt == pytest.approx(0.15) def test_remote_identity_delegates_to_rns(self): link, mock_rns = make_link() fake_id = MagicMock() mock_rns.get_remote_identity.return_value = fake_id assert link.remote_identity is fake_id class TestRadicleLinkFactories: def test_from_incoming_is_active(self): mock_rns = MagicMock() link = RadicleLink.from_incoming(mock_rns) assert link.state == LinkState.ACTIVE def test_from_incoming_does_not_set_established_callback(self): mock_rns = MagicMock() RadicleLink.from_incoming(mock_rns) mock_rns.set_link_established_callback.assert_not_called() def test_create_outbound_is_pending(self): mock_dest = MagicMock() with patch("radicle_reticulum.link.RNS.Link") as mock_link_cls: mock_rns = MagicMock() mock_link_cls.return_value = mock_rns link = RadicleLink.create_outbound(mock_dest) assert link.state == LinkState.PENDING