ForcePilot/backend/test/unit/channels/test_channels_signal.py
Kris 3264900bc9 test: 新增多渠道单元测试用例并配置测试环境变量
新增了Twitch、Telegram、Discord、Slack、Mattermost、WeChat、Zalo等多渠道的单元测试用例,覆盖了令牌处理、速率限制、消息去重、会话解析、格式转换、安全策略等模块
同时在测试配置中添加了测试用的OpenAI API密钥环境变量
2026-05-12 00:56:47 +08:00

1690 lines
58 KiB
Python

"""Unit tests for Signal channel adapter.
Tests the normalize, security, send, and message parsing modules.
"""
from unittest.mock import AsyncMock, MagicMock
import pytest
from yuxi.channels.adapters.signal.normalize import normalize_target, normalize_e164, parse_signal_message
from yuxi.channels.adapters.signal.security import SignalSecurityPolicy
from yuxi.channels.adapters.signal.send import SignalSender
from yuxi.channels.models import (
Attachment,
ChannelIdentity,
ChannelMessage,
ChannelResponse,
ChatType,
EventType,
MessageType,
)
class TestNormalizeTarget:
def test_e164_number(self):
assert normalize_target("+8613800138000") == "+8613800138000"
def test_number_without_plus(self):
assert normalize_target("8613800138000") == "+8613800138000"
def test_number_with_spaces_and_dash(self):
assert normalize_target("+1 (555) 123-4567") == "+15551234567"
def test_group_prefix_passthrough(self):
assert normalize_target("group:abc123def456") == "group:abc123def456"
def test_uuid_prefix_passthrough(self):
assert normalize_target("uuid:abcd-1234-efgh") == "uuid:abcd-1234-efgh"
def test_stripped_whitespace(self):
assert normalize_target(" +8613800138000 ") == "+8613800138000"
def test_invalid_number_raises(self):
with pytest.raises(ValueError, match="Unable to normalize"):
normalize_target("123")
def test_short_digit_only_raises(self):
with pytest.raises(ValueError, match="Unable to normalize"):
normalize_target("123456")
class TestNormalizeE164:
def test_strips_plus_and_non_digits(self):
assert normalize_e164("+86 138-0013-8000") == "8613800138000"
def test_already_clean(self):
assert normalize_e164("8613800138000") == "8613800138000"
class TestParseSignalMessage:
def test_direct_message(self):
raw = {
"envelope": {"source": "+1234567890"},
"dataMessage": {
"timestamp": 1715000000000,
"message": "Hello world",
},
}
msg = parse_signal_message(raw)
assert msg is not None
assert msg.chat_type == ChatType.DIRECT
assert msg.content == "Hello world"
assert msg.identity.channel_user_id == "+1234567890"
assert msg.identity.channel_chat_id == "+1234567890"
assert msg.identity.channel_message_id == "1715000000000"
assert msg.message_type == MessageType.TEXT
def test_group_message(self):
raw = {
"envelope": {"source": "+1234567890"},
"dataMessage": {
"timestamp": 1715000000001,
"message": "Group hello",
"groupInfo": {"groupId": "Z3JvdXAtaWQ="},
},
}
msg = parse_signal_message(raw)
assert msg is not None
assert msg.chat_type == ChatType.GROUP
assert msg.identity.channel_chat_id == "group:Z3JvdXAtaWQ="
def test_message_with_body_fallback(self):
raw = {
"envelope": {"source": "+1234567890"},
"dataMessage": {
"timestamp": 1715000000002,
"body": "Fallback body",
},
}
msg = parse_signal_message(raw)
assert msg is not None
assert msg.content == "Fallback body"
def test_message_with_quote(self):
raw = {
"envelope": {"source": "+1234567890"},
"dataMessage": {
"timestamp": 1715000000003,
"message": "Reply message",
"quote": {"id": 1715000000001},
},
}
msg = parse_signal_message(raw)
assert msg is not None
assert msg.reply_to_message_id == "1715000000001"
def test_message_with_image_attachment(self):
raw = {
"envelope": {"source": "+1234567890"},
"dataMessage": {
"timestamp": 1715000000004,
"message": "",
"attachments": [{"contentType": "image/png"}],
},
}
msg = parse_signal_message(raw)
assert msg is not None
assert msg.message_type == MessageType.IMAGE
def test_message_with_video_attachment(self):
raw = {
"envelope": {"source": "+1234567890"},
"dataMessage": {
"timestamp": 1715000000005,
"attachments": [{"contentType": "video/mp4"}],
},
}
msg = parse_signal_message(raw)
assert msg is not None
assert msg.message_type == MessageType.VIDEO
def test_message_with_audio_attachment(self):
raw = {
"envelope": {"source": "+1234567890"},
"dataMessage": {
"timestamp": 1715000000006,
"attachments": [{"contentType": "audio/ogg"}],
},
}
msg = parse_signal_message(raw)
assert msg is not None
assert msg.message_type == MessageType.AUDIO
def test_message_with_file_attachment(self):
raw = {
"envelope": {"source": "+1234567890"},
"dataMessage": {
"timestamp": 1715000000007,
"attachments": [{"contentType": "application/pdf"}],
},
}
msg = parse_signal_message(raw)
assert msg is not None
assert msg.message_type == MessageType.FILE
def test_no_data_message_returns_none(self):
raw = {"envelope": {"source": "+1234567890"}}
msg = parse_signal_message(raw)
assert msg is None
def test_empty_data_message_returns_none(self):
raw = {
"envelope": {"source": "+1234567890"},
"dataMessage": {},
}
msg = parse_signal_message(raw)
assert msg is None
def test_unknown_source(self):
raw = {
"envelope": {},
"dataMessage": {
"timestamp": 1715000000008,
"message": "test",
},
}
msg = parse_signal_message(raw)
assert msg is not None
assert msg.identity.channel_user_id == "unknown"
class TestSignalSecurityPolicy:
@staticmethod
def _make_msg(user_id: str, chat_id: str | None = None):
from yuxi.channels.models import ChannelIdentity
return ChannelMessage(
identity=ChannelIdentity(
channel_id="signal",
channel_type="signal",
channel_user_id=user_id,
channel_chat_id=chat_id or user_id,
),
content="hello",
)
def test_dm_open(self):
policy = SignalSecurityPolicy(dm_policy="open")
assert policy.check_dm_permission(self._make_msg("+1234")) is True
def test_dm_disabled(self):
policy = SignalSecurityPolicy(dm_policy="disabled")
assert policy.check_dm_permission(self._make_msg("+1234")) is False
def test_dm_allowlist_allowed(self):
policy = SignalSecurityPolicy(dm_policy="allowlist", allow_from=["+1234"])
assert policy.check_dm_permission(self._make_msg("+1234")) is True
def test_dm_allowlist_denied(self):
policy = SignalSecurityPolicy(dm_policy="allowlist", allow_from=["+1234"])
assert policy.check_dm_permission(self._make_msg("+9999")) is False
def test_dm_pairing_already_approved(self):
policy = SignalSecurityPolicy(dm_policy="pairing", allow_from=["+1234"])
assert policy.check_dm_permission(self._make_msg("+1234")) is True
def test_dm_pairing_new_user_pending(self):
policy = SignalSecurityPolicy(dm_policy="pairing")
msg = self._make_msg("+new_user")
assert policy.check_dm_permission(msg) is False
assert "+new_user" in policy.pending_pairings
async def test_dm_pairing_approve(self):
policy = SignalSecurityPolicy(dm_policy="pairing")
msg = self._make_msg("+new_user")
assert policy.check_dm_permission(msg) is False
await policy.approve_pairing("+new_user")
assert policy.check_dm_permission(msg) is True
assert "+new_user" not in policy.pending_pairings
def test_dm_pairing_reject(self):
policy = SignalSecurityPolicy(dm_policy="pairing")
msg = self._make_msg("+new_user")
policy.check_dm_permission(msg)
assert "+new_user" in policy.pending_pairings
policy.reject_pairing("+new_user")
assert "+new_user" not in policy.pending_pairings
assert policy.check_dm_permission(msg) is False
def test_group_open(self):
policy = SignalSecurityPolicy(group_policy="open")
assert policy.check_group_permission(self._make_msg("+1234", "group:abc")) is True
def test_group_disabled(self):
policy = SignalSecurityPolicy(group_policy="disabled")
assert policy.check_group_permission(self._make_msg("+1234", "group:abc")) is False
def test_group_allowlist_allowed(self):
policy = SignalSecurityPolicy(group_policy="allowlist", group_allow_from=["group:abc"])
assert policy.check_group_permission(self._make_msg("+1234", "group:abc")) is True
def test_group_allowlist_denied(self):
policy = SignalSecurityPolicy(group_policy="allowlist", group_allow_from=["group:abc"])
assert policy.check_group_permission(self._make_msg("+1234", "group:xyz")) is False
def test_add_remove_allowlist(self):
policy = SignalSecurityPolicy()
policy.add_to_allowlist("+1234", "dm")
assert "+1234" in policy._dm_allowlist
policy.remove_from_allowlist("+1234", "dm")
assert "+1234" not in policy._dm_allowlist
policy.add_to_allowlist("group:abc", "group")
assert "group:abc" in policy._group_allowlist
policy.remove_from_allowlist("group:abc", "group")
assert "group:abc" not in policy._group_allowlist
class TestSignalSenderRecipientParam:
def test_direct_message_uses_recipient(self):
sender = SignalSender(MagicMock(), "+1234567890")
result = sender._recipient_param("+8613800138000")
assert "recipient" in result
assert result["recipient"] == "+8613800138000"
assert "groupId" not in result
def test_group_message_uses_group_id(self):
sender = SignalSender(MagicMock(), "+1234567890")
result = sender._recipient_param("group:abc123")
assert "groupId" in result
assert result["groupId"] == "group:abc123"
assert "recipient" not in result
class TestSignalSenderSplitText:
def test_short_text_no_split(self):
from yuxi.channels.adapters.signal.format import split_text
chunks = split_text("Hello world", 4000)
assert chunks == ["Hello world"]
def test_long_text_splits_on_paragraphs(self):
from yuxi.channels.adapters.signal.format import split_text
para = "A" * 3000
text = f"{para}\n\n{para}"
chunks = split_text(text, 4000)
assert len(chunks) == 2
def test_very_long_paragraph_splits_on_length(self):
from yuxi.channels.adapters.signal.format import split_text
text = "B" * 5000
chunks = split_text(text, 4000)
assert len(chunks) == 2
def test_text_exactly_at_limit(self):
from yuxi.channels.adapters.signal.format import split_text
text = "C" * 4000
chunks = split_text(text, 4000)
assert len(chunks) == 1
def test_empty_text(self):
from yuxi.channels.adapters.signal.format import split_text
chunks = split_text("", 4000)
assert chunks == [""]
class TestParseSignalMessageMentions:
def test_mentions_with_uuid(self):
raw = {
"envelope": {"source": "+1234567890"},
"dataMessage": {
"timestamp": 1715000000000,
"message": "Hello @user",
"bodyRanges": [{"mentionUuid": "uuid-1234", "start": 6, "length": 5}],
},
}
msg = parse_signal_message(raw)
assert msg is not None
assert msg.mentions is not None
assert "uuid-1234" in msg.mentions.mentioned_user_ids
def test_mentions_with_no_body_ranges(self):
raw = {
"envelope": {"source": "+1234567890"},
"dataMessage": {
"timestamp": 1715000000000,
"message": "Plain message",
},
}
msg = parse_signal_message(raw)
assert msg is not None
assert msg.mentions is None
def test_mentions_with_empty_body_ranges(self):
raw = {
"envelope": {"source": "+1234567890"},
"dataMessage": {
"timestamp": 1715000000001,
"message": "No mentions",
"bodyRanges": [],
},
}
msg = parse_signal_message(raw)
assert msg is not None
assert msg.mentions is None
class TestParseSignalMessageUrls:
def test_urls_from_previews(self):
raw = {
"envelope": {"source": "+1234567890"},
"dataMessage": {
"timestamp": 1715000000000,
"message": "Check this out",
"previews": [{"url": "https://example.com"}],
},
}
msg = parse_signal_message(raw)
assert msg is not None
assert "https://example.com" in msg.extracted_urls
def test_urls_from_content_regex(self):
raw = {
"envelope": {"source": "+1234567890"},
"dataMessage": {
"timestamp": 1715000000001,
"message": "Visit https://signal.org for more",
},
}
msg = parse_signal_message(raw)
assert msg is not None
assert "https://signal.org" in msg.extracted_urls
def test_no_urls(self):
raw = {
"envelope": {"source": "+1234567890"},
"dataMessage": {
"timestamp": 1715000000002,
"message": "No links here",
},
}
msg = parse_signal_message(raw)
assert msg is not None
assert msg.extracted_urls == []
class TestParseSignalMessageMetadata:
def test_disappearing_message(self):
raw = {
"envelope": {"source": "+1234567890"},
"dataMessage": {
"timestamp": 1715000000000,
"message": "This will disappear",
"expiresInSeconds": 3600,
},
}
msg = parse_signal_message(raw)
assert msg is not None
assert msg.metadata.get("expires_in_seconds") == 3600
def test_view_once_message(self):
raw = {
"envelope": {"source": "+1234567890"},
"dataMessage": {
"timestamp": 1715000000001,
"message": "One time view",
"viewOnce": True,
},
}
msg = parse_signal_message(raw)
assert msg is not None
assert msg.metadata.get("view_once") is True
def test_sticker_message(self):
raw = {
"envelope": {"source": "+1234567890"},
"dataMessage": {
"timestamp": 1715000000002,
"message": "",
"sticker": {"packId": "sticker-pack-001"},
},
}
msg = parse_signal_message(raw)
assert msg is not None
assert msg.metadata.get("sticker_pack_id") == "sticker-pack-001"
def test_timestamp_parsing(self):
raw = {
"envelope": {"source": "+1234567890"},
"dataMessage": {
"timestamp": 1715000000000,
"message": "Hello",
},
}
msg = parse_signal_message(raw)
assert msg is not None
assert msg.timestamp is not None
assert msg.timestamp.year == 2024
assert msg.timestamp.month == 5
assert msg.timestamp.day == 6
class TestParseSignalDelete:
def test_delete_message(self):
raw = {
"envelope": {"source": "+1234567890"},
"deleteMessage": {"targetSentTimestamp": 1715000000000},
}
from yuxi.channels.adapters.signal.normalize import parse_signal_delete
msg = parse_signal_delete(raw)
assert msg is not None
assert msg.event_type.value == "message.deleted"
assert msg.content == "(message deleted)"
assert msg.identity.channel_user_id == "+1234567890"
def test_delete_message_with_group(self):
raw = {
"envelope": {"source": "+1234567890"},
"deleteMessage": {
"targetSentTimestamp": 1715000000001,
"groupInfo": {"groupId": "group-abc"},
},
}
from yuxi.channels.adapters.signal.normalize import parse_signal_delete
msg = parse_signal_delete(raw)
assert msg is not None
assert msg.chat_type == ChatType.GROUP
assert msg.identity.channel_chat_id == "group:group-abc"
def test_delete_message_no_delete_message_field(self):
raw = {
"envelope": {"source": "+1234567890"},
"dataMessage": {"timestamp": 1715000000000, "message": "normal"},
}
from yuxi.channels.adapters.signal.normalize import parse_signal_delete
msg = parse_signal_delete(raw)
assert msg is None
class TestSignalSecurityPolicyInvalidTargetType:
def test_add_to_allowlist_invalid_type_raises(self):
policy = SignalSecurityPolicy()
with pytest.raises(ValueError, match="Unknown target_type"):
policy.add_to_allowlist("+1234", "invalid")
def test_remove_from_allowlist_invalid_type_raises(self):
policy = SignalSecurityPolicy()
with pytest.raises(ValueError, match="Unknown target_type"):
policy.remove_from_allowlist("+1234", "invalid")
class TestSignalChannelMediaTypeDispatch:
def test_media_message_types_includes_all_media(self):
from yuxi.channels.adapters.signal.channel import SignalChannel
assert MessageType.IMAGE in SignalChannel.MEDIA_MESSAGE_TYPES
assert MessageType.VIDEO in SignalChannel.MEDIA_MESSAGE_TYPES
assert MessageType.AUDIO in SignalChannel.MEDIA_MESSAGE_TYPES
assert MessageType.FILE in SignalChannel.MEDIA_MESSAGE_TYPES
assert MessageType.TEXT not in SignalChannel.MEDIA_MESSAGE_TYPES
def test_mtype_to_media_mapping(self):
from yuxi.channels.adapters.signal.channel import SignalChannel
assert SignalChannel._MTYPE_TO_MEDIA[MessageType.IMAGE] == "image"
assert SignalChannel._MTYPE_TO_MEDIA[MessageType.VIDEO] == "video"
assert SignalChannel._MTYPE_TO_MEDIA[MessageType.AUDIO] == "audio"
assert SignalChannel._MTYPE_TO_MEDIA[MessageType.FILE] == "file"
class TestSignalChannelFormatOutbound:
def test_basic_text(self):
from yuxi.channels.adapters.signal.channel import SignalChannel
from yuxi.channels.models import ChannelType
channel = SignalChannel()
resp = ChannelResponse(
identity=ChannelIdentity(
channel_id="signal", channel_type=ChannelType.SIGNAL,
channel_user_id="+1234", channel_chat_id="+5678",
),
content="Hello",
)
result = channel.format_outbound(resp)
assert result["recipient"] == "+5678"
assert result["messageBody"] == "Hello"
assert "quoteTimestamp" not in result
def test_with_reply_to(self):
from yuxi.channels.adapters.signal.channel import SignalChannel
from yuxi.channels.models import ChannelType
channel = SignalChannel()
resp = ChannelResponse(
identity=ChannelIdentity(
channel_id="signal", channel_type=ChannelType.SIGNAL,
channel_user_id="+1234", channel_chat_id="+5678",
),
content="Reply",
reply_to_message_id="1715000000000",
)
result = channel.format_outbound(resp)
assert result["quoteTimestamp"] == 1715000000000
def test_with_attachments(self):
from yuxi.channels.adapters.signal.channel import SignalChannel
from yuxi.channels.models import ChannelType
channel = SignalChannel()
resp = ChannelResponse(
identity=ChannelIdentity(
channel_id="signal", channel_type=ChannelType.SIGNAL,
channel_user_id="+1234", channel_chat_id="+5678",
),
content="Image caption",
message_type=MessageType.IMAGE,
attachments=[Attachment(
type="image", mime_type="image/png",
filename="photo.png", file_id="att-001",
)],
)
result = channel.format_outbound(resp)
assert len(result["attachments"]) == 1
assert result["attachments"][0]["contentType"] == "image/png"
assert result["attachments"][0]["filename"] == "photo.png"
assert result["attachments"][0]["id"] == "att-001"
class TestSignalChannelNormalizeInbound:
def test_normalize_message(self):
from yuxi.channels.adapters.signal.channel import SignalChannel
channel = SignalChannel()
raw = {
"envelope": {"source": "+1234"},
"dataMessage": {"timestamp": 1715000000000, "message": "Hello"},
}
msg = channel.normalize_inbound(raw)
assert msg.content == "Hello"
def test_normalize_reaction(self):
from yuxi.channels.adapters.signal.channel import SignalChannel
channel = SignalChannel()
raw = {
"envelope": {"source": "+1234"},
"reaction": {"targetAuthor": "+5678", "emoji": "👍", "targetSentTimestamp": 1715000000000},
}
msg = channel.normalize_inbound(raw)
assert msg.event_type == EventType.MESSAGE_UPDATED
assert "👍" in msg.content
def test_normalize_receipt(self):
from yuxi.channels.adapters.signal.channel import SignalChannel
channel = SignalChannel()
raw = {
"envelope": {"source": "+1234"},
"receiptMessage": {"type": "READ", "timestamps": [1715000000000]},
}
msg = channel.normalize_inbound(raw)
assert msg.event_type == EventType.READ_RECEIPT
def test_normalize_delete(self):
from yuxi.channels.adapters.signal.channel import SignalChannel
channel = SignalChannel()
raw = {
"envelope": {"source": "+1234"},
"deleteMessage": {"targetSentTimestamp": 1715000000000},
}
msg = channel.normalize_inbound(raw)
assert msg.event_type == EventType.MESSAGE_DELETED
def test_unknown_event_raises(self):
from yuxi.channels.adapters.signal.channel import SignalChannel
channel = SignalChannel()
raw = {"envelope": {"source": "+1234"}, "unknownField": {}}
with pytest.raises(ValueError, match="no recognized event found"):
channel.normalize_inbound(raw)
class TestSignalChannelEditMessageVersionCheck:
def test_edit_when_not_supported(self):
import asyncio
from yuxi.channels.adapters.signal.channel import SignalChannel
channel = SignalChannel()
channel._sender = MagicMock()
channel._supports_edit = False
result = asyncio.run(channel.edit_message("+1234", "123", "new content"))
assert result.success is False
assert "Edit not supported" in result.error
class TestSignalChannelCallWithCB:
def test_cb_passes_result(self):
import asyncio
from yuxi.channels.adapters.signal.channel import SignalChannel
from yuxi.channels.models import DeliveryResult
channel = SignalChannel()
channel._sender = MagicMock()
channel._sender.send_text = AsyncMock(return_value=DeliveryResult(success=True, message_id="1"))
async def _test():
return await channel._call_with_cb(
channel._sender.send_text,
recipient="+1234",
message_body="hello",
)
result = asyncio.run(_test())
assert result.success is True
assert result.message_id == "1"
def test_cb_returns_error_on_circuit_breaker_open(self):
import time
import asyncio
from yuxi.channels.adapters.signal.channel import SignalChannel
from yuxi.channels.infra.circuit_breaker import CircuitState
channel = SignalChannel()
channel._sender = MagicMock()
channel._sender.send_text = AsyncMock()
channel._circuit_breaker.state = CircuitState.OPEN
channel._circuit_breaker._last_failure_time = time.monotonic()
async def _test():
return await channel._call_with_cb(
channel._sender.send_text,
recipient="+1234",
message_body="hello",
)
result = asyncio.run(_test())
assert result.success is False
assert "Circuit breaker open" in result.error
class TestSignalChannelSendReadReceipt:
def test_send_read_receipt(self):
import asyncio
from yuxi.channels.adapters.signal.channel import SignalChannel
from yuxi.channels.models import DeliveryResult
channel = SignalChannel()
channel._sender = AsyncMock()
channel._sender.send_read_receipt = AsyncMock(return_value=DeliveryResult(success=True))
result = asyncio.run(channel.send_read_receipt("+1234", "1715000000000"))
assert result.success is True
channel._sender.send_read_receipt.assert_called_once_with(
recipient="+1234", timestamps=[1715000000000],
)
def test_send_read_receipt_no_sender(self):
import asyncio
from yuxi.channels.adapters.signal.channel import SignalChannel
channel = SignalChannel()
result = asyncio.run(channel.send_read_receipt("+1234", "123"))
assert result.success is False
assert "not initialized" in result.error.lower()
class TestSignalChannelSendReactionRemove:
def test_send_reaction_remove(self):
import asyncio
from yuxi.channels.adapters.signal.channel import SignalChannel
from yuxi.channels.models import DeliveryResult
channel = SignalChannel()
channel._sender = AsyncMock()
channel._sender.send_reaction = AsyncMock(return_value=DeliveryResult(success=True))
result = asyncio.run(
channel.send_reaction("+1234", "1715000000000", "👍", remove=True),
)
assert result.success is True
channel._sender.send_reaction.assert_called_once_with(
recipient="+1234",
target_author=channel._account_number,
target_sent_timestamp=1715000000000,
reaction="👍",
remove=True,
)
class TestSignalChannelSendStreamChunkCB:
def test_stream_chunk_first_send_passes_cb(self):
import asyncio
from yuxi.channels.adapters.signal.channel import SignalChannel
from yuxi.channels.models import DeliveryResult
channel = SignalChannel({"block_streaming": True})
channel._sender = AsyncMock()
channel._rpc_client = AsyncMock()
channel._sender.send_text = AsyncMock(
return_value=DeliveryResult(success=True, message_id="msg-001"),
)
result = asyncio.run(
channel.send_stream_chunk("+1234", "", "hello", finished=False),
)
assert result.success is True
assert result.message_id == "msg-001"
def test_stream_chunk_edit_passes_cb(self):
import asyncio
from yuxi.channels.adapters.signal.channel import SignalChannel
from yuxi.channels.models import DeliveryResult
channel = SignalChannel()
channel._sender = AsyncMock()
channel._rpc_client = AsyncMock()
channel._supports_edit = True
channel._sender.edit_message = AsyncMock(
return_value=DeliveryResult(success=True),
)
result = asyncio.run(
channel.send_stream_chunk("+1234", "1715000000000", "updated chunk", finished=False),
)
assert result.success is True
def test_stream_chunk_final_fallback_passes_cb(self):
import asyncio
from yuxi.channels.adapters.signal.channel import SignalChannel
from yuxi.channels.models import DeliveryResult
channel = SignalChannel()
channel._sender = AsyncMock()
channel._rpc_client = AsyncMock()
channel._supports_edit = False
channel._sender.send_text = AsyncMock(
return_value=DeliveryResult(success=True),
)
result = asyncio.run(
channel.send_stream_chunk("+1234", "1715000000000", "final chunk", finished=True),
)
assert result.success is True
class TestSignalChannelSendMediaMetadata:
def test_send_media_passes_filename_and_caption(self):
import asyncio
from yuxi.channels.adapters.signal.channel import SignalChannel
from yuxi.channels.models import DeliveryResult
channel = SignalChannel()
channel._sender = AsyncMock()
channel._sender.send_media = AsyncMock(return_value=DeliveryResult(success=True))
result = asyncio.run(
channel.send_media(
"+1234", "image", b"fake_data",
filename="photo.png", caption="A nice photo",
),
)
assert result.success is True
channel._sender.send_media.assert_called_once_with(
recipient="+1234",
media_data=b"fake_data",
media_type="image",
filename="photo.png",
caption="A nice photo",
)
class TestIsOwnMessage:
def test_own_message_by_number(self):
from yuxi.channels.adapters.signal.normalize import is_own_message
data = {
"envelope": {"source": "+8613800138000"},
"dataMessage": {"timestamp": 1715000000000, "message": "Hello"},
}
assert is_own_message(data, "+8613800138000") is True
assert is_own_message(data, "+99999999999") is False
def test_own_message_by_uuid(self):
from yuxi.channels.adapters.signal.normalize import is_own_message
data = {
"envelope": {"source": "+12345678901", "sourceUuid": "uuid-self-001"},
"dataMessage": {"timestamp": 1715000000000, "message": "Hello"},
}
assert is_own_message(data, "+12345678901", "uuid-self-001") is True
assert is_own_message(data, "+99998888777", "uuid-self-001") is True
assert is_own_message(data, "+12345678901", "uuid-other") is True
def test_not_own_message(self):
from yuxi.channels.adapters.signal.normalize import is_own_message
data = {
"envelope": {"source": "+99998888777", "sourceUuid": "uuid-other"},
"dataMessage": {"timestamp": 1715000000000, "message": "Hello"},
}
assert is_own_message(data, "+12345678901", "uuid-self-001") is False
class TestIsSyncMessage:
def test_sync_message_detected(self):
from yuxi.channels.adapters.signal.normalize import is_sync_message
data = {"syncMessage": {"sentTranscript": {}}}
assert is_sync_message(data) is True
def test_normal_message_not_sync(self):
from yuxi.channels.adapters.signal.normalize import is_sync_message
data = {"envelope": {"source": "+1234"}, "dataMessage": {}}
assert is_sync_message(data) is False
class TestDedup:
def test_dedup_first_seeing(self):
from yuxi.channels.adapters.signal.normalize import build_dedup_key, check_and_add_dedup
data = {
"envelope": {"source": "+1234"},
"dataMessage": {"timestamp": 1715000000000, "message": "Hello"},
}
key = build_dedup_key(data, "+1234567890")
assert key is not None
assert check_and_add_dedup(key) is False
def test_dedup_duplicate(self):
from yuxi.channels.adapters.signal.normalize import build_dedup_key, check_and_add_dedup
data = {
"envelope": {"source": "+99998888999"},
"dataMessage": {"timestamp": 1815000000000, "message": "Hello"},
}
key = build_dedup_key(data, "+1234567890")
assert key is not None
assert check_and_add_dedup(key) is False
assert check_and_add_dedup(key) is True
def test_dedup_key_includes_group_id(self):
from yuxi.channels.adapters.signal.normalize import build_dedup_key
data = {
"envelope": {"source": "+1234"},
"dataMessage": {
"timestamp": 1715000000000,
"message": "Hello",
"groupInfo": {"groupId": "group-abc"},
},
}
key = build_dedup_key(data, "+1234567890")
assert "group-abc" in key
class TestReactionLevel:
def test_off_level_rejects_all(self):
from yuxi.channels.adapters.signal.reaction_level import ReactionLevelController
ctrl = ReactionLevelController("off")
assert ctrl.should_send_reaction("chat-1") is False
assert ctrl.should_send_reaction("chat-1", "\U0001f440") is False
assert ctrl.should_send_auto_ack("chat-1") is False
def test_ack_level_only_allows_eyes(self):
from yuxi.channels.adapters.signal.reaction_level import ReactionLevelController
ctrl = ReactionLevelController("ack")
assert ctrl.should_send_reaction("chat-1", "\U0001f440") is True
assert ctrl.should_send_reaction("chat-1", "\U0001f44d") is False
def test_minimal_level_allows_all(self):
from yuxi.channels.adapters.signal.reaction_level import ReactionLevelController
ctrl = ReactionLevelController("minimal")
assert ctrl.should_send_reaction("chat-1", "\U0001f44d") is True
def test_auto_ack_only_once(self):
from yuxi.channels.adapters.signal.reaction_level import ReactionLevelController
ctrl = ReactionLevelController("minimal")
assert ctrl.should_send_auto_ack("chat-1") is True
assert ctrl.should_send_auto_ack("chat-1") is False
ctrl.reset_ack("chat-1")
assert ctrl.should_send_auto_ack("chat-1") is True
class TestRequireMention:
def test_require_mention_without_mention(self):
from yuxi.channels.adapters.signal.security import SignalSecurityPolicy
from yuxi.channels.models import ChannelIdentity, ChannelMessage, ChatType
policy = SignalSecurityPolicy(require_mention=True)
msg = ChannelMessage(
identity=ChannelIdentity(
channel_id="signal", channel_type="signal",
channel_user_id="+1234", channel_chat_id="group:abc",
),
content="hello",
chat_type=ChatType.GROUP,
)
assert policy.check_require_mention(msg) is False
def test_require_mention_with_mention(self):
from yuxi.channels.adapters.signal.security import SignalSecurityPolicy
from yuxi.channels.models import ChannelIdentity, ChannelMessage, ChatType, MentionsInfo
policy = SignalSecurityPolicy(require_mention=True)
msg = ChannelMessage(
identity=ChannelIdentity(
channel_id="signal", channel_type="signal",
channel_user_id="+1234", channel_chat_id="group:abc",
),
content="hello @bot",
chat_type=ChatType.GROUP,
mentions=MentionsInfo(
mentioned_user_ids=["uuid-bot"],
is_bot_mentioned=True,
),
)
assert policy.check_require_mention(msg) is True
def test_require_mention_dm_always_passes(self):
from yuxi.channels.adapters.signal.security import SignalSecurityPolicy
from yuxi.channels.models import ChannelIdentity, ChannelMessage, ChatType
policy = SignalSecurityPolicy(require_mention=True)
msg = ChannelMessage(
identity=ChannelIdentity(
channel_id="signal", channel_type="signal",
channel_user_id="+1234", channel_chat_id="+1234",
),
content="hello",
chat_type=ChatType.DIRECT,
)
assert policy.check_require_mention(msg) is True
class TestReactionNotification:
def test_off_policy(self):
from yuxi.channels.adapters.signal.security import SignalSecurityPolicy
policy = SignalSecurityPolicy(reaction_notifications="off")
assert policy.check_reaction_notification("+1234") is False
def test_all_policy(self):
from yuxi.channels.adapters.signal.security import SignalSecurityPolicy
policy = SignalSecurityPolicy(reaction_notifications="all")
assert policy.check_reaction_notification("+1234") is True
def test_allowlist_policy(self):
from yuxi.channels.adapters.signal.security import SignalSecurityPolicy
policy = SignalSecurityPolicy(
reaction_notifications="allowlist",
reaction_allowlist=["+1234"],
)
assert policy.check_reaction_notification("+1234") is True
assert policy.check_reaction_notification("+9999") is False
class TestConfigOverrides:
def test_text_chunk_limit_from_config(self):
from yuxi.channels.adapters.signal.channel import SignalChannel
channel = SignalChannel({"text_chunk_limit": 2000})
assert channel.text_chunk_limit == 2000
def test_media_max_mb_from_config(self):
from yuxi.channels.adapters.signal.channel import SignalChannel
channel = SignalChannel({"media_max_mb": 8})
assert channel.max_media_size_mb == 8
def test_enabled_false(self):
from yuxi.channels.adapters.signal.channel import SignalChannel
channel = SignalChannel({"enabled": False, "signal_number": "+1234"})
assert channel._enabled is False
class TestE164Validation:
def test_valid_e164(self):
from yuxi.channels.adapters.signal.setup import validate_e164
assert validate_e164("+1234567890") is True
assert validate_e164("+8613800138000") is True
assert validate_e164("+15551234567") is True
def test_invalid_e164(self):
from yuxi.channels.adapters.signal.setup import validate_e164
assert validate_e164("123") is False
assert validate_e164("+12") is False
assert validate_e164("not-a-number") is False
class TestBlockStreaming:
def test_block_streaming_disabled_coalesces(self):
import asyncio
from yuxi.channels.adapters.signal.channel import SignalChannel
from yuxi.channels.models import DeliveryResult
channel = SignalChannel({"block_streaming": False})
channel._sender = AsyncMock()
channel._rpc_client = AsyncMock()
channel._sender.send_text = AsyncMock(
return_value=DeliveryResult(success=True, message_id="msg-001"),
)
result = asyncio.run(
channel.send_stream_chunk("+1234", "", "hello world", finished=False),
)
assert result.success is True
def test_block_streaming_enabled_sends_immediately(self):
import asyncio
from yuxi.channels.adapters.signal.channel import SignalChannel
from yuxi.channels.models import DeliveryResult
channel = SignalChannel({"block_streaming": True})
channel._sender = AsyncMock()
channel._rpc_client = AsyncMock()
channel._sender.send_text = AsyncMock(
return_value=DeliveryResult(success=True, message_id="msg-001"),
)
result = asyncio.run(
channel.send_stream_chunk("+1234", "", "hello world", finished=False),
)
assert result.success is True
class TestLooksLikeSignalTargetId:
def test_e164(self):
from yuxi.channels.adapters.signal.normalize import looks_like_signal_target_id
assert looks_like_signal_target_id("+8613800138000") is True
def test_group_prefix(self):
from yuxi.channels.adapters.signal.normalize import looks_like_signal_target_id
assert looks_like_signal_target_id("group:abc123") is True
def test_uuid_prefix(self):
from yuxi.channels.adapters.signal.normalize import looks_like_signal_target_id
assert looks_like_signal_target_id("uuid:abcd-1234") is True
def test_number_without_plus(self):
from yuxi.channels.adapters.signal.normalize import looks_like_signal_target_id
assert looks_like_signal_target_id("8613800138000") is True
def test_invalid_short(self):
from yuxi.channels.adapters.signal.normalize import looks_like_signal_target_id
assert looks_like_signal_target_id("123") is False
def test_plain_text(self):
from yuxi.channels.adapters.signal.normalize import looks_like_signal_target_id
assert looks_like_signal_target_id("hello world") is False
class TestFormatMarkdownToSignalStyles:
def test_bold_style(self):
from yuxi.channels.adapters.signal.format import markdown_to_signal_styles
result = markdown_to_signal_styles("Hello **world**")
assert result is not None
assert any(s.style == "BOLD" for s in result.styles)
def test_italic_style(self):
from yuxi.channels.adapters.signal.format import markdown_to_signal_styles
result = markdown_to_signal_styles("Hello *world*")
assert result is not None
assert any(s.style == "ITALIC" for s in result.styles)
def test_strikethrough_style(self):
from yuxi.channels.adapters.signal.format import markdown_to_signal_styles
result = markdown_to_signal_styles("Hello ~~world~~")
assert result is not None
assert any(s.style == "STRIKETHROUGH" for s in result.styles)
def test_monospace_style(self):
from yuxi.channels.adapters.signal.format import markdown_to_signal_styles
result = markdown_to_signal_styles("Hello `world`")
assert result is not None
assert any(s.style == "MONOSPACE" for s in result.styles)
def test_mixed_styles(self):
from yuxi.channels.adapters.signal.format import markdown_to_signal_styles
result = markdown_to_signal_styles("**bold** and *italic* and ~~strike~~")
assert result is not None
style_names = {s.style for s in result.styles}
assert "BOLD" in style_names
assert "ITALIC" in style_names
assert "STRIKETHROUGH" in style_names
def test_table_to_bullets(self):
from yuxi.channels.adapters.signal.format import markdown_to_signal_styles
result = markdown_to_signal_styles(
"| Col1 | Col2 |\n|------|------|\n| A | B |",
table_mode="bullets",
)
assert result is not None
assert "A" in result.body or len(result.body) > 0
def test_split_text_newline_mode(self):
from yuxi.channels.adapters.signal.format import split_text
chunks = split_text("Hello\n\nWorld", 4000, "newline")
assert len(chunks) >= 1
def test_split_text_length_mode(self):
from yuxi.channels.adapters.signal.format import split_text
long_text = "A" * 5000
chunks = split_text(long_text, 4000, "length")
assert len(chunks) == 2
def test_split_text_short(self):
from yuxi.channels.adapters.signal.format import split_text
chunks = split_text("Hello", 4000, "newline")
assert chunks == ["Hello"]
def test_clamp_styles(self):
from yuxi.channels.adapters.signal.format import StyleRange, clamp_styles_to_length
styles = [StyleRange(start=5, length=10, style="BOLD")]
result = clamp_styles_to_length(styles, "0123456789") # 10 chars
assert len(result) >= 0
class TestRpcClient:
def test_rpc_client_connect(self):
from unittest.mock import AsyncMock, patch
with patch("aiohttp.ClientSession"):
from yuxi.channels.adapters.signal.client import RpcClient
client = RpcClient("http://127.0.0.1:8080")
assert client.base_url == "http://127.0.0.1:8080"
def test_validate_daemon_url_loopback(self):
from yuxi.channels.adapters.signal.client import _validate_daemon_url
_validate_daemon_url("http://127.0.0.1:8080")
def test_validate_daemon_url_localhost(self):
from yuxi.channels.adapters.signal.client import _validate_daemon_url
_validate_daemon_url("http://localhost:8080")
def test_validate_daemon_url_private(self):
from yuxi.channels.adapters.signal.client import _validate_daemon_url
_validate_daemon_url("http://192.168.1.1:8080")
def test_validate_daemon_url_remote_rejected(self):
from yuxi.channels.adapters.signal.client import _validate_daemon_url
import pytest
with pytest.raises(ValueError, match="SSRF guard"):
_validate_daemon_url("http://93.184.216.34:8080")
def test_rate_limit_detection(self):
from yuxi.channels.adapters.signal.client import _detect_rate_limit
result = _detect_rate_limit("Rate limit exceeded", {"retry_after_seconds": 30})
assert result is not None
assert result.retry_after_seconds == 30
def test_rate_limit_no_match(self):
from yuxi.channels.adapters.signal.client import _detect_rate_limit
result = _detect_rate_limit("Unknown error", {})
assert result is None
def test_unauthorized_error(self):
from yuxi.channels.adapters.signal.client import UnauthorizedError
err = UnauthorizedError()
assert err.code == -32001
assert "Unauthorized" in str(err)
class TestMonitorEventRouting:
def test_is_message_event(self):
from yuxi.channels.adapters.signal.monitor import SSEMonitor
data = {"envelope": {"source": "+1234"}, "dataMessage": {"message": "hello"}}
assert SSEMonitor._is_message_event(data) is True
def test_is_not_message_event(self):
from yuxi.channels.adapters.signal.monitor import SSEMonitor
data = {"envelope": {"source": "+1234"}}
assert SSEMonitor._is_message_event(data) is False
def test_is_edit_message_event(self):
from yuxi.channels.adapters.signal.monitor import SSEMonitor
data = {
"envelope": {
"source": "+1234",
"editMessage": {"dataMessage": {"message": "edited"}},
},
}
assert SSEMonitor._is_edit_message_event(data) is True
def test_is_reaction_event(self):
from yuxi.channels.adapters.signal.monitor import SSEMonitor
data = {
"envelope": {"source": "+1234"},
"reaction": {"emoji": "👍", "targetSentTimestamp": 123},
}
assert SSEMonitor._is_reaction_event(data) is True
def test_is_delete_event(self):
from yuxi.channels.adapters.signal.monitor import SSEMonitor
data = {
"envelope": {"source": "+1234"},
"deleteMessage": {"targetSentTimestamp": 123},
}
assert SSEMonitor._is_delete_event(data) is True
def test_is_receipt_event(self):
from yuxi.channels.adapters.signal.monitor import SSEMonitor
data = {
"envelope": {"source": "+1234"},
"receiptMessage": {"type": "READ", "timestamps": [123]},
}
assert SSEMonitor._is_receipt_event(data) is True
def test_is_story_event(self):
from yuxi.channels.adapters.signal.monitor import SSEMonitor
data = {
"envelope": {"source": "+1234"},
"storyMessage": {},
}
assert SSEMonitor._is_story_event(data) is True
def test_extract_edit_data(self):
from yuxi.channels.adapters.signal.monitor import SSEMonitor
data = {
"envelope": {
"source": "+1234",
"editMessage": {"dataMessage": {"timestamp": 123, "message": "new"}},
},
}
extracted = SSEMonitor._extract_edit_data(data)
assert extracted["dataMessage"]["message"] == "new"
class TestMessageActions:
def test_describe_all_actions(self):
from yuxi.channels.adapters.signal.message_actions import (
SIGNAL_MESSAGE_ACTIONS,
describe_message_tools,
)
tools = describe_message_tools()
assert len(tools) == len(SIGNAL_MESSAGE_ACTIONS)
def test_describe_filter_actions(self):
from yuxi.channels.adapters.signal.message_actions import describe_message_tools
enabled = {"send": True, "react": False}
tools = describe_message_tools(enabled)
names = {t["name"] for t in tools}
def test_action_definitions(self):
from yuxi.channels.adapters.signal.message_actions import SIGNAL_MESSAGE_ACTIONS
assert "send" in SIGNAL_MESSAGE_ACTIONS
assert "react" in SIGNAL_MESSAGE_ACTIONS
assert "edit" in SIGNAL_MESSAGE_ACTIONS
assert "delete" in SIGNAL_MESSAGE_ACTIONS
assert "send_media" in SIGNAL_MESSAGE_ACTIONS
assert "send_typing" in SIGNAL_MESSAGE_ACTIONS
assert "send_read_receipt" in SIGNAL_MESSAGE_ACTIONS
assert "get_user_info" in SIGNAL_MESSAGE_ACTIONS
class TestExecAuth:
def test_auto_approve(self):
import asyncio
from yuxi.channels.adapters.signal.exec_auth import ExecAuthAdapter, ExecAuthResult
adapter = ExecAuthAdapter(auto_approve=True)
result = asyncio.run(
adapter.request_approval("delete", {}, "+1234"),
)
assert result == ExecAuthResult.APPROVED
def test_request_pending(self):
import asyncio
from yuxi.channels.adapters.signal.exec_auth import ExecAuthAdapter, ExecAuthResult
adapter = ExecAuthAdapter()
result = asyncio.run(
adapter.request_approval("delete", {}, "+1234"),
)
assert result == ExecAuthResult.PENDING
pending = adapter.get_pending()
assert len(pending) == 1
assert pending[0]["action"] == "delete"
def test_approve(self):
import asyncio
from yuxi.channels.adapters.signal.exec_auth import ExecAuthAdapter, ExecAuthResult
adapter = ExecAuthAdapter()
asyncio.run(adapter.request_approval("delete", {}, "+1234"))
result = adapter.approve("exec_auth_1")
assert result == ExecAuthResult.APPROVED
def test_deny(self):
import asyncio
from yuxi.channels.adapters.signal.exec_auth import ExecAuthAdapter, ExecAuthResult
adapter = ExecAuthAdapter()
asyncio.run(adapter.request_approval("delete", {}, "+1234"))
result = adapter.deny("exec_auth_1")
assert result == ExecAuthResult.DENIED
def test_deny_nonexistent(self):
from yuxi.channels.adapters.signal.exec_auth import ExecAuthAdapter, ExecAuthResult
adapter = ExecAuthAdapter()
result = adapter.deny("nonexistent")
assert result == ExecAuthResult.DENIED
def test_pending_empty(self):
from yuxi.channels.adapters.signal.exec_auth import ExecAuthAdapter
adapter = ExecAuthAdapter()
pending = adapter.get_pending()
assert len(pending) == 0
class TestEntityCache:
def test_set_and_get(self):
import asyncio
from yuxi.channels.adapters.signal.entity_cache import EntityCache
cache = EntityCache(ttl=60, max_size=10)
async def _test():
await cache.set("key1", {"name": "Alice"})
result = await cache.get("key1")
assert result == {"name": "Alice"}
asyncio.run(_test())
def test_expired_ttl(self):
import asyncio
from yuxi.channels.adapters.signal.entity_cache import EntityCache
cache = EntityCache(ttl=-1, max_size=10)
async def _test():
await cache.set("key1", "value")
result = await cache.get("key1")
assert result is None
asyncio.run(_test())
def test_lru_eviction(self):
import asyncio
from yuxi.channels.adapters.signal.entity_cache import EntityCache
cache = EntityCache(ttl=600, max_size=3)
async def _test():
await cache.set("k1", "v1")
await cache.set("k2", "v2")
await cache.set("k3", "v3")
await cache.set("k4", "v4")
assert await cache.get("k1") is None
assert await cache.get("k2") == "v2"
asyncio.run(_test())
def test_delete(self):
import asyncio
from yuxi.channels.adapters.signal.entity_cache import EntityCache
cache = EntityCache()
async def _test():
await cache.set("key1", "value")
await cache.delete("key1")
assert await cache.get("key1") is None
asyncio.run(_test())
def test_clear(self):
import asyncio
from yuxi.channels.adapters.signal.entity_cache import EntityCache
cache = EntityCache()
async def _test():
await cache.set("k1", "v1")
await cache.set("k2", "v2")
await cache.clear()
assert await cache.get("k1") is None
asyncio.run(_test())
def test_stats(self):
import asyncio
from yuxi.channels.adapters.signal.entity_cache import EntityCache
cache = EntityCache()
async def _test():
await cache.set("k1", "v1")
s = await cache.stats()
assert s["size"] == 1
assert s["max_size"] == 500
assert s["ttl"] == 600
asyncio.run(_test())
class TestOrderedEventQueue:
def test_push_and_process(self):
import asyncio
from yuxi.channels.adapters.signal.event_queue import OrderedEventQueue
events_received = []
async def handler(event):
events_received.append(event)
queue = OrderedEventQueue(max_size=10)
async def _test():
queue.set_handler(handler)
await queue.push({"msg": "hello"}, timestamp=100)
await queue.start()
await asyncio.sleep(0.1)
await queue.stop()
assert len(events_received) == 1
assert events_received[0] == {"msg": "hello"}
asyncio.run(_test())
def test_ordered_delivery(self):
import asyncio
from yuxi.channels.adapters.signal.event_queue import OrderedEventQueue
events_received = []
async def handler(event):
events_received.append(event)
queue = OrderedEventQueue(max_size=10)
async def _test():
queue.set_handler(handler)
await queue.push({"msg": "c"}, timestamp=300)
await queue.push({"msg": "a"}, timestamp=100)
await queue.push({"msg": "b"}, timestamp=200)
await queue.start()
await asyncio.sleep(0.1)
await queue.stop()
msgs = [e["msg"] for e in events_received]
assert msgs == ["a", "b", "c"]
asyncio.run(_test())
def test_size(self):
import asyncio
from yuxi.channels.adapters.signal.event_queue import OrderedEventQueue
queue = OrderedEventQueue(max_size=10)
async def _test():
await queue.push({"msg": "1"}, timestamp=1)
await queue.push({"msg": "2"}, timestamp=2)
assert queue.size == 2
asyncio.run(_test())
class TestDisplayName:
def test_source_name_extracted(self):
from yuxi.channels.adapters.signal.normalize import parse_signal_message
raw = {
"envelope": {"source": "+1234567890", "sourceName": "Alice"},
"dataMessage": {
"timestamp": 1715000000000,
"message": "Hello",
},
}
msg = parse_signal_message(raw)
assert msg is not None
assert msg.metadata.get("sender_display_name") == "Alice"
def test_source_name_fallback_to_number(self):
from yuxi.channels.adapters.signal.normalize import parse_signal_message
raw = {
"envelope": {"source": "+1234567890"},
"dataMessage": {
"timestamp": 1715000000000,
"message": "Hello",
},
}
msg = parse_signal_message(raw)
assert msg is not None
assert "sender_display_name" not in msg.metadata
class TestConfigWrites:
def test_allow_config_writes(self):
from yuxi.channels.adapters.signal.channel import SignalChannel
channel = SignalChannel({"config_writes": True})
assert channel.allow_config_writes() is True
def test_deny_config_writes(self):
from yuxi.channels.adapters.signal.channel import SignalChannel
channel = SignalChannel({"config_writes": False})
assert channel.allow_config_writes() is False
def test_deny_default(self):
from yuxi.channels.adapters.signal.channel import SignalChannel
channel = SignalChannel()
assert channel.allow_config_writes() is False
class TestDirectorySearch:
def test_match_query_by_number(self):
from yuxi.channels.adapters.signal.directory import _match_query
entry = {"number": "+8613800138000", "name": "Test"}
assert _match_query(entry, "+86138") is True
def test_match_query_by_name(self):
from yuxi.channels.adapters.signal.directory import _match_query
entry = {"number": "+8613800138000", "name": "Alice"}
assert _match_query(entry, "alice") is True
def test_match_query_no_match(self):
from yuxi.channels.adapters.signal.directory import _match_query
entry = {"number": "+8613800138000", "name": "Alice"}
assert _match_query(entry, "Bob") is False
class TestReactionLevelIntegration:
def test_send_reaction_respects_off_level(self):
import asyncio
from yuxi.channels.adapters.signal.channel import SignalChannel
from yuxi.channels.models import DeliveryResult
channel = SignalChannel({"reaction_level": "off"})
channel._sender = AsyncMock()
result = asyncio.run(
channel.send_reaction("+1234", "123", "👍"),
)
assert result.success is True
class TestConfigSchema:
def test_new_config_items(self):
from yuxi.channels.adapters.signal.config_schema import SIGNAL_CONFIG_SCHEMA
assert "allow_remote_daemon" in SIGNAL_CONFIG_SCHEMA
assert "download_max_mb" in SIGNAL_CONFIG_SCHEMA
assert "entity_cache_ttl" in SIGNAL_CONFIG_SCHEMA
assert "event_queue_max_size" in SIGNAL_CONFIG_SCHEMA
assert "prefer_native_binary" in SIGNAL_CONFIG_SCHEMA
class TestProbeResult:
def test_signal_probe_result_defaults(self):
from yuxi.channels.adapters.signal.probe import SignalProbeResult
result = SignalProbeResult()
assert result.status == "unknown"
assert result.success is False
def test_signal_probe_result_healthy(self):
from yuxi.channels.adapters.signal.probe import SignalProbeResult
result = SignalProbeResult(status="healthy", version="1.0", latency_ms=15.0)
assert result.success is True
assert result.version == "1.0"
assert result.latency_ms == 15.0
def test_signal_probe_result_unhealthy(self):
from yuxi.channels.adapters.signal.probe import SignalProbeResult
result = SignalProbeResult(status="unhealthy", error_type="TIMEOUT", latency_ms=7500)
assert result.success is False