"""Comprehensive unit tests for Signal channel adapter — covering untested functions, edge cases, error handling, and boundary conditions across all modules. """ import asyncio from unittest.mock import AsyncMock, MagicMock, patch import pytest from yuxi.channels.adapters.signal.channel import SignalChannel from yuxi.channels.adapters.signal.client import ( RpcClient, RpcError, RateLimitError, UnauthorizedError, _validate_daemon_url, _detect_rate_limit, ) from yuxi.channels.adapters.signal.send import ( SignalSender, _extract_chunk_styles, _make_message_id, ) from yuxi.channels.adapters.signal.format import ( FormattedText, StyleRange, markdown_to_signal_styles, split_text, clamp_styles_to_length, _convert_tables, _process_links, _url_text_equivalent, _overlaps_any, _table_rows_to_bullets, _convert_headings, _convert_blockquotes, _merge_adjacent_styles, ) from yuxi.channels.adapters.signal.security import ( SignalSecurityPolicy, ) from yuxi.channels.adapters.signal.normalize import ( normalize_target, parse_signal_message, parse_signal_reaction, parse_signal_receipt, is_own_message, build_dedup_key, check_debounce, ) from yuxi.channels.adapters.signal.setup import ( SetupStep, SetupWizard, validate_e164, format_e164, ) from yuxi.channels.adapters.signal.session import resolve_thread from yuxi.channels.adapters.signal.outbound_session import OutboundSession from yuxi.channels.adapters.signal.media_vision import MediaVisionAnalyzer from yuxi.channels.adapters.signal.rpc_context import RpcContext from yuxi.channels.adapters.signal.token_utils import ensure_oauth_prefix, normalize_token from yuxi.channels.adapters.signal.config_ui_hints import SIGNAL_CONFIG_UI_HINTS from yuxi.channels.adapters.signal.reaction_level import ReactionLevelController, ReactionLevel from yuxi.channels.adapters.signal.entity_cache import EntityCache from yuxi.channels.adapters.signal.event_queue import OrderedEventQueue from yuxi.channels.adapters.signal.exec_auth import ExecAuthAdapter, ExecAuthResult from yuxi.channels.models import ( ChannelIdentity, ChannelResponse, ChannelType, ChatType, DeliveryResult, EventType, MessageType, ) # ─── send.py – SignalSender edge cases ─────────────────────────────────────── class TestMakeMessageId: def test_with_timestamp(self): msg_id = _make_message_id(1715000000000) assert msg_id is not None assert msg_id.startswith("1715000000000:") assert len(msg_id.split(":")[1]) == 8 def test_with_string_timestamp(self): msg_id = _make_message_id("1715000000000") assert msg_id is not None assert msg_id.startswith("1715000000000:") def test_with_none_returns_none(self): assert _make_message_id(None) is None def test_with_empty_string(self): assert _make_message_id("") is None def test_with_zero(self): assert _make_message_id(0) is None class TestExtractChunkStyles: def test_full_overlap(self): styles = [StyleRange(start=0, length=10, style="BOLD")] result = _extract_chunk_styles(styles, offset=0, chunk_len=10) assert len(result) == 1 assert result[0].start == 0 assert result[0].length == 10 def test_partial_overlap_start(self): styles = [StyleRange(start=0, length=20, style="BOLD")] result = _extract_chunk_styles(styles, offset=5, chunk_len=10) assert len(result) == 1 assert result[0].start == 0 assert result[0].length == 10 def test_partial_overlap_end(self): styles = [StyleRange(start=8, length=20, style="ITALIC")] result = _extract_chunk_styles(styles, offset=0, chunk_len=10) assert len(result) == 1 assert result[0].start == 8 assert result[0].length == 2 def test_no_overlap_before(self): styles = [StyleRange(start=0, length=3, style="BOLD")] result = _extract_chunk_styles(styles, offset=5, chunk_len=10) assert len(result) == 0 def test_no_overlap_after(self): styles = [StyleRange(start=20, length=5, style="BOLD")] result = _extract_chunk_styles(styles, offset=0, chunk_len=10) assert len(result) == 0 def test_empty_styles(self): result = _extract_chunk_styles([], offset=0, chunk_len=10) assert result == [] def test_multiple_styles(self): styles = [ StyleRange(start=0, length=5, style="BOLD"), StyleRange(start=5, length=5, style="ITALIC"), StyleRange(start=15, length=5, style="MONOSPACE"), ] result = _extract_chunk_styles(styles, offset=0, chunk_len=10) assert len(result) == 2 class TestSignalSenderSendText: def test_plain_mode_with_formatted(self): rpc = AsyncMock() rpc.call = AsyncMock(return_value={"timestamp": 1715000000000}) sender = SignalSender(rpc, "+8612345678901") formatted = FormattedText( body="**bold**", styles=[StyleRange(start=0, length=4, style="BOLD")], ) result = asyncio.run( sender.send_text("+8615678123400", "fallback", formatted_body=formatted, text_mode="plain") ) assert result.success is True def test_send_text_with_reply(self): rpc = AsyncMock() rpc.call = AsyncMock(return_value={"timestamp": 1715000000000}) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.send_text("+8615678123400", "Hello", reply_to_id="1715000000001")) assert result.success is True def test_send_text_with_previews_and_mentions(self): rpc = AsyncMock() rpc.call = AsyncMock(return_value={"timestamp": 1715000000000}) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run( sender.send_text( "+8615678123400", "Hello @user", previews=[{"url": "https://example.com"}], mentions=[{"user_id": "uuid-123", "start": 6, "length": 5}], ) ) assert result.success is True def test_send_text_rpc_error_propagates(self): rpc = AsyncMock() rpc.call = AsyncMock(side_effect=RpcError("RPC failed", code=-32000)) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.send_text("+8615678123400", "Hello")) assert result.success is False assert "RPC failed" in result.error def test_send_text_generic_error_returns_failure(self): rpc = AsyncMock() rpc.call = AsyncMock(side_effect=RuntimeError("boom")) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.send_text("+8615678123400", "Hello")) assert result.success is False assert "boom" in result.error def test_send_text_multiple_chunks(self): rpc = AsyncMock() rpc.call = AsyncMock(return_value={"timestamp": 1715000000000}) sender = SignalSender(rpc, "+8612345678901") long_text = "A" * 5000 result = asyncio.run(sender.send_text("+8615678123400", long_text)) assert result.success is True def test_send_text_chunk_failure_returns_error(self): rpc = AsyncMock() call_count = [0] async def fail_once(*args, **kwargs): call_count[0] += 1 if call_count[0] == 1: return {"timestamp": 1715000000000} raise RuntimeError("chunk failed") rpc.call = fail_once sender = SignalSender(rpc, "+8612345678901") long_text = "A" * 5000 result = asyncio.run(sender.send_text("+8615678123400", long_text)) assert result.success is False class TestSignalSenderSendMedia: def test_send_image(self): rpc = AsyncMock() rpc.call = AsyncMock(return_value={"timestamp": 1715000000000}) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run( sender.send_media("+8615678123400", b"fake_image", "image", filename="photo.jpg", caption="nice") ) assert result.success is True def test_send_media_unknown_type(self): rpc = AsyncMock() rpc.call = AsyncMock(return_value={"timestamp": 1715000000000}) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.send_media("+8615678123400", b"data", "unknown_type")) assert result.success is True def test_send_media_no_filename_or_caption(self): rpc = AsyncMock() rpc.call = AsyncMock(return_value={"timestamp": 1715000000000}) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.send_media("+8615678123400", b"data", "file")) assert result.success is True def test_send_media_failure(self): rpc = AsyncMock() rpc.call = AsyncMock(side_effect=RuntimeError("media fail")) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.send_media("+8615678123400", b"data", "image")) assert result.success is False class TestSignalSenderTypingIndicator: def test_success(self): rpc = AsyncMock() rpc.call = AsyncMock(return_value={}) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.send_typing_indicator("+8615678123400")) assert result.success is True def test_failure(self): rpc = AsyncMock() rpc.call = AsyncMock(side_effect=RuntimeError("fail")) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.send_typing_indicator("+8615678123400")) assert result.success is False class TestSignalSenderEditMessage: def test_success(self): rpc = AsyncMock() rpc.call = AsyncMock(return_value={"timestamp": 1715000000000}) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.edit_message("+8615678123400", "+8612345678901", 1715000000000, "edited")) assert result.success is True def test_failure(self): rpc = AsyncMock() rpc.call = AsyncMock(side_effect=RuntimeError("fail")) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.edit_message("+8615678123400", "+8612345678901", 1715000000000, "edited")) assert result.success is False class TestSignalSenderDeleteMessage: def test_success(self): rpc = AsyncMock() rpc.call = AsyncMock(return_value={}) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.delete_message("+8615678123400", [1715000000000])) assert result.success is True def test_failure(self): rpc = AsyncMock() rpc.call = AsyncMock(side_effect=RuntimeError("fail")) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.delete_message("+8615678123400", [1715000000000])) assert result.success is False class TestSignalSenderSticker: def test_success(self): rpc = AsyncMock() rpc.call = AsyncMock(return_value={"timestamp": 1715000000000}) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.send_sticker("+8615678123400", "pack-1", 42)) assert result.success is True def test_failure(self): rpc = AsyncMock() rpc.call = AsyncMock(side_effect=RuntimeError("fail")) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.send_sticker("+8615678123400", "pack-1", 42)) assert result.success is False class TestSignalSenderSilentMessage: def test_success(self): rpc = AsyncMock() rpc.call = AsyncMock(return_value={"timestamp": 1715000000000}) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.send_silent_message("+8615678123400", "shh")) assert result.success is True def test_with_reply(self): rpc = AsyncMock() rpc.call = AsyncMock(return_value={"timestamp": 1715000000000}) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.send_silent_message("+8615678123400", "shh", reply_to_id="1715000000001")) assert result.success is True class TestSignalSenderPinUnpin: def test_pin_message_success(self): rpc = AsyncMock() rpc.call = AsyncMock(return_value={}) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.pin_message("group:abc", 1715000000000)) assert result.success is True def test_unpin_message_success(self): rpc = AsyncMock() rpc.call = AsyncMock(return_value={}) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.unpin_message("group:abc")) assert result.success is True def test_pin_failure(self): rpc = AsyncMock() rpc.call = AsyncMock(side_effect=RuntimeError("fail")) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.pin_message("group:abc", 1715000000000)) assert result.success is False def test_unpin_failure(self): rpc = AsyncMock() rpc.call = AsyncMock(side_effect=RuntimeError("fail")) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.unpin_message("group:abc")) assert result.success is False class TestSignalSenderVoice: def test_success(self): rpc = AsyncMock() rpc.call = AsyncMock(return_value={"timestamp": 1715000000000}) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.send_voice("+8615678123400", b"audio_data", duration_ms=5000)) assert result.success is True def test_failure(self): rpc = AsyncMock() rpc.call = AsyncMock(side_effect=RuntimeError("fail")) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.send_voice("+8615678123400", b"audio_data")) assert result.success is False class TestSignalSenderGroupOps: def test_create_group_success(self): rpc = AsyncMock() rpc.call = AsyncMock(return_value={"groupId": "xyz"}) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.create_group("Test Group", ["+8615678123400", "+8619999888877"])) assert result.success is True assert result.message_id == "xyz" def test_create_group_with_avatar(self): rpc = AsyncMock() rpc.call = AsyncMock(return_value={"groupId": "xyz"}) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.create_group("Test", ["+8615678123400"], avatar_path="/tmp/av.png")) assert result.success is True def test_create_group_failure(self): rpc = AsyncMock() rpc.call = AsyncMock(side_effect=RuntimeError("fail")) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.create_group("Fail", ["+8615678123400"])) assert result.success is False def test_delete_group_adds_prefix(self): rpc = AsyncMock() rpc.call = AsyncMock(return_value={}) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.delete_group("abc123")) assert result.success is True call_args = rpc.call.call_args[0] assert call_args[1]["groupId"] == "group:abc123" def test_delete_group_already_prefixed(self): rpc = AsyncMock() rpc.call = AsyncMock(return_value={}) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.delete_group("group:abc123")) assert result.success is True def test_delete_group_failure(self): rpc = AsyncMock() rpc.call = AsyncMock(side_effect=RuntimeError("fail")) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.delete_group("abc")) assert result.success is False def test_manage_members_add(self): rpc = AsyncMock() rpc.call = AsyncMock(return_value={}) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.manage_members("group:abc", add=["+8619999888877"])) assert result.success is True def test_manage_members_remove(self): rpc = AsyncMock() rpc.call = AsyncMock(return_value={}) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.manage_members("group:abc", remove=["+8619999888877"])) assert result.success is True def test_manage_members_both(self): rpc = AsyncMock() rpc.call = AsyncMock(return_value={}) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.manage_members("group:abc", add=["+8618888888888"], remove=["+8617777777777"])) assert result.success is True def test_manage_members_adds_prefix(self): rpc = AsyncMock() rpc.call = AsyncMock(return_value={}) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.manage_members("abc123", add=["+8619999888877"])) assert result.success is True call_args = rpc.call.call_args[0] assert call_args[1]["groupId"] == "group:abc123" def test_manage_members_failure(self): rpc = AsyncMock() rpc.call = AsyncMock(side_effect=RuntimeError("fail")) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.manage_members("group:abc", add=["+8619999888877"])) assert result.success is False class TestSignalSenderCache: def test_cache_and_retrieve(self): async def _t(): sender = SignalSender(MagicMock(), "+8612345678901") sender._cache_sent("msg-1", "+8615678123400") entry = sender._get_cached("msg-1") assert entry is not None assert entry["recipient"] == "+8615678123400" asyncio.run(_t()) def test_cache_nonexistent(self): async def _t(): sender = SignalSender(MagicMock(), "+8612345678901") assert sender._get_cached("no-exist") is None asyncio.run(_t()) def test_prune_removes_excess(self): async def _t(): sender = SignalSender(MagicMock(), "+8612345678901") sender.CACHE_MAX_SIZE = 3 for i in range(10): sender._cache_sent(f"msg-{i}", f"+86100000000{i:02d}") assert len(sender._sent_message_cache) <= 3 asyncio.run(_t()) def test_retry_logic_exponential_backoff(self): rpc = AsyncMock() call_count = [0] async def fail_twice(*args, **kwargs): call_count[0] += 1 if call_count[0] < 3: raise ConnectionError("transient") return {"timestamp": 1715000000000} rpc.call = fail_twice sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.send_text("+8615678123400", "Hello")) assert result.success is True assert call_count[0] == 3 def test_retry_exhausted(self): rpc = AsyncMock() rpc.call = AsyncMock(side_effect=ConnectionError("always fails")) sender = SignalSender(rpc, "+8612345678901") result = asyncio.run(sender.send_text("+8615678123400", "Hello")) assert result.success is False assert "always fails" in result.error # ─── client.py – RpcClient edge cases ───────────────────────────────────────── class TestRpcClientRetry: @patch("aiohttp.ClientSession") def test_retry_on_client_error(self, mock_session_cls): mock_session = MagicMock() mock_session_cls.return_value = mock_session mock_resp = AsyncMock() mock_resp.__aenter__ = AsyncMock(return_value=mock_resp) mock_resp.__aexit__ = AsyncMock(return_value=None) mock_resp.status = 200 call_count = [0] async def mock_json(): call_count[0] += 1 if call_count[0] < 3: raise TimeoutError("timeout") return {"result": {"timestamp": 1715000000000}} mock_resp.json = mock_json mock_session.post = MagicMock(return_value=mock_resp) client = RpcClient("http://127.0.0.1:8080", allow_remote_daemon=True) client._session = mock_session client._retry_config = {"attempts": 3, "min_delay_ms": 1, "max_delay_ms": 50, "jitter": 0} result = asyncio.run(client.call("send", {"msg": "hello"})) assert result["timestamp"] == 1715000000000 assert call_count[0] == 3 def test_call_without_session_raises(self): client = RpcClient("http://127.0.0.1:8080", allow_remote_daemon=True) with pytest.raises(RuntimeError, match="not connected"): asyncio.run(client.call("test", {})) def test_rpc_error_propagates(self): client = RpcClient("http://127.0.0.1:8080", allow_remote_daemon=True) mock_session = MagicMock() mock_resp = AsyncMock() mock_resp.__aenter__ = AsyncMock(return_value=mock_resp) mock_resp.__aexit__ = AsyncMock(return_value=None) mock_resp.status = 200 mock_resp.json = AsyncMock(return_value={"error": {"code": -32000, "message": "custom error"}}) mock_session.post = MagicMock(return_value=mock_resp) client._session = mock_session with pytest.raises(RpcError, match="custom error"): asyncio.run(client.call("test", {})) def test_unauthorized_http_401(self): client = RpcClient("http://127.0.0.1:8080", allow_remote_daemon=True) mock_session = MagicMock() mock_resp = AsyncMock() mock_resp.__aenter__ = AsyncMock(return_value=mock_resp) mock_resp.__aexit__ = AsyncMock(return_value=None) mock_resp.status = 401 mock_session.post = MagicMock(return_value=mock_resp) client._session = mock_session with pytest.raises(UnauthorizedError): asyncio.run(client.call("test", {})) def test_disconnect(self): client = RpcClient("http://127.0.0.1:8080", allow_remote_daemon=True) mock_session = AsyncMock() client._session = mock_session asyncio.run(client.disconnect()) mock_session.close.assert_called_once() assert client._session is None def test_disconnect_no_session(self): client = RpcClient("http://127.0.0.1:8080", allow_remote_daemon=True) asyncio.run(client.disconnect()) class TestDetectRateLimit: def test_detected(self): err = _detect_rate_limit("Rate limit exceeded", {"retry_after_seconds": 60}) assert err is not None assert err.retry_after_seconds == 60 assert isinstance(err, RateLimitError) def test_detected_lowercase(self): err = _detect_rate_limit("ratelimitexception: too many", {}) assert err is not None assert isinstance(err, RateLimitError) def test_not_detected(self): assert _detect_rate_limit("unknown error", {}) is None def test_not_detected_empty(self): assert _detect_rate_limit("Something else", {}) is None class TestValidateDaemonUrl: def test_ipv6_loopback(self): _validate_daemon_url("http://[::1]:8080") def test_ipv6_private_rejected(self): with pytest.raises(ValueError, match="SSRF guard"): _validate_daemon_url("http://[2001:db8::1]:8080") def test_empty_hostname_raises(self): with pytest.raises(ValueError, match="cannot determine host"): _validate_daemon_url("http://") def test_non_resolvable_hostname_in_loopback(self): with pytest.raises(ValueError, match="SSRF guard"): _validate_daemon_url("http://example.com:8080") class TestRpcErrorClasses: def test_rpc_error_code(self): err = RpcError("test", code=-32000) assert err.code == -32000 assert str(err) == "test" def test_rate_limit_error(self): err = RateLimitError("rate", retry_after_seconds=30, token="abc") assert err.code == -32603 assert err.retry_after_seconds == 30 assert err.token == "abc" def test_rate_limit_without_optional(self): err = RateLimitError("rate") assert err.retry_after_seconds is None assert err.token is None # ─── setup.py – SetupWizard and E.164 ──────────────────────────────────────── class TestSetupWizard: def test_default_step_is_status(self): wizard = SetupWizard() assert wizard.current_step == "status" def test_advance_steps(self): wizard = SetupWizard() wizard.advance() assert wizard.current_step == "prepare" wizard.advance() assert wizard.current_step == "cli_path" def test_full_advance_loop(self): wizard = SetupWizard() steps = list(SetupStep) for _ in range(len(steps) - 1): wizard.advance() assert wizard.current_step == steps[-1].value def test_get_step_result_status(self): wizard = SetupWizard() result = wizard.get_step_result() assert result["step"] == "status" assert "total_steps" in result def test_get_step_result_completion(self): wizard = SetupWizard() for _ in range(len(SetupStep) - 1): wizard.advance() result = wizard.get_step_result() assert result["step"] == "completion" assert "next_steps" in result def test_prepare_step_with_config(self): wizard = SetupWizard({"cli_path": "signal-cli"}) wizard.advance() result = wizard.get_step_result() assert result["step"] == "prepare" assert "fields" in result def test_signal_number_step(self): wizard = SetupWizard({"signal_number": "+8613800138000"}) wizard.advance() wizard.advance() wizard.advance() result = wizard.get_step_result() assert result["step"] == "signal_number" def test_allow_from_step(self): wizard = SetupWizard({"security": {"allow_from": ["+8619847654321", "+8615678123400"]}}) wizard.advance() wizard.advance() wizard.advance() wizard.advance() result = wizard.get_step_result() assert result["step"] == "allow_from" class TestValidateE164: def test_valid_various(self): assert validate_e164("+1234567890") is True assert validate_e164("+8613800138000") is True assert validate_e164("+15551234567") is True assert validate_e164("+12345") is True def test_invalid_short(self): assert validate_e164("+1234") is False def test_invalid_long(self): assert validate_e164("+1234567890123456") is False def test_invalid_no_plus(self): assert validate_e164("1234567890") is False def test_invalid_letters(self): assert validate_e164("+1234abc") is False def test_invalid_empty(self): assert validate_e164("") is False class TestFormatE164: def test_already_valid(self): assert format_e164("+8613800138000") == "+8613800138000" def test_strips_non_digits(self): assert format_e164("+86 138-0013-8000") == "+8613800138000" def test_adds_plus(self): assert format_e164("8613800138000") == "+8613800138000" def test_invalid_raises(self): with pytest.raises(ValueError, match="Invalid E.164"): format_e164("123") # ─── normalize.py – additional parsing coverage ────────────────────────────── class TestParseSignalReactionDetail: def test_reaction_add(self): raw = { "envelope": {"source": "+1234"}, "reaction": { "targetAuthor": "+5678", "targetSentTimestamp": 1715000000000, "emoji": "❤️", }, } msg = parse_signal_reaction(raw) assert msg is not None assert msg.event_type == EventType.MESSAGE_UPDATED assert "❤️" in msg.content assert msg.metadata["reaction_emoji"] == "❤️" assert msg.metadata["reaction_removed"] is False def test_reaction_remove(self): raw = { "envelope": {"source": "+1234"}, "reaction": { "targetAuthor": "+5678", "targetSentTimestamp": 1715000000000, "emoji": "👍", "remove": True, }, } msg = parse_signal_reaction(raw) assert msg is not None assert "reaction_removed" in msg.content assert msg.metadata["reaction_removed"] is True def test_reaction_in_group(self): raw = { "envelope": {"source": "+1234"}, "reaction": { "targetAuthor": "+5678", "targetSentTimestamp": 1715000000000, "emoji": "🔥", "groupInfo": {"groupId": "group-abc"}, }, } msg = parse_signal_reaction(raw) assert msg is not None assert msg.chat_type == ChatType.GROUP assert msg.identity.channel_chat_id == "group:group-abc" def test_no_reaction_returns_none(self): raw = {"envelope": {"source": "+1234"}} msg = parse_signal_reaction(raw) assert msg is None class TestParseSignalReceiptDetail: def test_read_receipt(self): raw = { "envelope": {"source": "+1234"}, "receiptMessage": { "type": "READ", "timestamps": [1715000000000, 1715000000001], }, } msg = parse_signal_receipt(raw) assert msg is not None assert msg.event_type == EventType.READ_RECEIPT assert len(msg.metadata["receipt_timestamps"]) == 2 def test_delivery_receipt(self): raw = { "envelope": {"source": "+1234"}, "receiptMessage": {"type": "DELIVERED", "timestamps": [1715000000000]}, } msg = parse_signal_receipt(raw) assert msg is not None def test_no_receipt_returns_none(self): raw = {"envelope": {"source": "+1234"}} msg = parse_signal_receipt(raw) assert msg is None class TestCheckDebounce: def test_first_call_no_debounce(self): assert check_debounce("conv-1", interval_ms=1000) is False def test_second_call_within_interval_debounces(self): assert check_debounce("conv-debounce", interval_ms=1000) is False assert check_debounce("conv-debounce", interval_ms=1000) is True def test_different_conversations_independent(self): assert check_debounce("conv-a", interval_ms=1000) is False assert check_debounce("conv-b", interval_ms=1000) is False def test_zero_interval_no_debounce(self): assert check_debounce("conv-c", interval_ms=0) is False assert check_debounce("conv-c", interval_ms=0) is False class TestParseSignalMessageForwardedContactLocation: def test_forwarded_message(self): raw = { "envelope": {"source": "+1234"}, "dataMessage": { "timestamp": 1715000000000, "message": "FWD", "forwarded": True, }, } msg = parse_signal_message(raw) assert msg is not None assert msg.metadata.get("forwarded") is True def test_contact_share(self): raw = { "envelope": {"source": "+1234"}, "dataMessage": { "timestamp": 1715000000000, "message": "", "contacts": [{"name": "Alice", "number": "+99998888777"}], }, } msg = parse_signal_message(raw) assert msg is not None assert "contacts" in msg.metadata def test_location_share(self): raw = { "envelope": {"source": "+1234"}, "dataMessage": { "timestamp": 1715000000000, "message": "", "location": {"latitude": 39.9, "longitude": 116.4, "label": "Beijing"}, }, } msg = parse_signal_message(raw) assert msg is not None assert msg.message_type == MessageType.LOCATION assert msg.metadata["location"]["latitude"] == 39.9 def test_sticker_message_type(self): raw = { "envelope": {"source": "+1234"}, "dataMessage": { "timestamp": 1715000000000, "sticker": {"packId": "pack-1", "stickerId": 42}, }, } msg = parse_signal_message(raw) assert msg is not None assert msg.message_type == MessageType.STICKER def test_message_with_text_and_attachment(self): raw = { "envelope": {"source": "+1234"}, "dataMessage": { "timestamp": 1715000000000, "message": "Check this photo", "attachments": [{"contentType": "image/png", "id": "att-1", "size": 1024}], }, } msg = parse_signal_message(raw) assert msg is not None assert msg.content == "Check this photo" assert msg.message_type == MessageType.IMAGE assert len(msg.attachments) == 1 assert msg.attachments[0].file_id == "att-1" def test_mentions_by_startswith(self): raw = { "envelope": {"source": "+1234"}, "dataMessage": { "timestamp": 1715000000000, "message": "@Alice hello", "bodyRanges": [{"startsWith": True, "start": 0, "length": 6}], }, } msg = parse_signal_message(raw) assert msg is not None assert msg.mentions is not None assert "@Alice" in msg.mentions.mentioned_user_ids # ─── format.py – complete coverage ─────────────────────────────────────────── class TestUrlTextEquivalent: def test_exact_match(self): assert _url_text_equivalent("https://example.com", "https://example.com") is True def test_trailing_slash(self): assert _url_text_equivalent("https://example.com/", "https://example.com") is True def test_label_matches_url_without_scheme(self): assert _url_text_equivalent("example.com", "https://example.com") is True assert _url_text_equivalent("example.com", "http://example.com") is True def test_no_match(self): assert _url_text_equivalent("click here", "https://example.com") is False def test_case_insensitive(self): assert _url_text_equivalent("Example.Com", "https://example.com") is True class TestOverlapsAny: def test_overlaps(self): assert _overlaps_any(5, 10, [(0, 8)]) is True assert _overlaps_any(5, 10, [(8, 15)]) is True assert _overlaps_any(5, 10, [(5, 10)]) is True def test_no_overlap(self): assert _overlaps_any(5, 10, [(0, 4)]) is False assert _overlaps_any(5, 10, [(10, 15)]) is False def test_empty_ranges(self): assert _overlaps_any(5, 10, []) is False class TestTableRowsToBullets: def test_empty_returns_empty(self): assert _table_rows_to_bullets([]) == [] def test_header_plus_rows(self): rows = [["Col1", "Col2"], ["A", "B"]] result = _table_rows_to_bullets(rows) assert len(result) >= 1 def test_single_row(self): rows = [["A", "B"]] result = _table_rows_to_bullets(rows) assert len(result) > 0 class TestConvertTables: def test_passthrough_mode(self): text = "| A | B |\n| 1 | 2 |" result = _convert_tables(text, table_mode="passthrough") assert result == text def test_no_table(self): text = "Plain text" result = _convert_tables(text, table_mode="bullets") assert result == text class TestProcessLinks: def test_simple_link(self): text = "[click](https://example.com)" result, ranges = _process_links(text) assert "click" in result assert "https://example.com" in result def test_auto_link(self): text = "[https://example.com](https://example.com)" result, ranges = _process_links(text) assert result == "https://example.com" class TestConvertHeadings: def test_h1_bold(self): result = _convert_headings("# Title", "bold") assert "Title" in result assert "**" in result def test_plain_style(self): result = _convert_headings("# Title", "plain") assert result == "# Title" def test_h3(self): result = _convert_headings("### Subtitle", "bold") assert "Subtitle" in result def test_no_heading(self): result = _convert_headings("plain text", "bold") assert result == "plain text" class TestConvertBlockquotes: def test_single_blockquote(self): result = _convert_blockquotes("> quoted", "> ") assert result == "> quoted" def test_custom_prefix(self): result = _convert_blockquotes("> text", "| ") assert result == "| text" def test_no_blockquote(self): result = _convert_blockquotes("plain", "> ") assert result == "plain" class TestMergeAdjacentStyles: def test_adjacent_same_style(self): styles = [ StyleRange(start=0, length=5, style="BOLD"), StyleRange(start=5, length=5, style="BOLD"), ] result = _merge_adjacent_styles(styles) assert len(result) == 1 assert result[0].length == 10 def test_different_styles(self): styles = [ StyleRange(start=0, length=5, style="BOLD"), StyleRange(start=5, length=5, style="ITALIC"), ] result = _merge_adjacent_styles(styles) assert len(result) == 2 def test_empty_list(self): assert _merge_adjacent_styles([]) == [] class TestClampStyles: def test_style_within_bounds(self): styles = [StyleRange(start=0, length=5, style="BOLD")] result = clamp_styles_to_length(styles, "Hello") assert len(result) == 1 def test_style_exceeds_body(self): styles = [StyleRange(start=2, length=10, style="BOLD")] result = clamp_styles_to_length(styles, "Hi") assert len(result) == 0 def test_style_starts_past_body(self): styles = [StyleRange(start=10, length=5, style="BOLD")] result = clamp_styles_to_length(styles, "short") assert len(result) == 0 def test_style_at_body_boundary(self): styles = [StyleRange(start=3, length=2, style="BOLD")] result = clamp_styles_to_length(styles, "Hello") assert len(result) == 1 assert result[0].length == 2 class TestFullMarkdownConversion: def test_colon_emoji_preserved(self): result = markdown_to_signal_styles("Hello :smile: world") assert result is not None assert ":smile:" in result.body def test_mixed_formatting_edge(self): result = markdown_to_signal_styles("**a*b**c*") assert result is not None def test_list_items(self): result = markdown_to_signal_styles("- item1\n- item2") assert result is not None assert "item1" in result.body # ─── security.py – additional scenarios ────────────────────────────────────── class TestSecurityExpandAllowlist: def test_strips_signal_prefix(self): result = SignalSecurityPolicy._expand_allowlist(["signal:+1234"]) assert "+1234" in result def test_mixed_entries(self): result = SignalSecurityPolicy._expand_allowlist(["signal:+1234", "uuid:abc", "+5678"]) assert "+1234" in result assert "uuid:abc" in result assert "+5678" in result def test_empty(self): assert SignalSecurityPolicy._expand_allowlist([]) == [] class TestSecurityWildcard: def test_wildcard_match_true(self): policy = SignalSecurityPolicy(allow_from=["*"]) assert policy._is_wildcard_match("+8610847654321") is True def test_wildcard_match_false(self): policy = SignalSecurityPolicy(allow_from=["+8610847654321"]) assert policy._is_wildcard_match("+8615678123400") is False def test_wildcard_dm_always_allowed(self): policy = SignalSecurityPolicy(dm_policy="allowlist", allow_from=["*"]) msg = MagicMock() msg.identity.channel_user_id = "+8619999888877" assert policy.check_dm_permission(msg) is True class TestSecurityPairingChallenge: def test_record_and_check(self): policy = SignalSecurityPolicy() policy.record_pairing_challenge("+8610847654321") assert policy.has_pairing_challenge("+8610847654321") is True assert policy.has_pairing_challenge("+8615678123400") is False def test_reject_removes_pending(self): policy = SignalSecurityPolicy(dm_policy="pairing") msg = MagicMock() msg.identity.channel_user_id = "+8610847654321" policy.check_dm_permission(msg) assert "+8610847654321" in policy.pending_pairings policy.reject_pairing("+8610847654321") assert "+8610847654321" not in policy.pending_pairings class TestSecurityCommandDoubleAuth: def test_wildcard_passes(self): policy = SignalSecurityPolicy(command_double_auth=True, allow_from=["*"]) msg = MagicMock() msg.identity.channel_user_id = "+8619999888877" msg.identity.channel_chat_id = "group:unknown" assert policy.check_command_double_auth(msg) is True def test_not_enabled_passes(self): policy = SignalSecurityPolicy(command_double_auth=False) msg = MagicMock() msg.identity.channel_user_id = "+8619999888877" msg.identity.channel_chat_id = "group:unknown" assert policy.check_command_double_auth(msg) is True def test_neither_allowed_denies(self): policy = SignalSecurityPolicy(command_double_auth=True) msg = MagicMock() msg.identity.channel_user_id = "+8619999888877" msg.identity.channel_chat_id = "group:unknown" assert policy.check_command_double_auth(msg) is False class TestSecuritySetStoreHandlers: async def test_set_and_approve_with_store(self): policy = SignalSecurityPolicy(dm_policy="pairing") write_fn = AsyncMock() read_fn = AsyncMock() policy.set_store_handlers(write_fn, read_fn) await policy.approve_pairing("+8610847654321") write_fn.assert_called_once() async def test_load_pairing_store_without_handler(self): policy = SignalSecurityPolicy() await policy.load_pairing_store() async def test_load_pairing_store_with_data(self): policy = SignalSecurityPolicy() read_fn = AsyncMock( return_value={ "k1": {"user_id": "+8610847654321"}, "k2": {"user_id": "+8615678123400"}, } ) policy.set_store_handlers(AsyncMock(), read_fn) await policy.load_pairing_store() assert "+8610847654321" in policy._dm_allowlist assert "+8615678123400" in policy._dm_allowlist async def test_load_pairing_store_handles_error(self): policy = SignalSecurityPolicy() read_fn = AsyncMock(side_effect=RuntimeError("fail")) policy.set_store_handlers(AsyncMock(), read_fn) await policy.load_pairing_store() # ─── session.py / outbound_session.py / rpc_context.py / token_utils.py ───── class TestResolveThread: def test_direct_message(self): identity = ChannelIdentity( channel_id="signal", channel_type=ChannelType.SIGNAL, channel_user_id="+8613800138000", channel_chat_id="+8613800138000", ) thread = resolve_thread(identity, "main") assert thread.startswith("agent:main:signal:dm:") assert "8613800138000" in thread def test_group_message(self): identity = ChannelIdentity( channel_id="signal", channel_type=ChannelType.SIGNAL, channel_user_id="+8613800138000", channel_chat_id="group:abc123", ) thread = resolve_thread(identity, "main") assert thread == "agent:main:signal:group:abc123" def test_wrong_channel_type_raises(self): identity = ChannelIdentity( channel_id="discord", channel_type=ChannelType.DISCORD, channel_user_id="+8613800138000", channel_chat_id="+8613800138000", ) with pytest.raises(ValueError, match="Unexpected channel type"): resolve_thread(identity) class TestOutboundSession: def test_resolve_group(self): session = OutboundSession.resolve("group:abc123", "+8610847654321") assert session.is_group is True assert session.from_ == "+8610847654321" assert session.to == "group:abc123" def test_resolve_uuid(self): session = OutboundSession.resolve("uuid:abcd-1234", "+8610847654321") assert session.is_group is False assert session.chat_type == ChatType.DIRECT def test_resolve_direct(self): session = OutboundSession.resolve("+8615678123400", "+8610847654321") assert session.is_group is False assert session.chat_type == ChatType.DIRECT assert session.conversation_id == "+8615678123400" class TestRpcContext: @patch("yuxi.channels.adapters.signal.rpc_context.RpcClient") def test_resolve(self, mock_rpc_client_cls): mock_client = AsyncMock() mock_client.connect = AsyncMock() mock_rpc_client_cls.return_value = mock_client async def _test(): ctx = await RpcContext.resolve("http://127.0.0.1:8080", "+8610847654321") assert ctx.account == "+8610847654321" assert ctx.base_url == "http://127.0.0.1:8080" asyncio.run(_test()) @patch("yuxi.channels.adapters.signal.rpc_context.RpcClient") def test_context_manager(self, mock_rpc_client_cls): mock_client = AsyncMock() mock_client.connect = AsyncMock() mock_client.disconnect = AsyncMock() mock_rpc_client_cls.return_value = mock_client async def _test(): async with await RpcContext.resolve("http://127.0.0.1:8080", "+8610847654321") as ctx: assert ctx.account == "+8610847654321" mock_client.disconnect.assert_called_once() asyncio.run(_test()) class TestTokenUtils: def test_already_prefixed(self): assert ensure_oauth_prefix("oauth:token123") == "oauth:token123" def test_not_prefixed(self): assert ensure_oauth_prefix("token123") == "oauth:token123" def test_with_whitespace(self): assert ensure_oauth_prefix(" oauth:token ") == "oauth:token" def test_normalize_token_calls_ensure(self): assert normalize_token("mytoken") == "oauth:mytoken" # ─── config_ui_hints.py ────────────────────────────────────────────────────── class TestConfigUiHints: def test_all_core_keys_exist(self): assert "enabled" in SIGNAL_CONFIG_UI_HINTS assert "signal_number" in SIGNAL_CONFIG_UI_HINTS assert "accounts" in SIGNAL_CONFIG_UI_HINTS assert "cli_path" in SIGNAL_CONFIG_UI_HINTS assert "block_streaming" in SIGNAL_CONFIG_UI_HINTS assert "reaction_level" in SIGNAL_CONFIG_UI_HINTS assert "config_writes" in SIGNAL_CONFIG_UI_HINTS assert "media_max_mb" in SIGNAL_CONFIG_UI_HINTS assert "allow_remote_daemon" in SIGNAL_CONFIG_UI_HINTS assert "ai_vision" in SIGNAL_CONFIG_UI_HINTS assert "ingest" in SIGNAL_CONFIG_UI_HINTS assert "main_dm_owner_pin" in SIGNAL_CONFIG_UI_HINTS def test_all_entries_have_label_and_group(self): for key, entry in SIGNAL_CONFIG_UI_HINTS.items(): assert "label" in entry, f"Missing label in {key}" assert "group" in entry, f"Missing group in {key}" # ─── reaction_level.py ─────────────────────────────────────────────────────── class TestReactionLevelEnum: def test_values(self): assert ReactionLevel.OFF == "off" assert ReactionLevel.ACK == "ack" assert ReactionLevel.MINIMAL == "minimal" assert ReactionLevel.EXTENSIVE == "extensive" class TestReactionLevelControllerExtended: def test_extensive_allows_all(self): ctrl = ReactionLevelController("extensive") assert ctrl.should_send_reaction("c1", "🎉") is True def test_off_denies_all(self): ctrl = ReactionLevelController("off") assert ctrl.should_send_reaction("c1") is False def test_minimal_always_allows(self): ctrl = ReactionLevelController("minimal") assert ctrl.should_send_reaction("c1") is True assert ctrl.should_send_reaction("c1") is True def test_ack_only_for_eyes(self): ctrl = ReactionLevelController("ack") assert ctrl.should_send_reaction("c1") is False assert ctrl.should_send_reaction("c1", "\U0001f440") is True def test_should_send_auto_ack_once(self): ctrl = ReactionLevelController("ack") assert ctrl.should_send_auto_ack("c1") is True assert ctrl.should_send_auto_ack("c1") is False def test_should_send_auto_ack_off_level(self): ctrl = ReactionLevelController("off") assert ctrl.should_send_auto_ack("c1") is False def test_reset_ack(self): ctrl = ReactionLevelController("ack") ctrl.should_send_auto_ack("c1") assert ctrl.should_send_auto_ack("c1") is False ctrl.reset_ack("c1") assert ctrl.should_send_auto_ack("c1") is True # ─── channel.py – SignalChannel additional coverage ───────────────────────── class TestSignalChannelWarnOnce: def test_first_call_logs(self): with patch("logging.Logger.warning") as mock_warn: channel = SignalChannel() channel._warn_once("key1", "test message") mock_warn.assert_called_once() def test_second_call_suppressed(self): with patch("logging.Logger.warning") as mock_warn: channel = SignalChannel() channel._warn_once("key1", "msg1") channel._warn_once("key1", "msg1") assert mock_warn.call_count == 1 def test_different_keys_both_logged(self): with patch("logging.Logger.warning") as mock_warn: channel = SignalChannel() channel._warn_once("k1", "m1") channel._warn_once("k2", "m2") assert mock_warn.call_count == 2 class TestSignalChannelValidateConfig: def test_empty_config_passes(self): channel = SignalChannel({"enabled": False}) channel._validate_config() def test_missing_signal_number_in_account(self): channel = SignalChannel( { "accounts": {"acc1": {"enabled": True}}, "enabled": False, } ) with pytest.raises(ValueError, match="signal_number is required"): channel._validate_config() class TestSignalChannelSendTyping: async def test_success(self): channel = SignalChannel() channel._sender = AsyncMock() channel._sender.send_typing_indicator = AsyncMock(return_value=DeliveryResult(success=True)) result = await channel.send_chat_action("+8610847654321") assert result.success is True async def test_no_sender(self): channel = SignalChannel() result = await channel.send_chat_action("+8610847654321") assert result.success is False class TestSignalChannelDeleteMessage: async def test_success(self): channel = SignalChannel() channel._sender = AsyncMock() channel._sender.delete_message = AsyncMock(return_value=DeliveryResult(success=True)) result = await channel.delete_message("+8610847654321", "123") assert result.success is True async def test_no_sender(self): channel = SignalChannel() result = await channel.delete_message("+8610847654321", "123") assert result.success is False class TestSignalChannelSend: async def test_send_with_rpc_error_handling(self): channel = SignalChannel() channel._sender = MagicMock() async def send_error(*args, **kwargs): raise RpcError("RPC crash", code=-32000) channel._sender.send_text = send_error channel._circuit_breaker.state = "closed" response = ChannelResponse( identity=ChannelIdentity( channel_id="signal", channel_type=ChannelType.SIGNAL, channel_user_id="+8610847654321", channel_chat_id="+8610847654321", ), content="hello", ) result = await channel.send(response) assert result.success is False async def test_send_disabled_channel(self): channel = SignalChannel({"enabled": False}) response = ChannelResponse( identity=ChannelIdentity( channel_id="signal", channel_type=ChannelType.SIGNAL, channel_user_id="+8610847654321", channel_chat_id="+8610847654321", ), content="hello", ) result = await channel.send(response) assert result.success is False class TestSignalChannelHealthCheck: async def test_on_connected_channel(self): channel = SignalChannel() channel._rpc_client = AsyncMock() channel._rpc_client.call = AsyncMock(return_value={"version": "1.0"}) channel._sender = MagicMock() channel._account_number = "+8610847654321" from yuxi.channels.models import ChannelStatus with patch.object(channel, "_status", ChannelStatus.CONNECTED): result = await channel.health_check() assert result.status == "healthy" class TestSignalChannelCapabilities: def test_capabilities_structure(self): caps = SignalChannel.capabilities assert caps.reactions is True assert caps.edit is True assert caps.unsend is True assert caps.media is True assert caps.pin is True class TestSignalChannelMeta: def test_meta_structure(self): assert SignalChannel.meta.id == "signal" assert SignalChannel.meta.label == "Signal" class TestSignalChannelStreamingModes: def test_streaming_modes_list(self): assert "off" in SignalChannel.streaming_modes assert "block" in SignalChannel.streaming_modes assert "progress" in SignalChannel.streaming_modes # ─── entity_cache.py – additional edge cases ───────────────────────────────── class TestEntityCacheEdge: def test_set_none_value(self): cache = EntityCache() async def _t(): await cache.set("k", None) assert await cache.get("k") is None asyncio.run(_t()) def test_get_miss_increments_counter(self): cache = EntityCache() async def _t(): _ = await cache.get("no-exist") s = await cache.stats() assert "size" in s asyncio.run(_t()) def test_set_updates_hit(self): cache = EntityCache() async def _t(): await cache.set("k", "v1") await cache.get("k") s = await cache.stats() assert s["size"] >= 1 asyncio.run(_t()) def test_max_size_boundary(self): cache = EntityCache(max_size=1) async def _t(): await cache.set("k1", "v1") await cache.set("k2", "v2") assert await cache.get("k1") is None assert await cache.get("k2") == "v2" asyncio.run(_t()) # ─── event_queue.py – additional edge cases ────────────────────────────────── class TestEventQueueEdge: def test_push_overflow_drops_oldest(self): queue = OrderedEventQueue(max_size=2) async def _t(): await queue.push({"m": "a"}, timestamp=1) await queue.push({"m": "b"}, timestamp=2) await queue.push({"m": "c"}, timestamp=3) assert queue.size <= 2 asyncio.run(_t()) def test_start_twice_noop(self): queue = OrderedEventQueue(max_size=10) async def _t(): await queue.start() await queue.start() await queue.stop() asyncio.run(_t()) def test_stop_not_started_noop(self): queue = OrderedEventQueue(max_size=10) async def _t(): await queue.stop() asyncio.run(_t()) def test_same_timestamp_fifo(self): events = [] async def handler(event): events.append(event) queue = OrderedEventQueue(max_size=10) async def _t(): queue.set_handler(handler) await queue.push({"m": "x"}, timestamp=100) await queue.push({"m": "y"}, timestamp=100) await queue.start() await asyncio.sleep(0.1) await queue.stop() msgs = [e["m"] for e in events] assert msgs == ["x", "y"] asyncio.run(_t()) # ─── exec_auth.py – additional edge cases ──────────────────────────────────── class TestExecAuthEdge: def test_auto_approve_returns_approved(self): adapter = ExecAuthAdapter(auto_approve=True) async def _t(): r1 = await adapter.request_approval("a1", {}, "+1") r2 = await adapter.request_approval("a2", {}, "+2") assert r1 == ExecAuthResult.APPROVED assert r2 == ExecAuthResult.APPROVED asyncio.run(_t()) def test_approve_nonexistent_returns_denied(self): adapter = ExecAuthAdapter() result = adapter.approve("nonexistent") assert result == ExecAuthResult.DENIED def test_pending_requests_appear_in_list(self): adapter = ExecAuthAdapter() async def _t(): task = asyncio.create_task(adapter.request_approval("a1", {}, "+1", timeout=0.1)) await asyncio.sleep(0.05) pending = adapter.get_pending() assert len(pending) == 1 adapter.approve(pending[0]["id"]) result = await task assert result == ExecAuthResult.APPROVED asyncio.run(_t()) # ─── media_vision.py – full coverage ───────────────────────────────────────── class TestMediaVisionAnalyzer: def test_disabled_returns_none(self): analyzer = MediaVisionAnalyzer(None, enabled=False) result = asyncio.run(analyzer.analyze_image(b"data")) assert result is None def test_no_llm_call_returns_none(self): analyzer = MediaVisionAnalyzer(None, enabled=True) result = asyncio.run(analyzer.analyze_image(b"data")) assert result is None async def test_analyze_image_success(self): llm = AsyncMock(return_value={"content": "A cat on a sofa"}) analyzer = MediaVisionAnalyzer(llm, enabled=True) result = await analyzer.analyze_image(b"fake_image_data") assert result == "A cat on a sofa" async def test_analyze_image_str_return(self): llm = AsyncMock(return_value="simple string result") analyzer = MediaVisionAnalyzer(llm, enabled=True) result = await analyzer.analyze_image(b"fake_data") assert result == "simple string result" async def test_analyze_image_exception(self): llm = AsyncMock(side_effect=RuntimeError("vision failed")) analyzer = MediaVisionAnalyzer(llm, enabled=True) result = await analyzer.analyze_image(b"data") assert result is None def test_analyze_attachment_video(self): analyzer = MediaVisionAnalyzer(None, enabled=True) result = asyncio.run(analyzer.analyze_attachment(b"video_data", filename="clip.mp4", mime_type="video/mp4")) assert "video" in result assert "clip.mp4" in result def test_analyze_attachment_audio(self): analyzer = MediaVisionAnalyzer(None, enabled=True) result = asyncio.run(analyzer.analyze_attachment(b"audio", filename="song.ogg", mime_type="audio/ogg")) assert "audio" in result def test_analyze_attachment_file(self): analyzer = MediaVisionAnalyzer(None, enabled=True) result = asyncio.run(analyzer.analyze_attachment(b"data", filename="doc.pdf", mime_type="application/pdf")) assert "file" in result or "doc.pdf" in result def test_analyze_attachment_image_delegates(self): llm = AsyncMock(return_value={"content": "photo desc"}) analyzer = MediaVisionAnalyzer(llm, enabled=True) result = asyncio.run(analyzer.analyze_attachment(b"img", mime_type="image/png")) assert result == "photo desc" # ─── split_text – additional modes ─────────────────────────────────────────── class TestSplitTextLengthMode: def test_exact_multiple(self): chunks = split_text("A" * 8000, 4000, "length") assert len(chunks) == 2 assert all(len(c) == 4000 for c in chunks) def test_uneven_split(self): chunks = split_text("B" * 5000, 4000, "length") assert len(chunks) == 2 assert len(chunks[0]) == 4000 assert len(chunks[1]) == 1000 # ─── parse_signal_message – full metadata paths ────────────────────────────── class TestParseSignalMessageFullMetadata: def test_all_metadata_paths(self): raw = { "envelope": {"source": "+1234", "sourceName": "Alice"}, "dataMessage": { "timestamp": 1715000000000, "message": "full test", "expiresInSeconds": 3600, "viewOnce": True, "sticker": {"packId": "pack-1", "stickerId": 42}, "forwarded": True, "contacts": [{"name": "Bob", "number": "+9999"}], "location": {"latitude": 40.0, "longitude": -74.0, "label": "NYC"}, "quote": {"id": 1715000000001}, }, } msg = parse_signal_message(raw) assert msg is not None assert msg.metadata["expires_in_seconds"] == 3600 assert msg.metadata["view_once"] is True assert msg.metadata["sticker_pack_id"] == "pack-1" assert msg.metadata["forwarded"] is True assert len(msg.metadata["contacts"]) == 1 assert msg.metadata["location"]["latitude"] == 40.0 assert msg.metadata["sender_display_name"] == "Alice" assert msg.reply_to_message_id == "1715000000001" # ─── dedup – additional key variants ───────────────────────────────────────── class TestDedupKeyVariants: def test_reaction_key(self): data = { "envelope": {"source": "+1234"}, "reaction": {"targetSentTimestamp": 1715000000000}, } key = build_dedup_key(data, "acct1") assert key is not None assert "1715000000000" in key def test_delete_key(self): data = { "envelope": {"source": "+1234"}, "deleteMessage": {"targetSentTimestamp": 1715000000000}, } key = build_dedup_key(data, "acct1") assert key is not None def test_receipt_key(self): data = { "envelope": {"source": "+1234"}, "receiptMessage": {"timestamps": [1715000000000]}, } key = build_dedup_key(data, "acct1") assert key is not None def test_no_source_returns_none(self): data = { "dataMessage": {"timestamp": 1715000000000}, } key = build_dedup_key(data, "acct1") assert key is None def test_no_timestamp_returns_none(self): data = { "envelope": {"source": "+1234"}, } key = build_dedup_key(data, "acct1") assert key is None # ─── normalize_target edge cases ───────────────────────────────────────────── class TestNormalizeTargetEdge: def test_e164_with_country_code(self): assert normalize_target("+442071838750") == "+442071838750" def test_already_normalized_uuid(self): assert normalize_target("uuid:abcd-1234-efgh") == "uuid:abcd-1234-efgh" def test_already_normalized_group(self): assert normalize_target("group:base64encoded") == "group:base64encoded" def test_digits_only_with_spaces(self): assert normalize_target(" 86 138 0013 8000 ") == "+8613800138000" def test_invalid_raises_value_error(self): with pytest.raises(ValueError, match="Unable to normalize"): normalize_target("abc") class TestIsOwnMessageUuidOnly: def test_match_by_uuid_alone(self): data = { "envelope": {"source": "+8611111111111", "sourceUuid": "uuid-me"}, } assert is_own_message(data, "+8612222222222", "uuid-me") is True def test_no_match(self): data = { "envelope": {"source": "+8611111111111", "sourceUuid": "uuid-other"}, } assert is_own_message(data, "+8612222222222", "uuid-me") is False